Skip to content

Introduce modern Asgardeo Nuxt.js SDK#477

Merged
brionmario merged 44 commits intoasgardeo:mainfrom
kavindadimuthu:nuxt-sdk-start-without-playground
Apr 28, 2026
Merged

Introduce modern Asgardeo Nuxt.js SDK#477
brionmario merged 44 commits intoasgardeo:mainfrom
kavindadimuthu:nuxt-sdk-start-without-playground

Conversation

@kavindadimuthu
Copy link
Copy Markdown
Contributor

@kavindadimuthu kavindadimuthu commented Apr 27, 2026

Purpose

This pull request introduces modern Nuxt.Js SDK with nuxt 3 for asgardeo integrations as @asgardeo/nuxt package, focusing on better configuration handling, expanded Nuxt integration, and enhanced developer ergonomics. The most important changes include a major refactor of the Nuxt module setup for more robust config layering and security, auto-registration of a full suite of Nuxt components and composables, and several utility and bugfix improvements in the JavaScript SDK.

Nuxt module implementations:

  • Refactored the Nuxt module (src/module.ts) to support layered configuration (environment variables, user config, and hard defaults), enforce secrets never leak to public config, and register all server API routes, plugins, middleware, and a comprehensive set of Nuxt-specific components and composables automatically. This enables a plug-and-play developer experience and ensures secure, flexible configuration.
  • Updated package.json for @asgardeo/nuxt to expose new exports for server, errors, and utils, added new dependencies and peer dependencies, and bumped the version to 0.1.0-alpha.0. [1] [2] [3] [4]
  • Enhanced the Nuxt entrypoint (src/index.ts) to re-export all major types, composables, and errors, and to provide Nuxt-specific wrappers for navigation and SSR support.

JavaScript SDK improvements and bugfixes:

  • Fixed a bug in updateMeProfile so that user profile updates now strip the userstore prefix from userName, ensuring UI consistency after optimistic updates. [1] [2]
  • Improved generateFlattenedUserProfile utility to correctly handle SCIM2 multi-valued complex attributes by falling back to array filtering when the flattened schema key is not found.

These changes collectively make the Nuxt integration more robust, secure, and user-friendly, while also addressing important edge cases in the core SDK utilities.

Related Issues

Related PRs

  • N/A

Checklist

  • Followed the CONTRIBUTING guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
  • Unit tests provided. (Add links if there are any)

Security checks

Summary by CodeRabbit

  • New Features
    • Full Nuxt 3 module with SSR session handling, embedded and redirect auth (sign-in/sign-up/sign-out), AsgardeoRoot, Nuxt-aware composable, auth middleware, many UI components (sign-in/out/up, user/profile, org controls, loading/signed-in/out), and server API endpoints for auth, session, profile, organizations, and branding.
  • Bug Fixes
    • Normalize returned usernames and improve flattened profile extraction for dotted/multi-valued schema fields.
  • Documentation
    • Expanded Nuxt README with setup, configuration, and usage guidance.
  • Chores
    • Removed Vue SDK playground; added Nuxt sample app.

kavindadimuthu and others added 30 commits April 27, 2026 15:39
- Updated pnpm workspace configuration to include nuxt-sdk-playground.
- Created .gitignore for nuxt-sdk-playground to exclude unnecessary files.
- Added README.md for setup instructions and environment variables.
- Implemented main application layout in app.vue with authentication controls.
- Added global styles in main.css for consistent UI.
- Configured nuxt.config.ts for Nuxt 3 with Asgardeo module integration.
- Created package.json with necessary dependencies and scripts for the playground.
- Developed index.vue as the main landing page with authentication status display.
- Created protected.vue to demonstrate access control based on authentication.
- Added TypeScript configuration for the playground.
…ment

- Add route middleware for authentication to protect pages.
- Implement returnTo functionality for redirecting users after sign-in.
- Introduce server utilities for session management: useServerSession and requireServerSession.
- Create a route matcher utility for advanced middleware handling.
- Update API routes to support session retrieval and token management.
- Enhance the SDK playground with new API demo page and improved user experience.
…ession manager, and URL validation

- Implemented a minimal stub for Nuxt's `#app` virtual module to facilitate unit testing.
- Created tests for `defineAsgardeoMiddleware` to validate authentication flows for signed-in and unauthenticated users.
- Added tests for error codes and the `AsgardeoError` class to ensure consistent error handling.
- Developed tests for the `createRouteMatcher` utility to verify path matching behavior.
- Implemented tests for session token creation and verification, including handling of tampered tokens.
- Added URL validation tests to prevent open redirect vulnerabilities.
- Configured Vitest for testing environment and module resolution.
- Updated AsgardeoPlugin to support delegated mode for SSR environments.
- Create .env.example for environment variables configuration.
- Update .gitignore to exclude sensitive files and logs.
- Revise README.md to provide clearer setup instructions and environment variable descriptions.
- Refactor app.vue to utilize Asgardeo components for sign-in/out functionality.
- Remove unused main.css file and styles.
- Update nuxt.config.ts for improved configuration and environment variable handling.
- Simplify package.json scripts and dependencies.
- Delete api-demo.vue and protected.vue pages; add dashboard.vue for protected content.
- Revise index.vue to enhance user experience with loading states and authentication checks.
- Remove deprecated server API route for user session access.
- Introduced `AsgardeoNuxtClient` for improved client handling.
- Replaced `useAsgardeoServerClient` with direct client instance usage.
- Added proactive access token refresh logic in `getValidAccessToken`.
- Enhanced session token creation to include access token expiry, refresh token, and ID token.
- Updated auth state resolution to initialize the client on the first request.
- Removed legacy client utility functions and streamlined imports.
- Updated tests to cover new session management features and token refresh logic.
- Modified playground sample to utilize new client structure.

Co-authored-by: Copilot <copilot@github.com>
…ion management

- Introduced a new Nitro plugin for SSR that initializes the Asgardeo client and manages user session verification.
- Added routes for fetching user organizations, all organizations, user profiles, and updating user profiles.
- Implemented organization switching functionality with session management.
- Enhanced state management for user profiles and organizations in the Nuxt plugin.
- Added comprehensive error handling for API routes and improved logging.
- Updated types to support new features and ensure type safety across the integration.

Co-authored-by: Copilot <copilot@github.com>
- Introduced OrganizationProfile, OrganizationSwitcher, SignIn, SignInButton, SignOutButton, SignUp, SignUpButton, SignedIn, SignedOut, User, UserDropdown, and UserProfile components to enhance user authentication and organization management.
- Implemented SSR-safe navigation using Nuxt's navigateTo for sign-in, sign-out, and sign-up flows.
- Updated types to include theme mode preferences in AsgardeoNuxtConfig.
- Modified playground sample configuration to reflect new environment variables and preferences for theme and user fetching.
Co-authored-by: Copilot <copilot@github.com>
…rences to asgardeo-ssr for clarity in context augmentation
…ment and user session handling

- Added new API routes for managing organizations, including creating, fetching, and switching organizations.
- Implemented session management routes for signing in, signing out, and retrieving user session information.
- Enhanced the AsgardeoNuxtClient with methods for organization creation and retrieval.
- Updated the sign-out process to fetch the redirect URL from the server.
- Introduced branding preference retrieval based on the current organization context.
- Added user profile management routes for fetching and updating user information.

Co-authored-by: Copilot <copilot@github.com>
…yground

- Implemented new pages for user profile retrieval and update, including GET /api/auth/user/profile and PATCH /api/auth/user/profile.
- Added organization management routes: GET /api/auth/organizations/me and POST /api/auth/organizations/switch.
- Created session management routes: GET /api/auth/session, GET /api/auth/token, and sign-in/sign-out functionalities.
- Introduced utility functions in sdkRoutes.ts for easier API calls.
- Enhanced user experience with loading states and error handling in the UI components.
- Created `profile-patch.vue` for updating user profiles via SCIM2 PatchOp.
- Implemented `user.vue` for fetching basic user information from the current session.
- Added utility overview page at `utilities/index.vue` to showcase server utilities.
- Introduced `session.vue` for managing session data and fetching session information.
- Developed `token.vue` to handle access token management and fetching token metadata.
- Created `userinfo.vue` for fetching SCIM2 user profiles using AsgardeoNuxtClient.
- Established a manifest file `sdk-manifest.ts` to serve as a single source of truth for SDK exports and utilities.

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
…sdk capabilities

- Introduced a new context utility in `context.vue` to fetch Asgardeo context via the demo API.
- Updated `index.vue` to clarify the need for explicit imports for server utilities.
- Enhanced `session.vue` to reflect explicit import requirements for session handling.
- Modified `token.vue` to require explicit imports for token management functions.
- Removed `userinfo.vue` as it was redundant and not aligned with the current API structure.
- Updated `preferences.vue` to streamline preference handling and removed SSR state checks.
- Improved `state.vue` to warn against using internal SDK state keys directly.
- Created a new demo API route for context retrieval in `context.get.ts`.
- Refined session and token API routes to reflect explicit import changes.
- Added manifest alignment tests to ensure all exports are correctly referenced.
- Updated SDK manifest to reflect changes in server utilities and middleware.
- Adjusted SDK routes to rename organization switch parameter for clarity.
- Added Vitest configuration for testing the playground.
- Introduced SignUpOptionFactory for V1 sign-up flow rendering.
- Created BaseSignUp and SignUp components for V2 sign-up flow handling.
- Implemented callback page for embedded authentication flows in Nuxt.js.
- Enhanced error handling and form validation for sign-up processes.
- Added support for passkey registration and social login redirection.
…in nuxtjs sdk playground

- Deleted `preferences.vue` and `state.vue` pages to streamline the SDK playground.
- Introduced `token-debugger.vue` for inspecting and decoding tokens in the current auth flow.
- Added `playground-overrides.client.ts` to manage theme mode persistence in localStorage.
- Created server API endpoint `tokens.get.ts` to expose auth tokens for debugging in development.
- Implemented JWT decoding utility functions in `decodeJwt.ts`.
- Added localStorage helpers in `playground-overrides.ts` for managing runtime overrides.
- Introduced time formatting utilities in `timeFormat.ts` for better token expiration display.

Co-authored-by: Copilot <copilot@github.com>
…ersisted preferences to eliminate flash-of-wrong-theme during hydration
…cution

- Updated `theme.vue` and `user.vue` to utilize a new composable specification structure for improved state inspection and function execution.
- Introduced `composables-manifest.ts` to define and manage composable specifications, including state and function details.
- Refactored state display in `theme.vue` and `user.vue` to use `StateInspectionTable` for better organization and clarity.
- Added function execution handling in `user.vue` for `updateProfile` and `revalidateProfile` with loading and error states.
- Updated SDK manifest to include `useFlowMeta` composable for flow metadata management.

Co-authored-by: Copilot <copilot@github.com>
…outing and example environment configuration
- Introduced `ComponentCard.vue` for displaying component details and previews.
- Added `ComponentTabNav.vue` for navigation between different components.
- Created `PreviewCanvas.vue` to provide a canvas for component previews.
- Implemented `PreviewSignInGate.vue` to handle sign-in requirements for previews.
- Developed `SlotInspection.vue` for inspecting slot variants and payloads.
- Added `PropsCustomizer.vue` for customizing component props dynamically.
- Created `SlotVariantPicker.vue` for selecting slot variants.
- Implemented various control components: `BooleanControl.vue`, `ChipMultiSelectControl.vue`, `JsonControl.vue`, `SelectControl.vue`, and `StringControl.vue` for prop customization.
- Added `_sandbox.vue` page for testing components in a sandbox environment.
- Established `components-manifest.ts` to define component specifications and properties.
- Updated user.vue to enhance component structure and improve user data handling.
- Removed auto-import badges from various composable and middleware pages for a cleaner UI.
- Streamlined code snippets and descriptions for better clarity and usability.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@kavindadimuthu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 48 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4c5e24e-880f-4fcc-a3dc-15d711d57563

📥 Commits

Reviewing files that changed from the base of the PR and between db4636c and 6bae96f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (74)
  • packages/nuxt/package.json
  • packages/nuxt/src/index.ts
  • packages/nuxt/src/module.ts
  • packages/nuxt/src/runtime/components/AsgardeoRoot.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
  • packages/nuxt/src/runtime/components/actions/SignOutButton.ts
  • packages/nuxt/src/runtime/components/actions/SignUpButton.ts
  • packages/nuxt/src/runtime/components/auth/Callback.ts
  • packages/nuxt/src/runtime/components/auth/SignIn.ts
  • packages/nuxt/src/runtime/components/auth/SignUp.ts
  • packages/nuxt/src/runtime/components/control/Loading.ts
  • packages/nuxt/src/runtime/components/control/SignedIn.ts
  • packages/nuxt/src/runtime/components/control/SignedOut.ts
  • packages/nuxt/src/runtime/components/organization/CreateOrganization.ts
  • packages/nuxt/src/runtime/components/organization/Organization.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationList.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts
  • packages/nuxt/src/runtime/components/user/User.ts
  • packages/nuxt/src/runtime/components/user/UserDropdown.ts
  • packages/nuxt/src/runtime/components/user/UserProfile.ts
  • packages/nuxt/src/runtime/composables/useAsgardeo.ts
  • packages/nuxt/src/runtime/errors/asgardeo-error.ts
  • packages/nuxt/src/runtime/errors/error-codes.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/src/runtime/plugins/asgardeo.ts
  • packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts
  • packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts
  • packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts
  • packages/nuxt/src/runtime/server/utils/event-context.ts
  • packages/nuxt/src/runtime/server/utils/serverSession.ts
  • packages/nuxt/src/runtime/server/utils/session.ts
  • packages/nuxt/src/runtime/server/utils/token-refresh.ts
  • packages/nuxt/src/runtime/types.ts
  • packages/nuxt/src/runtime/types/augments.d.ts
  • packages/nuxt/src/runtime/utils/createRouteMatcher.ts
  • packages/nuxt/src/runtime/utils/log.ts
  • packages/nuxt/src/runtime/utils/url-validation.ts
  • packages/nuxt/tests/__mocks__/nuxt-app.ts
  • packages/nuxt/tests/unit/asgardeo-root.test.ts
  • packages/nuxt/tests/unit/asgardeo-ssr.test.ts
  • packages/nuxt/tests/unit/callback-post.test.ts
  • packages/nuxt/tests/unit/define-asgardeo-middleware.test.ts
  • packages/nuxt/tests/unit/error-codes.test.ts
  • packages/nuxt/tests/unit/route-matcher.test.ts
  • packages/nuxt/tests/unit/session-manager.test.ts
  • packages/nuxt/tests/unit/signin-post.test.ts
  • packages/nuxt/tests/unit/signup-post.test.ts
  • packages/nuxt/tests/unit/token-refresh.test.ts
  • packages/nuxt/tests/unit/url-validation.test.ts
  • packages/nuxt/tsconfig.eslint.json
  • packages/nuxt/vitest.config.ts
  • packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts
  • packages/vue/src/components/presentation/sign-up/v1/SignUp.ts
  • packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts
  • packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts
  • packages/vue/src/plugins/AsgardeoPlugin.ts
📝 Walkthrough

Walkthrough

Adds a full-featured Nuxt SDK (@asgardeo/nuxt) including server routes, SSR Nitro plugin, server-side session/token management, client plugin and composable, UI components, middleware, utilities, types, tests, docs, a Nuxt sample app, Vue SignUp refactors, and small JavaScript utility fixes for SCIM2 username/profile handling.

Changes

Cohort / File(s) Summary
Nuxt package surface & metadata
packages/nuxt/package.json, packages/nuxt/src/index.ts
New package exports, version bump, added peerDeps, new named exports and subpath exports (./server, ./errors, ./utils).
Nuxt module & runtime wiring
packages/nuxt/src/module.ts, packages/nuxt/src/runtime/plugins/asgardeo.ts, packages/nuxt/src/runtime/composables/useAsgardeo.ts
Module refactor to asgardeo config key, runtimeConfig merging, server plugin + client plugin registration, new useAsgardeo() composable with Nuxt-aware signIn/signOut/signUp behaviors.
SSR Nitro plugin & server client
packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts, packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts
New Nitro request hook initializing singleton AsgardeoNuxtClient, session rehydration, parallel SSR fetches; server client wraps legacy client and exposes sign-in/exchange, SCIM/org/branding APIs.
Server session & token utilities
packages/nuxt/src/runtime/server/utils/session.ts, .../server/utils/serverSession.ts, .../server/utils/token-refresh.ts
JWT temp/session token creation/verification, cookie helpers, issueSessionCookie, and access-token refresh logic with reissued session cookie.
Server routes (auth/session/user/org/branding)
packages/nuxt/src/runtime/server/routes/auth/session/*, .../auth/user/*, .../auth/organizations/*, .../auth/branding/branding.get.ts
New REST endpoints for signin/signout/callback/session/token, embedded sign-in/sign-up flows, user profile get/patch, org list/switch/create/current, and branding retrieval; all guarded by session verification.
SSR event context & server index
packages/nuxt/src/runtime/server/utils/event-context.ts, packages/nuxt/src/runtime/server/index.ts
Typed event.context helper and server barrel exports for server-only APIs.
Components: root, controls, auth, user, organization, actions
packages/nuxt/src/runtime/components/AsgardeoRoot.ts, .../control/*, .../auth/*, .../actions/*, .../user/*, .../organization/*
New AsgardeoRoot wiring SSR-hydrated useState values and many Vue components (SignIn/SignUp/Callback, SignInButton/SignOutButton/SignUpButton, control components, user/org UI) with optimistic update handlers.
Middleware & factory
packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts, .../middleware/auth.ts
Middleware factory and default auth middleware that redirect unauthenticated users and supports requireScopes/requireOrganization options.
Errors & logging
packages/nuxt/src/runtime/errors/*, packages/nuxt/src/runtime/utils/log.ts
New AsgardeoError class, ErrorCode enum, error barrel, and simple logger/maskToken utility.
Utilities: route matcher & url validation
packages/nuxt/src/runtime/utils/createRouteMatcher.ts, .../url-validation.ts, .../index.ts
Glob-style route matcher for middleware, open-redirect-safe returnUrl validation, and public utils barrel.
Types & TS augmentations
packages/nuxt/src/runtime/types.ts, packages/nuxt/src/runtime/types/augments.d.ts
Introduces AsgardeoNuxtConfig, AsgardeoSessionPayload/JWTPayload-based session types, SSR/auth state types, and Nuxt/H3 module augmentations.
Tests & test config
packages/nuxt/tests/**/*, packages/nuxt/vitest.config.ts, packages/nuxt/tests/__mocks__/nuxt-app.ts
Extensive unit tests for SSR plugin, routes, middleware, session/token utilities, components; Vitest config and Nuxt mock added.
Documentation & sample app
packages/nuxt/README.md, samples/nuxt/*
Large Nuxt README and a complete Nuxt sample application (routes, layouts, pages, Tailwind config, env example).
Vue SDK refactors & SignUp V1/V2
packages/vue/src/components/presentation/sign-up/*, .../BaseSignUp.ts, .../SignUp.ts
BaseSignUp split into platform-aware dispatcher and detailed V1/V2 implementations (embedded flow, passkey handling).
Vue UserProfile SCIM2 changes
packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts
Update to build SCIM2-compliant PATCH payloads (nested schema wrapping, multi-valued typed arrays, WSO2 mobile dual-write).
Vue plugin options
packages/vue/src/plugins/AsgardeoPlugin.ts, packages/vue/src/index.ts
Adds AsgardeoPluginOptions with delegated install mode and exports type.
JavaScript SDK small fixes
packages/javascript/src/api/updateMeProfile.ts, packages/javascript/src/utils/generateFlattenedUserProfile.ts
Post-process SCIM2 PATCH response to normalize username prefix; enhance flattened profile generation to read typed multi-valued dotted fields.
Workspace & samples cleanup
pnpm-workspace.yaml, samples/vue-sdk-playground/*
Enables nuxt package in workspace and removes old Vue playground samples (many files deleted).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser/Client
    participant NuxtServer as Nuxt Server (Nitro)
    participant AuthProvider as Identity Provider
    participant AsgardeoAPI as Asgardeo API

    Client->>NuxtServer: GET /api/auth/signin (optional returnTo)
    NuxtServer->>NuxtServer: createTempSessionToken + set temp-cookie
    NuxtServer->>Client: 302 redirect to AuthProvider /authorize
    Client->>AuthProvider: authorize (user authenticates)
    AuthProvider->>Client: Redirect back to /api/auth/callback?code&state
    Client->>NuxtServer: GET /api/auth/callback
    NuxtServer->>NuxtServer: verifyTempSessionToken(state)
    NuxtServer->>AuthProvider: POST /token (exchange code)
    AuthProvider->>NuxtServer: { access_token, id_token, refresh_token }
    NuxtServer->>AsgardeoAPI: GET /Me (SCIM2) (optional)
    AsgardeoAPI->>NuxtServer: user profile
    NuxtServer->>NuxtServer: issueSessionCookie (JWT)
    NuxtServer->>Client: set session cookie + redirect to returnTo
Loading
sequenceDiagram
    participant Browser as Client
    participant Nitro as Nitro SSR Plugin
    participant ClientSDK as AsgardeoNuxtClient
    participant API as Asgardeo API
    participant Store as Nuxt useState

    Browser->>Nitro: Any page request
    Nitro->>ClientSDK: initialize client (once)
    Nitro->>Nitro: verifyAndRehydrateSession(cookie)
    alt session valid
        Nitro->>ClientSDK: rehydrateSessionFromPayload
        par parallel
            Nitro->>API: getUserProfile(sessionId)
            Nitro->>API: getMyOrganizations(sessionId)
            Nitro->>API: getBrandingPreference()
        and
        end
        Nitro->>Store: populate event.context.asgardeo.ssr
    else no session
        Nitro->>Store: event.context.asgardeo = { isSignedIn: false }
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • Nuxt SDK #350 — Implements a full Nuxt SDK (server routes, composables, SSR, components) that aligns with the objective of adding official Nuxt support.
  • #475 — Redirect-based support for the Nuxt SDK — PR implements redirect-based OAuth flows, callback handling, session management, and documentation requested by this issue.

Suggested reviewers

  • thiva-k
  • DonOmalVindula

Poem

🐰
I hopped through code, a tiny guide,
SSR burrows open wide.
Redirects, tokens, roots in place —
A carrot-flavored auth embrace.
Hop—nuxty joy across the tide!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (25)
packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts-45-50 (1)

45-50: ⚠️ Potential issue | 🟠 Major

Avoid leaking internal error messages to the client.

Inlining err.message (or String(err)) into statusMessage can surface internal details (stack hints, upstream API messages, sometimes tokens or IDs) to anonymous callers. Recommend logging the raw error server-side and returning a generic message — same pattern is likely worth applying across the other organization/session route handlers in this PR.

🛡️ Proposed change
-  } catch (err) {
-    throw createError({
-      statusCode: 500,
-      statusMessage: `Failed to retrieve organisations: ${err instanceof Error ? err.message : String(err)}`,
-    });
-  }
+  } catch (err) {
+    console.error('[asgardeo] Failed to retrieve organisations for session', err);
+    throw createError({
+      statusCode: 500,
+      statusMessage: 'Failed to retrieve organisations.',
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts` around
lines 45 - 50, The catch block in the organizations me route currently includes
the raw error text in the createError statusMessage (throw createError({...})),
which can leak internal details to clients; instead, log the full error
server-side (e.g., console.error or the existing server logger) inside the catch
and throw a createError with a generic statusMessage such as "Failed to retrieve
organisations" (no err.message). Update the catch in
packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts to log err
and return a sanitized message, and apply the same pattern to other
organization/session route handlers that currently inline err.message.
packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts-40-46 (1)

40-46: ⚠️ Potential issue | 🟠 Major

Don't silently swallow getUser failures.

The bare catch {} returns a signed-out hydration state for any failure — transient network/IdP outages, decryption errors, programming bugs — with zero observability. Two concerns:

  1. Operator blindness: there's no log to diagnose why authenticated users suddenly appear signed-out in production.
  2. Possible hydration thrash: if the client treats isSignedIn: false as authoritative and the session cookie is still valid, you may flap auth UI on every transient upstream blip.

At minimum, log; consider distinguishing transient upstream errors (5xx) from "no valid session" so the client can decide whether to retry or clear local state.

🛡️ Proposed change
-  try {
-    const client = AsgardeoNuxtClient.getInstance();
-    const user = await client.getUser(session.sessionId);
-    return {isSignedIn: true, user, isLoading: false};
-  } catch {
-    return {isSignedIn: false, user: null, isLoading: false};
-  }
+  try {
+    const client = AsgardeoNuxtClient.getInstance();
+    const user = await client.getUser(session.sessionId);
+    return {isSignedIn: true, user, isLoading: false};
+  } catch (err) {
+    console.error('[asgardeo] /api/auth/session: failed to load user for valid session', err);
+    return {isSignedIn: false, user: null, isLoading: false};
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts` around
lines 40 - 46, The current catch in session.get.ts around
AsgardeoNuxtClient.getInstance() / client.getUser(session.sessionId) swallows
all failures; update the error handling to log the caught error (including its
message/stack and any HTTP status if present) via your server logger and
distinguish transient upstream errors from "no valid session" by inspecting
error properties (e.g., error.status or error.code) so you can return a
different shape or flag (e.g., isSignedIn: false plus isError: true or
transientError: true) for retryable 5xx/network errors versus a definitive
session-missing case; ensure you still return a safe client-facing object but
with observability and a clear transient vs permanent decision for the caller.
packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts-33-40 (1)

33-40: ⚠️ Potential issue | 🟠 Major

Config error in production is masked as 401.

sessionSecret passed to verifyAndRehydrateSession is optional. When undefined, getSecret() throws an explicit error in production (ASGARDEO_SESSION_SECRET environment variable is required), but this error is caught by the verifyAndRehydrateSession try-catch block, returning null and manifesting here as a 401. The config problem is thus hidden from the API consumer. In development, the function silently uses a default secret instead.

Consider validating sessionSecret before calling verifyAndRehydrateSession, or re-throwing configuration errors separately so they surface as 5xx instead of 401, making deployment misconfiguration obvious.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts` around
lines 33 - 40, The current code passes an optional sessionSecret (from
useRuntimeConfig) into verifyAndRehydrateSession which masks configuration
errors as a 401; validate that sessionSecret is present and valid before calling
verifyAndRehydrateSession (check the config.asgardeo?.sessionSecret value
returned by useRuntimeConfig) and if it's missing/invalid throw a server error
(5xx) using createError so configuration failures surface, or alternatively
catch configuration-specific errors from verifyAndRehydrateSession and re-throw
them as 5xx instead of returning null/letting the code produce a 401.
packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts-34-34 (1)

34-34: ⚠️ Potential issue | 🟠 Major

Pass event to useRuntimeConfig inside all organization route handlers.

Inside Nitro event handlers, useRuntimeConfig(event) is the recommended form to ensure per-request env overrides and async-context-bound config are correctly resolved. This pattern applies to all organization route handlers in this PR: current.get.ts, id.get.ts, index.get.ts, index.post.ts, me.get.ts, and switch.post.ts. The same fix should also be applied to user route handlers (profile.get.ts, user.get.ts, profile.patch.ts) and session handlers (signup.post.ts, signout.post.ts). Note that branding.get.ts already implements this correctly.

Example for id.get.ts
-  const config = useRuntimeConfig();
+  const config = useRuntimeConfig(event);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts` at line
34, Replace plain useRuntimeConfig() with useRuntimeConfig(event) in all Nitro
route handlers so runtime config is resolved per-request; specifically update
the organization handlers (current.get.ts, id.get.ts, index.get.ts,
index.post.ts, me.get.ts, switch.post.ts), the user handlers (profile.get.ts,
user.get.ts, profile.patch.ts) and the session handlers (signup.post.ts,
signout.post.ts) to call useRuntimeConfig(event) where you currently call
useRuntimeConfig(), ensuring the `event` object from the route handler is passed
through to the call.
packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts-46-51 (1)

46-51: ⚠️ Potential issue | 🟠 Major

Avoid leaking raw error details into statusMessage.

statusMessage is sent as the HTTP reason phrase, which is single-line ASCII. Interpolating err.message (sourced from upstream APIs / network layers) can:

  • Leak internal error details (stack hints, internal hostnames, tokens included in upstream errors) to unauthenticated/external callers.
  • Break the response if err.message contains CR/LF (header injection / truncation).

Prefer a stable reason phrase and put any detail in the body via data/message, while logging the original error server-side. The same pattern is repeated in packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts (lines 47–50) and packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts (lines 55–58) — consider centralizing this in a small helper used by all auth/* routes.

🛡️ Suggested fix
-  } catch (err) {
-    throw createError({
-      statusCode: 500,
-      statusMessage: `Failed to retrieve current organisation: ${err instanceof Error ? err.message : String(err)}`,
-    });
-  }
+  } catch (err) {
+    console.error('[asgardeo] getCurrentOrganization failed:', err);
+    throw createError({
+      statusCode: 500,
+      statusMessage: 'Internal Server Error',
+      message: 'Failed to retrieve current organisation.',
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts`
around lines 46 - 51, The catch block currently interpolates err.message into
createError.statusMessage (in the catch of current.get handler using
createError), which can leak/internalize untrusted text and break headers;
change it to use a stable, constant statusMessage like "Failed to retrieve
current organisation" and move the detailed error text into the response body
(e.g., pass a safe data or message field) and log the original error
server-side; apply the same pattern for the similar catch blocks in profile.get
and profile.patch or factor this into a small helper (e.g., a safeCreateError or
handleRouteError used by auth/* routes) that logs the original error and returns
createError with a fixed statusMessage and optional safe body message.
packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts-51-59 (1)

51-59: ⚠️ Potential issue | 🟠 Major

Don't leak raw error messages in HTTP responses.

statusMessage: \Failed to create organisation: ${err.message}`returns the underlying SDK/network error verbatim to the caller (and often to the browser as a status line). Internal exceptions can include stack hints, upstream URLs, or upstream error bodies. Log the detail server-side and return a generic message; usedata` if you need a structured client-facing payload.

🔧 Suggested change
-  } catch (err) {
-    throw createError({
-      statusCode: 500,
-      statusMessage: `Failed to create organisation: ${err instanceof Error ? err.message : String(err)}`,
-    });
-  }
+  } catch (err) {
+    console.error('[asgardeo] createOrganization failed', err);
+    throw createError({
+      statusCode: 500,
+      statusMessage: 'Failed to create organisation.',
+    });
+  }

The same pattern is repeated in organizations/index.get.ts (Line 48) and signin.get.ts (Line 73 is OK, generic). Please sweep the other auth routes too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts`
around lines 51 - 59, The handler currently throws createError with the raw
err.message (see AsgardeoNuxtClient.getInstance and createOrganization usage),
which leaks internal errors; instead, log the full error server-side (e.g.,
using your logger) and throw createError with a generic statusMessage like
"Failed to create organisation" (do not append err.message); if you need to
return structured client info, put a non-sensitive object in the error response
body/data rather than the statusMessage. Sweep the other auth route handlers
(e.g., the organizations index GET and signin handlers) and apply the same
pattern: log full error internally and return a generic client-facing message
via createError.
packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts-44-48 (1)

44-48: ⚠️ Potential issue | 🟠 Major

returnTo validation has bypass paths and an unsafe cast.

Two concerns:

  1. Backslash bypass. Several browsers (notably some older Chromium/Edge builds and most user-agents that normalize backslashes) treat /\evil.com as //evil.com, leading to an open redirect. The current filter only rejects //. Reject any input whose second character is / or \, and ideally any input containing \, control chars, or whitespace.
  2. Type coercion. getQuery(event) returns Record<string, string | string[] | undefined>; if a client sends ?returnTo=/a&returnTo=/b, this cast yields an array at runtime and returnTo.startsWith throws. Coerce defensively.
🔧 Suggested hardening
-  const query = getQuery(event);
-  const returnTo = query['returnTo'] as string | undefined;
-
-  // Validate returnTo is a relative path to prevent open redirect
-  const safeReturnTo = returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : undefined;
+  const query = getQuery(event);
+  const rawReturnTo = query['returnTo'];
+  const returnTo = typeof rawReturnTo === 'string' ? rawReturnTo : undefined;
+
+  // Must be a relative path. Reject protocol-relative (`//`, `/\`),
+  // control chars and whitespace to prevent open-redirect bypass.
+  const safeReturnTo =
+    returnTo &&
+    returnTo.startsWith('/') &&
+    returnTo[1] !== '/' &&
+    returnTo[1] !== '\\' &&
+    !/[\s\\]/.test(returnTo)
+      ? returnTo
+      : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts` around
lines 44 - 48, The current returnTo handling in the getQuery(event) flow uses an
unsafe cast and allows backslash bypasses; update the logic around the returnTo
variable and safeReturnTo creation: first coerce/guard that returnTo from
getQuery(event) is a single string (e.g., if it's an array, pick the first
element or ignore) by checking typeof returnTo === 'string' before using string
methods, then validate the string rejects any input whose first char is not '/',
whose second character is '/' or '\' (reject if returnTo.length > 1 && ['/',
'\\'].includes(returnTo[1])), and reject any returnTo containing backslashes,
control characters, or whitespace (use a conservative regex/scan to ensure only
allowed path chars). Only set safeReturnTo when the value passes the defensive
type check and the tightened validation.
packages/nuxt/src/runtime/utils/url-validation.ts-73-81 (1)

73-81: ⚠️ Potential issue | 🟠 Major

decodeURIComponent can throw URIError on malformed inputs.

If an attacker (or simply a buggy client) submits a returnTo like /%E0abc or /%XYZ, decodeURIComponent throws URIError. That escapes validateReturnUrl as an uncaught exception (not an AsgardeoError), so:

  • Callers using validateReturnUrl directly will see a generic 500 instead of a structured OpenRedirectBlocked 400.
  • safeReturnUrl does still fall back (because its catch is untyped), but the intent — “invalid encoding ⇒ rejected as redirect attack” — is implicit and easy to break later.

Wrap the decode in try/catch and treat malformed encodings as a redirect-blocked failure.

🛡️ Suggested change
-  // Reject encoded protocol-relative or absolute indicators in the first segment
-  const decoded = decodeURIComponent(trimmed.slice(1, 5).toLowerCase());
-  if (decoded.startsWith('/') || decoded.startsWith('\\')) {
-    throw new AsgardeoError(
-      `Open redirect blocked: returnTo "${trimmed}" contains an encoded redirect sequence.`,
-      ErrorCode.OpenRedirectBlocked,
-      {statusCode: 400},
-    );
-  }
+  // Reject encoded protocol-relative or absolute indicators in the first segment
+  let decoded: string;
+  try {
+    decoded = decodeURIComponent(trimmed.slice(1, 5).toLowerCase());
+  } catch {
+    throw new AsgardeoError(
+      `Open redirect blocked: returnTo "${trimmed}" contains a malformed percent-encoding.`,
+      ErrorCode.OpenRedirectBlocked,
+      {statusCode: 400},
+    );
+  }
+  if (decoded.startsWith('/') || decoded.startsWith('\\')) {
+    throw new AsgardeoError(
+      `Open redirect blocked: returnTo "${trimmed}" contains an encoded redirect sequence.`,
+      ErrorCode.OpenRedirectBlocked,
+      {statusCode: 400},
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/utils/url-validation.ts` around lines 73 - 81, The
decodeURIComponent call that builds `decoded` can throw URIError for malformed
percent-encodings; wrap the decode step in a try/catch inside
`validateReturnUrl` (the block that computes `decoded` from
`trimmed.slice(1,5)`), and on any error throw the same AsgardeoError
(ErrorCode.OpenRedirectBlocked, statusCode 400) so malformed encodings are
treated as blocked redirects; keep the existing startsWith checks and behavior
otherwise.
packages/nuxt/src/runtime/components/auth/SignIn.ts-79-97 (1)

79-97: ⚠️ Potential issue | 🟠 Major

Possible token leakage via URL query params after sign-in.

handleSuccess appends every entry of authData as a query parameter on afterSignInUrl. If authData carries token-shaped material (e.g. idToken, accessToken, code, state), those values end up in window.history, server access logs, and any Referer header sent to third-party assets on the destination page. Even if Asgardeo currently passes only opaque flow metadata, the unfiltered iteration is fragile against future additions.

Consider explicitly allow-listing the keys you want to forward (or removing the URL-param propagation altogether and relying on the session cookie / a server-side state store).

🛡️ Suggested allow-list
-          Object.entries(authData).forEach(([key, value]: [string, any]) => {
-            if (value !== undefined && value !== null) {
-              url.searchParams.append(key, String(value));
-            }
-          });
+          const FORWARD_KEYS = new Set(['flowStatus', 'sessionId']); // adjust as needed
+          Object.entries(authData).forEach(([key, value]) => {
+            if (FORWARD_KEYS.has(key) && value !== undefined && value !== null) {
+              url.searchParams.append(key, String(value));
+            }
+          });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/SignIn.ts` around lines 79 - 97, In
handleSuccess, authData is blindly appended to afterSignInUrl which can leak
tokens; change the code to stop iterating all entries and instead
filter/authenticate keys before adding them: either implement an explicit
allow-list of safe keys (e.g. only forward known non-sensitive fields such as
"username", "userId", "provider", "next" or your app-specific safe attributes)
or explicitly exclude token-like keys ("idToken", "accessToken", "refreshToken",
"code", "state", etc.) when building query params; update the logic around
handleSuccess and the client-only URL construction so only the approved keys
from authData are appended (or remove query propagation entirely and rely on
session/cookie or server-side state) while keeping the SSR branch unchanged.
packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts-73-84 (1)

73-84: 🛠️ Refactor suggestion | 🟠 Major

Remove the redundant re-fetching of config / sessionSecret.

config and sessionSecret are already in scope from lines 40–41. Re-declaring them inside the second try block via const shadows the outer variables and re-invokes useRuntimeConfig() for no reason — easy to drift if someone tweaks one but not the other.

♻️ Suggested change
   // Re-issue the session cookie with the new token so subsequent SSR requests
   // pick up the switched organisation context — mirrors callback.get.ts.
   try {
-    const config = useRuntimeConfig();
-    const sessionSecret = config.asgardeo?.sessionSecret;
     await issueSessionCookie(event, sessionId, tokenResponse, sessionSecret);
   } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts`
around lines 73 - 84, The try block re-declares and shadows config and
sessionSecret by calling useRuntimeConfig() again; remove the inner const config
= useRuntimeConfig() and const sessionSecret = config.asgardeo?.sessionSecret
and instead use the already-in-scope config/sessionSecret from lines 40–41 when
calling issueSessionCookie(event, sessionId, tokenResponse, sessionSecret) to
avoid redundant runtime config calls and variable shadowing.
packages/nuxt/src/runtime/plugins/asgardeo.ts-186-221 (1)

186-221: ⚠️ Potential issue | 🟠 Major

noop ASGARDEO_KEY actions silently break base components that call them directly.

clearSession, exchangeToken, getDecodedIdToken, getIdToken, signInSilently, switchOrganization, and http.request/requestAll all return undefined. As the comment at Lines 150-153 acknowledges, base components from @asgardeo/vue (e.g. OrganizationSwitcher, BaseSignUpButton) consume the context-provided actions directly — they will get a no-op even when the corresponding Nitro route or Nuxt composable could service the request. This will manifest as user actions that silently do nothing.

Either wire these to the appropriate /api/auth/* Nitro routes (similar to signIn/signOut) or have the no-op variants throw/warn so the gap is observable, with a follow-up to provide real implementations for the redirect-based flow promised by issue #475.

Want me to draft Nitro-backed implementations for switchOrganization and getDecodedIdToken that mirror the existing signOut shape, or open a tracking issue for the remaining context actions?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/plugins/asgardeo.ts` around lines 186 - 221, The
provided ASGARDEO_KEY currently injects noop functions that return undefined
(noop) for actions like clearSession, exchangeToken, getDecodedIdToken,
getIdToken, signInSilently, switchOrganization and http.request/requestAll,
which lets base components silently fail; update the ASGARDEO_KEY provider so
these symbols either call the corresponding Nitro endpoints (mirror the existing
signIn/signOut pattern: use nuxtApp.$fetch or useRuntimeConfig-backed
/api/auth/* routes and return the fetch response) or replace noop with functions
that emit a clear error/warning and throw (so failures are observable); update
the provider block where noop is assigned (ASGARDEO_KEY, noop, signIn, signOut)
to wire each of: clearSession, exchangeToken, getDecodedIdToken, getIdToken,
signInSilently, switchOrganization, http.request and http.requestAll to real
implementations that follow signOut/signIn shape or to fail-fast warnings.
packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts-140-181 (1)

140-181: ⚠️ Potential issue | 🟠 Major

Multi-valued complex attribute handling ignores extension schemas.

Two related concerns in buildScimPatchValue:

  1. Hardcoded list limitation: The complexMultiValued set (Lines 144–153) only covers core SCIM2 attribute names. Custom/extension schemas that declare their own multi-valued complex attributes won't be matched, so flattened values like customMultiAttr.work fall through to the nested-object path and produce an invalid {customMultiAttr: {work: ...}} PATCH that the server silently ignores.

  2. Missing schemaId wrapping: The early return at Line 159 places [head]: [{type, value}] at the root unconditionally, even when the attribute's schemaId is an extension URN. The URN-wrapping logic (Lines 175–178) is only reachable via the nested-object path, so a multi-valued complex attribute under an extension schema never gets wrapped under its schema URN.

A more robust approach would avoid the hardcoded list and instead:

  • Detect multi-valued complex attributes from the schema entry metadata (e.g., by looking up the parent attribute definition rather than relying on a fixed set), or
  • Apply the extension-URN wrapping uniformly to both code paths so extension schemas are always handled consistently.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts`
around lines 140 - 181, The current multi-valued complex handling in
buildScimPatchValue uses a hardcoded complexMultiValued set and returns {[head]:
[{type: tail, value: rawValue}]} at root, which misses extension schema
attributes and prevents URN wrapping; fix by (1) replacing or augmenting the
hardcoded complexMultiValued check with a schema-driven lookup using entry
(e.g., inspect entry.schema or entry.attributes for head and confirm
attribute.multiValued && attribute.type === 'complex') instead of the static
Set, and (2) after constructing the complex value for head/tail ensure you apply
the same schemaId wrapping as the nested path (i.e., if schemaId && schemaId !==
CORE_USER_SCHEMA_ID return {[schemaId]: {[head]: [{type: tail, value:
rawValue}]}}) so extension URNs are honored; keep other logic (flatKey, head,
tail, rawValue, entry, CORE_USER_SCHEMA_ID) unchanged.
packages/nuxt/src/runtime/server/utils/token-refresh.ts-99-119 (1)

99-119: ⚠️ Potential issue | 🟠 Major

Refresh-token POST has no timeout — can hang Nitro workers indefinitely

fetch(tokenEndpoint, ...) runs on the request path with no AbortController/timeout. If the IdP becomes slow or unresponsive, every authenticated request that needs a refresh will hang as long as the OS / runtime allows, exhausting Nitro worker concurrency. This is a request-thread blocking call against an external system — exactly the case that needs an explicit timeout.

Suggested fix
+const REFRESH_TIMEOUT_MS = 10_000;
@@
-  let refreshed: OIDCTokenRefreshResponse;
+  let refreshed: OIDCTokenRefreshResponse;
+  const ac = new AbortController();
+  const timer = setTimeout(() => ac.abort(), REFRESH_TIMEOUT_MS);
   try {
     const res = await fetch(tokenEndpoint, {
       method: 'POST',
       body,
       headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+      signal: ac.signal,
     });

     if (!res.ok) {
       const errText = await res.text().catch(() => String(res.status));
       throw new Error(`Token endpoint returned ${res.status}: ${errText}`);
     }

     refreshed = (await res.json()) as OIDCTokenRefreshResponse;
   } catch (err: unknown) {
     const msg = err instanceof Error ? err.message : String(err);
     console.error('[asgardeo] Token refresh failed:', msg);
     throw createError({
       statusCode: 401,
       statusMessage: 'Token refresh failed. Please sign in again.',
     });
+  } finally {
+    clearTimeout(timer);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` around lines 99 -
119, The fetch call to tokenEndpoint in token-refresh.ts has no timeout and can
hang Nitro workers; wrap the POST fetch in an AbortController with a short
configurable timeout (e.g., REFRESH_TIMEOUT_MS), pass controller.signal into
fetch, start a setTimeout that calls controller.abort() after the timeout, and
clear that timer after fetch completes; handle the abort case in the catch (map
an AbortError to a suitable createError response such as 504/408) while keeping
the existing error logging and throwing createError for other errors, updating
the fetch invocation that sets `refreshed` and the surrounding try/catch
accordingly.
packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts-100-107 (1)

100-107: ⚠️ Potential issue | 🟠 Major

Singleton bakes afterSignInUrl from the first request

The AsgardeoNuxtClient is a process-level singleton (getInstance() with isInitialized guard). In the SSR plugin, client.initialize() is called only once when !client.isInitialized (line 68), and passes afterSignInUrl: resolveCallbackUrl(event) derived from the incoming request's origin. After the first initialization, isInitialized is set to true, and all subsequent requests skip the initialization block entirely—leaving the callback URL permanently fixed to whichever origin triggered first initialization.

For multi-origin setups (apex + www subdomains, multi-tenant deployments, or reverse proxies routing different hostnames to the same Nitro process), users accessing from non-first origins will receive OAuth redirect URIs mismatched against their entry point, causing redirect_uri_mismatch errors at the IdP.

Pass the callback URL per-flow (resolved from the live event at sign-in initiation) rather than at client initialization, or configure a single canonical afterSignInUrl in runtime config instead of deriving it from the request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts` around lines 100 -
107, The singleton AsgardeoNuxtClient is being initialized with a
request-specific afterSignInUrl (resolveCallbackUrl(event)) which freezes the
callback for all future requests; change the flow so client.initialize(...)
(called via getInstance()/isInitialized) does NOT bake in
resolveCallbackUrl(event) — instead remove afterSignInUrl from the
initialization arguments and pass the per-request callback URL at the time of
sign-in/redirect (e.g., in the method that triggers authentication or in a new
parameter like startSignIn(event)/signIn(resolveCallbackUrl(event))) or read a
single canonical afterSignOut/afterSignIn from runtime config if you intend a
global URL; update references to afterSignInUrl to use the per-request value
(resolveCallbackUrl(event)) when creating the redirect/authorization request
rather than in initialize().
packages/nuxt/src/runtime/server/utils/token-refresh.ts-65-138 (1)

65-138: ⚠️ Potential issue | 🟠 Major

Add per-session locking to prevent concurrent refresh-token race

Two requests for the same session arriving within the skew window (common with parallel $fetch calls) will both attempt to refresh using the same refresh_token. Most OIDC providers including Asgardeo rotate refresh tokens on each use, so the second request fails with invalid_grant (401), causing spurious sign-out errors. Additionally, the cookie write order is non-deterministic and the older token may stomp the newer one.

The standard fix is a per-session in-process Map<sessionId, Promise<string>> that deduplicates concurrent refresh attempts. This covers most deployments; cross-instance scenarios would require a shared lock (Redis, etc.).

Example pattern
const inflight = new Map<string, Promise<string>>();

export async function getValidAccessToken(event: H3Event): Promise<string> {
  const session = await requireServerSession(event);
  // ...freshness check unchanged...

  const existing = inflight.get(session.sessionId);
  if (existing) return existing;

  const p = doRefresh(event, session).finally(() => inflight.delete(session.sessionId));
  inflight.set(session.sessionId, p);
  return p;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` around lines 65 -
138, getValidAccessToken can race when multiple requests for the same session
refresh in parallel (refresh_token rotation and non-deterministic cookie
writes); fix by adding an in-memory per-session inflight Map<string,
Promise<string>> keyed by session.sessionId to deduplicate refreshes: on
detecting expired token, check inflight.get(session.sessionId) and return it if
present, otherwise create a promise that performs the current refresh logic (the
existing token POST, createSessionToken, setCookie, returning the new access
token), store it in inflight, and ensure inflight.delete(session.sessionId) runs
in finally so the entry is removed after completion or error; keep all existing
behavior and use the same symbols getValidAccessToken, session.sessionId,
createSessionToken, and setCookie.
packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts-125-127 (1)

125-127: ⚠️ Potential issue | 🟠 Major

Use validateReturnUrl() to block encoded open-redirect bypasses

returnTo is extracted from the signed temp session cookie (L78) and used for redirect (L126) without validation. While signin.get.ts applies a basic check (startsWith('/') && !startsWith('//') at L48), this is bypassable: /%2F%2Fevil.com passes the basic filter but decodes to //evil.com (protocol-relative URL = open redirect). The codebase already has validateReturnUrl() in url-validation.ts that explicitly blocks encoded sequences (lines 74–80), but it is not imported or used here.

Add validateReturnUrl() to both routes:

  • signin.get.ts L48: Replace basic check with validateReturnUrl(returnTo) or safeReturnUrl(returnTo).
  • callback.get.ts L126: Apply validateReturnUrl(returnTo) before redirect (defense-in-depth, even though JWT is signed).
  • If publicConfig.afterSignInUrl is user-configurable, validate it too.

Also emit raw provider error messages to logs only; sanitize for client response

Line 102 exposes err?.message in the error response data field, leaking internal provider details. Send sanitized statusMessage to client and log the raw error server-side.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts` around
lines 125 - 127, Replace the ad-hoc returnTo check with the existing URL
validation helper by importing and calling validateReturnUrl (or safeReturnUrl)
where returnTo is used: update signin.get.ts to call validateReturnUrl(returnTo)
instead of the startsWith checks, and in callback.get.ts validate returnTo
before building redirectUrl (also validate publicConfig.afterSignInUrl if it can
be user-configured) so only safe, non-encoded open-redirects are allowed;
additionally, stop returning err?.message directly to the client—log the raw
provider error server-side (using the existing logger) and send a sanitized
statusMessage in the client response (preserve status codes) so internal
provider details are not leaked.
packages/nuxt/src/runtime/server/utils/session.ts-116-123 (1)

116-123: ⚠️ Potential issue | 🟠 Major

verifySessionToken doesn't enforce type === 'session' — type confusion possible.

createSessionToken sets type: 'session' (Line 78) and createTempSessionToken sets type: 'temp' (Line 99), and both are signed with the same secret. verifyTempSessionToken correctly rejects mismatched types (Line 135-137), but verifySessionToken does not — meaning a valid temp-session JWT (15-minute TTL, only sessionId payload) would be accepted by anything calling useServerSession / requireServerSession / SSR-plugin verification. The downstream code then reads fields like accessToken, sub, scopes that are simply absent.

It's defence-in-depth, but cheap to add and matches the temp-side behaviour.

🛡️ Proposed fix
 export async function verifySessionToken(
   token: string,
   sessionSecret?: string,
 ): Promise<AsgardeoSessionPayload> {
   const secret = getSecret(sessionSecret);
   const {payload} = await jwtVerify(token, secret);
+  if (payload['type'] !== 'session') {
+    throw new Error('Invalid token type: expected session');
+  }
   return payload as AsgardeoSessionPayload;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/session.ts` around lines 116 - 123,
verifySessionToken currently verifies the JWT signature but doesn't assert
payload.type === 'session', so temp tokens could be accepted; update
verifySessionToken (which calls getSecret and jwtVerify and returns
AsgardeoSessionPayload) to check that payload.type === 'session' and throw an
error (or reject) if not, mirroring the behavior in verifyTempSessionToken and
preventing a temp token from being treated as a full session token created by
createSessionToken / createTempSessionToken.
packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts-290-310 (1)

290-310: ⚠️ Potential issue | 🟠 Major

OAuth error in the popup is swallowed — user never sees it.

When the popup lands on a URL containing error=, the code logs it, closes the popup, and runs cleanup(), but it never calls handleError, props.onError, nor emits 'error'. From the parent component's perspective, handleSubmit already returned (because handleRedirectionIfNeeded returned true and the finally ran isLoading = false), so the user is left looking at a dismissed popup with no error message and a non-loading form.

The same applies to the catch (err) on Line 307 — the thrown error is silently logged.

🛡️ Proposed fix
             if (error) {
               processed = true;
               logger.error(`OAuth error during social sign-up: ${error}`);
+              const e = new Error(`OAuth error during social sign-up: ${error}`);
+              handleError(e);
+              props.onError?.(e);
+              emit('error', e);
               popup.close();
               cleanup();
               return;
             }
-        } catch (err) {
-          logger.error('Error monitoring sign-up popup');
-        }
+        } catch (err) {
+          logger.error('Error monitoring sign-up popup');
+          processed = true;
+          handleError(err);
+          props.onError?.(err as Error);
+          emit('error', err);
+          popup.close();
+          cleanup();
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts` around
lines 290 - 310, The popup OAuth error and the catch block currently only log
and close the popup without surfacing the error to the component; update the
popup handling in BaseSignUp.ts so that when an OAuth error is detected (the
branch that logs `OAuth error during social sign-up: ${error}`) you call the
existing error-handling flow (invoke handleError with the error, call
props.onError if present, and emit an 'error' event) before closing the popup
and running cleanup; likewise, in the catch (err) block call handleError(err),
props.onError(err) if present, and emit('error', err) in addition to logging so
the parent sees the failure; keep using the same variables/functions shown
(processed, popup, cleanup, handleError, props.onError, emit) and ensure these
calls occur before popup.close()/cleanup to guarantee the UI receives the error.
packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts-258-269 (1)

258-269: ⚠️ Potential issue | 🟠 Major

Guard new URL(props.afterSignUpUrl) against non-absolute URLs.

The origin check correctly accepts both afterSignUpUrl origin and window.location.origin, matching the React SDK pattern. However, new URL() will throw a TypeError if props.afterSignUpUrl is a relative path (e.g., "/welcome") since the constructor requires an absolute URL. The messageHandler lacks error handling for this. Wrap the URL constructor in a try-catch or validate that afterSignUpUrl is absolute before constructing the URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts` around
lines 258 - 269, The messageHandler currently assumes props.afterSignUpUrl is an
absolute URL and calls new URL(props.afterSignUpUrl) which throws for relative
paths; update messageHandler to guard the URL construction by validating
props.afterSignUpUrl is absolute (e.g., starts with http:// or https://) or wrap
new URL(...) in a try-catch, falling back to window.location.origin if parsing
fails, then use that value for expectedOrigin; ensure you still compare
event.origin against both the parsed expectedOrigin and window.location.origin
and keep the existing logic that checks popup, extracts {code, state}, sets
processed and calls continueWithCode.
packages/nuxt/src/runtime/components/auth/SignUp.ts-98-114 (1)

98-114: ⚠️ Potential issue | 🟠 Major

Use external: false for afterSignUpUrl, and conditionally set external: true only for absolute URLs.

The afterSignUpUrl (and oauthRedirectUrl if relative) should use external: false for client-side navigation. The same afterSignUpUrl is correctly handled with external: false in the Nuxt composable (useAsgardeo.ts), but this component incorrectly uses external: true, causing unnecessary full-page reloads. The server returns this URL as a relative path (e.g., / or from config), and Nuxt's navigateTo should handle it as an internal route by default.

For the OAuth redirect case, gate the external option on whether the URL starts with http(s)://.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/SignUp.ts` around lines 98 - 114,
The handleComplete handler currently calls navigateTo with external: true for
both oauthRedirectUrl and props.afterSignUpUrl which forces full page reloads;
change it to detect absolute URLs (e.g., startsWith('http://') or
startsWith('https://')) and only pass { external: true } for absolute URLs,
otherwise call navigateTo without external or with { external: false } so
relative/internal paths (oauthRedirectUrl and props.afterSignUpUrl) use
client-side navigation; update the oauthRedirectUrl branch and the
afterSignUpUrl branch in handleComplete to perform this conditional external
flag logic.
packages/vue/src/components/presentation/sign-up/v2/BaseSignUp.ts-290-393 (1)

290-393: ⚠️ Potential issue | 🟠 Major

Popup OAuth: messageHandler/popupMonitor leak on component unmount; relative afterSignUpUrl will throw in new URL(...).origin.

Two issues with handleRedirectionIfNeeded:

  1. cleanup() only runs on the OAuth-callback success/error paths and when the polling loop detects popup.closed. If the user navigates away (route change in Nuxt), the component unmounts but the message listener on window and the 1-second setInterval remain attached, holding a closure over the (now orphan) flow state. Register an onBeforeUnmount cleanup that invokes the same cleanup() for any in-flight popups.

  2. Line 346-348:

    const expectedOrigin: string = props.afterSignUpUrl
      ? new URL(props.afterSignUpUrl).origin
      : window.location.origin;

    props.afterSignUpUrl defaults to / in many setups (and the V2 SignUp wrapper allows arbitrary user values). new URL('/') throws TypeError: Invalid URL. Pass a base URL to handle relative paths:

Proposed fix for (2)
-        const expectedOrigin: string = props.afterSignUpUrl
-          ? new URL(props.afterSignUpUrl).origin
-          : window.location.origin;
+        const expectedOrigin: string = props.afterSignUpUrl
+          ? new URL(props.afterSignUpUrl, window.location.origin).origin
+          : window.location.origin;

For (1), keep a module-scoped currentCleanup: (() => void) | null (or store it on a ref), reassign it from inside handleRedirectionIfNeeded, and call it from onBeforeUnmount and at the start of any new redirection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v2/BaseSignUp.ts` around
lines 290 - 393, handleRedirectionIfNeeded leaks the window.message listener and
setInterval on unmount and will throw for relative afterSignUpUrl; fix by
storing the cleanup function in a module-scoped/ref variable (e.g.
currentCleanup) and reassigning it inside handleRedirectionIfNeeded so you can
call currentCleanup() before starting a new redirection and from onBeforeUnmount
to guarantee removal of messageHandler and popupMonitor, and change the
expectedOrigin calculation to use a base when constructing the URL (e.g. new
URL(props.afterSignUpUrl, window.location.origin).origin) to safely handle
relative paths for props.afterSignUpUrl.
packages/vue/src/components/presentation/sign-up/v2/SignUp.ts-90-99 (1)

90-99: ⚠️ Potential issue | 🟠 Major

Substring filter !includes('oauth') && !includes('auth') is broken and will silently swallow valid redirects.

Two problems:

  1. 'oauth' already contains the substring 'auth', so the second predicate fully subsumes the first — the 'oauth' check is dead.
  2. The 'auth' substring matches far more than intended: /authentication/..., /author/..., /api/auth/..., even https://example.com/auth-success — all valid post-signup destinations get silently skipped, leaving the user stranded after a Redirection-typed completion.

If the intent is to suppress redirects to the IdP's own OAuth authorize endpoint (because the SDK handles that internally), match on path/host instead of any-position substring, e.g. compare against the configured baseUrl or against the well-known authorize path.

Also note: this third if is reachable concurrently with the second one only if response?.type === EmbeddedFlowResponseType.Redirection (mutually exclusive with the second block's negation), but the missing return after the second block makes that invariant non-obvious — consider adding return for clarity.

Suggested direction
       if (
         props.shouldRedirectAfterSignUp &&
         response?.type !== EmbeddedFlowResponseType.Redirection &&
         props.afterSignUpUrl
       ) {
         window.location.href = props.afterSignUpUrl;
+        return;
       }

       if (
         props.shouldRedirectAfterSignUp &&
         response?.type === EmbeddedFlowResponseType.Redirection &&
-        response?.data?.redirectURL &&
-        !response.data.redirectURL.includes('oauth') &&
-        !response.data.redirectURL.includes('auth')
+        response?.data?.redirectURL &&
+        !isInternalAuthorizeUrl(response.data.redirectURL /*, configuredBaseUrl */)
       ) {
         window.location.href = response.data.redirectURL;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v2/SignUp.ts` around lines
90 - 99, The redirect filtering in SignUp.ts incorrectly uses broad substring
checks on response.data.redirectURL (the condition under
props.shouldRedirectAfterSignUp and EmbeddedFlowResponseType.Redirection), which
can erroneously block valid redirects; update the logic to explicitly detect the
IdP authorize endpoint by comparing host/path against your configured base URL
or well‑known authorize path (rather than using
.includes('auth')/.includes('oauth')), e.g. check URL.hostname and URL.pathname
against the SDK's configured baseUrl/authorizePath or a whitelist, and ensure
you keep the existing redirect when it is legitimate; also add an explicit
return after the preceding handling block so the control flow between the second
block and the EmbeddedFlowResponseType.Redirection branch is obvious.
packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts-39-47 (1)

39-47: ⚠️ Potential issue | 🟠 Major

getInputName precedence contradicts its own documentation—fix the fallback order.

The comment on lines 41–42 states that config.identifier is the bound parameter name (e.g. http://wso2.org/claims/emailaddress) with config.name as a fallback. The implementation on lines 44–47 does the opposite: it checks cfg.name first and only falls back to cfg.identifier.

This same bug also exists in the Vue BaseSignUp.ts component's collectInputNames() function, which uses these field names to key the form submission payload. If the server's V1 contract expects inputs keyed by config.identifier (the SCIM claim URI) but the client sends them under config.name (a potentially localized display label), registration will fail server-side with cryptic missing-field errors.

Proposed fix
 const getInputName = (component: any): string => {
   const cfg: any = component.config || {};
-  return (cfg.name as string) || (cfg.identifier as string) || (component.id as string);
+  return (cfg.identifier as string) || (cfg.name as string) || (component.id as string);
 };

Apply the same fix to BaseSignUp.ts line 286 in the collectInputNames() function.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts`
around lines 39 - 47, The getInputName implementation reverses the documented
precedence: check cfg.identifier first, then cfg.name, then component.id; update
the getInputName function to return (cfg.identifier as string) || (cfg.name as
string) || (component.id as string). Also make the identical precedence change
in BaseSignUp.ts inside the collectInputNames() logic so it keys form payloads
by config.identifier before falling back to config.name and then id.
packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts-110-128 (1)

110-128: ⚠️ Potential issue | 🟠 Major

isInitialized is set even when legacy.initialize fails.

If the legacy client returns false (or initialization is partial), this.isInitialized = true is still committed unconditionally. Subsequent calls then short-circuit at Line 111-113 and return true, permanently masking the failure and preventing recovery via re-invocation.

🛡️ Proposed fix
     const result = await this.legacy.initialize(authConfig, storage);
-    this.isInitialized = true;
+    this.isInitialized = result;
     return result;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts` around lines 110 -
128, The initialize method sets this.isInitialized to true regardless of the
legacy.initialize outcome, which masks failures; change initialize
(AsgardeoNuxtClient.initialize) to only set this.isInitialized = true when the
await this.legacy.initialize(authConfig, storage) returns a truthy/successful
result (store the result in a variable, check it, set this.isInitialized only on
success, and return the result), and ensure any false/failed result leaves
this.isInitialized false so subsequent calls can retry initialization.
packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts-152-177 (1)

152-177: ⚠️ Potential issue | 🟠 Major

expires_in fallback masks already-expired tokens.

At Line 159-162, expiresInSeconds correctly clamps to 0 for an expired session via Math.max(0, ...). But Line 170 then writes String(expiresInSeconds || 3600), which treats 0 as falsy and reports a fresh 3600s window to the legacy storage manager. Downstream getAccessToken logic that consults expires_in against created_at will then believe an expired token is still valid for an hour, defeating the proactive refresh path.

🛡️ Proposed fix
-        expires_in: String(expiresInSeconds || 3600),
+        expires_in: String(
+          typeof session.accessTokenExpiresAt === 'number' ? expiresInSeconds : 3600,
+        ),

This preserves the 3600s default only when accessTokenExpiresAt is genuinely absent, while honoring an explicit 0 (expired) for sessions whose accessTokenExpiresAt is known.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts` around lines 152 -
177, The expires_in fallback currently uses String(expiresInSeconds || 3600)
which treats a valid 0 (expired) as falsy and overwrites it with 3600; in
rehydrateSessionFromPayload update the call to storageManager.setSessionData
(and the expires_in field) to use the computed expiresInSeconds directly (e.g.,
String(expiresInSeconds)) so that explicit 0 values are preserved while still
honoring the default set when accessTokenExpiresAt is absent.

Comment on lines +70 to +82
const user = authState.value.user as Record<string, unknown> | null;

if (requireOrganization && !user?.['organizationId']) {
return navigateTo(redirectTo, {external: true});
}

if (requireScopes.length > 0) {
const sessionScopes = String(user?.['scopes'] ?? '').split(' ');
const hasAllScopes = requireScopes.every((s) => sessionScopes.includes(s));
if (!hasAllScopes) {
return navigateTo(redirectTo, {external: true});
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm what fields are actually attached to ssr.user / authState.user.
fd -t f 'asgardeo-ssr.ts' --exec rg -nP -C3 '\bssr\.(user|scopes|organizationId)\b|\buser\s*:\s*\{' {}
ast-grep --pattern 'event.context.asgardeo = {
  $$$
}'
rg -nP -C3 'asgardeo:auth' --type=ts

Repository: asgardeo/javascript

Length of output: 5785


🏁 Script executed:

# Find and examine asgardeo-ssr.ts to see what's in ssrData
fd -t f 'asgardeo-ssr.ts' -path '*/runtime/server/*' | head -1

Repository: asgardeo/javascript

Length of output: 232


🏁 Script executed:

# Check size and read the asgardeo-ssr.ts file
wc -l packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts

Repository: asgardeo/javascript

Length of output: 124


🏁 Script executed:

# Find the AsgardeoAuthState type definition
rg -n 'type AsgardeoAuthState|interface AsgardeoAuthState' -A 10

Repository: asgardeo/javascript

Length of output: 363


🏁 Script executed:

# Search for organizationId usage to see where it comes from
rg -n 'organizationId' -B 2 -A 2

Repository: asgardeo/javascript

Length of output: 50377


🏁 Script executed:

# Search for scopes attribute on user object
rg -n '"scopes"|\'scopes\'' -B 2 -A 2

Repository: asgardeo/javascript

Length of output: 132


🏁 Script executed:

rg -n 'type User|interface User' -A 15 packages/nuxt/src/runtime/types.ts

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# Read the asgardeo-ssr.ts file to see how user is extracted from session
sed -n '1,100p' packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts | head -80

Repository: asgardeo/javascript

Length of output: 3475


🏁 Script executed:

# Look for where user is set from session
rg -n 'user.*=.*session|\.user' packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts -A 2 -B 2

Repository: asgardeo/javascript

Length of output: 1241


🏁 Script executed:

# Check how authState is hydrated in asgardeo.ts plugin
sed -n '80,120p' packages/nuxt/src/runtime/plugins/asgardeo.ts

Repository: asgardeo/javascript

Length of output: 1765


🏁 Script executed:

# Find the User type definition
rg -n 'export.*type User|export.*interface User' packages/nuxt/src/runtime/types.ts -A 10

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# Search for User imports in types.ts
head -40 packages/nuxt/src/runtime/types.ts | grep -i import

Repository: asgardeo/javascript

Length of output: 207


🏁 Script executed:

# Find AsgardeoNuxtClient and getUser implementation
fd -type f 'AsgardeoNuxtClient.ts'

Repository: asgardeo/javascript

Length of output: 234


🏁 Script executed:

# Check where User comes from in the types file
rg -n 'import.*User|from.*User' packages/nuxt/src/runtime/types.ts

Repository: asgardeo/javascript

Length of output: 172


🏁 Script executed:

# Find AsgardeoNuxtClient implementation
find packages/nuxt -name 'AsgardeoNuxtClient.ts' -o -name 'AsgardeoNuxtClient.js'

Repository: asgardeo/javascript

Length of output: 118


🏁 Script executed:

# Search for client.getUser or getUser implementations
rg -n 'getUser' packages/nuxt/src/runtime -B 2 -A 5 | head -100

Repository: asgardeo/javascript

Length of output: 9456


🏁 Script executed:

# Check the test file to see what mock user object looks like
rg -n 'MOCK_AUTH_STATE|mockUser|user.*=.*{' packages/nuxt/tests/unit/define-asgardeo-middleware.test.ts | head -20

Repository: asgardeo/javascript

Length of output: 197


🏁 Script executed:

# Check asgardeo-ssr.test.ts for how user is mocked
rg -n 'user.*organizationId|user.*scopes' packages/nuxt/tests/unit/asgardeo-ssr.test.ts -B 3 -A 3

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# Read the test file to see mock user setup
sed -n '75,120p' packages/nuxt/tests/unit/define-asgardeo-middleware.test.ts

Repository: asgardeo/javascript

Length of output: 2087


🏁 Script executed:

# Check if `@asgardeo/node` is in this repo or external
find packages -name '*.ts' -path '*/node/*' | head -5

Repository: asgardeo/javascript

Length of output: 300


🏁 Script executed:

# Look for what `@asgardeo/node` exports or if it's documented
rg -l '@asgardeo/node' packages/nuxt/src --type=ts | head -3

Repository: asgardeo/javascript

Length of output: 199


🏁 Script executed:

# Check the session manager test to see what user object structure is expected
sed -n '200,250p' packages/nuxt/tests/unit/session-manager.test.ts

Repository: asgardeo/javascript

Length of output: 1635


🏁 Script executed:

# Look for the actual User type definition or what getUser returns
find packages/node -name '*.ts' -exec grep -l 'type User\|interface User' {} \;

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# Check what fields come from `@asgardeo/node` for User
rg -n 'export.*User' packages/node/src -B 2 -A 8 | head -80

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# See if there's documentation on what User contains in the node package
head -100 packages/node/src/AsgardeoNodeClient.ts | grep -A 20 'getUser'

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# Check actual test setup - what do real session payloads look like
rg -n 'sub|email|organizationId|scopes' packages/nuxt/tests/unit/asgardeo-ssr.test.ts | grep -i mock | head -15

Repository: asgardeo/javascript

Length of output: 693


🏁 Script executed:

# Check AsgardeoSSRData type to see what data is available
rg -n 'type AsgardeoSSRData|interface AsgardeoSSRData' packages/nuxt/src/runtime/types.ts -A 15

Repository: asgardeo/javascript

Length of output: 1051


🏁 Script executed:

# Check how organizationId is accessed elsewhere in the code
rg -n 'organizationId' packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts -B 3 -A 3

Repository: asgardeo/javascript

Length of output: 546


🏁 Script executed:

# Check what the NextJS equivalent does
rg -n 'organizationId.*user|requireOrganization' packages/nextjs/src --type=ts | head -20

Repository: asgardeo/javascript

Length of output: 705


🏁 Script executed:

# Check if there's any transformation of user object that adds these fields
rg -n 'user.*organizationId|scopes.*user' packages/nuxt/src/runtime -B 2 -A 2 | head -40

Repository: asgardeo/javascript

Length of output: 598


🏁 Script executed:

# Check AsgardeoSessionPayload to see if it has scopes
rg -n 'interface AsgardeoSessionPayload|type AsgardeoSessionPayload' packages/nuxt/src/runtime/types.ts -A 10

Repository: asgardeo/javascript

Length of output: 576


🏁 Script executed:

# Check if there's any code that properly accesses organizationId from session or currentOrg
rg -n 'currentOrganization|session.*organizationId' packages/nuxt/src/runtime -B 1 -A 1 | head -30

Repository: asgardeo/javascript

Length of output: 2932


Access organizationId and scopes from the session payload, not the user object.

The authState.user object (from @asgardeo/node) does not include organizationId or scopes. These fields live on AsgardeoSessionPayload (available via authState.value.session or middleware context). Checking user?.['organizationId'] and user?.['scopes'] will always fail, causing:

  • requireOrganization: true to reject all authenticated users (even those with a valid org)
  • requireScopes checks to reject all authenticated users (even those with valid scopes)

Update the middleware to access these fields from the session payload. Additionally, the String(...).split(' ') approach creates empty tokens when separators contain multiple spaces; use .split(/\s+/).filter(Boolean) instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts` around
lines 70 - 82, The middleware is reading organizationId and scopes from
authState.value.user which are actually on the session payload; update the
checks in defineAsgardeoMiddleware to read authState.value.session (or
middleware context session) for organizationId and scopes (use
session['organizationId'] and session['scopes'] instead of
user['organizationId'] / user['scopes']); also replace String(...).split(' ')
with a whitespace-safe split like .split(/\s+/).filter(Boolean) when deriving
sessionScopes so requireOrganization and requireScopes logic
(requireOrganization, requireScopes, hasAllScopes, navigateTo) behave correctly.

Comment on lines +410 to +420
override async getAllOrganizations(options?: any, sessionId?: string): Promise<AllOrganizationsApiResponse> {
const resolvedSessionId = sessionId ?? '';
const accessToken: string = await this.getAccessToken(resolvedSessionId);
const configData = await this.legacy.getConfigData?.() as (AuthClientConfig<AsgardeoNuxtConfig> | undefined);
const baseUrl: string = (configData?.baseUrl ?? '') as string;

return getAllOrganizations({
baseUrl,
headers: {Authorization: `Bearer ${accessToken}`},
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the route handler passes options that this method currently drops.
fd -e ts 'index.get.ts' packages/nuxt/src/runtime/server/routes/auth/organizations | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

# And inspect the `@asgardeo/node` helper's expected request shape.
rg -nP -C3 'export\s+(async\s+)?(function|const)\s+getAllOrganizations\b'

Repository: asgardeo/javascript

Length of output: 2056


🏁 Script executed:

# Find the `@asgardeo/node` package
fd -t f -e ts -e js 'getAllOrganizations' packages/ | head -20

# Also check if there's a node_modules or type definitions
find . -type f -name '*.d.ts' | xargs grep -l 'getAllOrganizations' 2>/dev/null | head -10

Repository: asgardeo/javascript

Length of output: 401


🏁 Script executed:

# Search in the entire codebase for the getAllOrganizations import and definition
rg -n 'getAllOrganizations' --type ts --type js -A 5 | head -100

Repository: asgardeo/javascript

Length of output: 8508


🏁 Script executed:

cat packages/nextjs/src/server/actions/getAllOrganizations.ts

Repository: asgardeo/javascript

Length of output: 1638


🏁 Script executed:

cat packages/javascript/src/api/getAllOrganizations.ts

Repository: asgardeo/javascript

Length of output: 5468


Forward the options parameter to the helper function to enable pagination and filtering.

The options parameter accepted by getAllOrganizations(options?, sessionId?) is never forwarded to the underlying helper call. This means any filter, limit, recursive, or other query parameters provided by callers are silently ignored, always returning a default unfiltered first page of 10 organizations.

The helper function at packages/javascript/src/api/getAllOrganizations.ts explicitly supports these parameters:

  • filter?: string
  • limit?: number (defaults to 10)
  • recursive?: boolean (defaults to false)
🛠️ Proposed fix
-  override async getAllOrganizations(options?: any, sessionId?: string): Promise<AllOrganizationsApiResponse> {
+  override async getAllOrganizations(
+    options?: Parameters<typeof getAllOrganizations>[0] extends infer P
+      ? Omit<P, 'baseUrl' | 'headers'>
+      : any,
+    sessionId?: string,
+  ): Promise<AllOrganizationsApiResponse> {
     const resolvedSessionId = sessionId ?? '';
     const accessToken: string = await this.getAccessToken(resolvedSessionId);
     const configData = await this.legacy.getConfigData?.() as (AuthClientConfig<AsgardeoNuxtConfig> | undefined);
     const baseUrl: string = (configData?.baseUrl ?? '') as string;

     return getAllOrganizations({
+      ...(options ?? {}),
       baseUrl,
       headers: {Authorization: `Bearer ${accessToken}`},
     });
   }

Also tighten the options type away from any to match the helper's request shape for better IDE autocomplete and type safety on filter/pagination parameters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts` around lines 410 -
420, The getAllOrganizations method in AsgardeoNuxtClient currently ignores its
options parameter; update override async getAllOrganizations(options?: any,
sessionId?: string) to accept a typed options object matching the helper's
request shape (e.g., { filter?: string; limit?: number; recursive?: boolean })
instead of any, pass that options object through to the helper
getAllOrganizations call (alongside baseUrl and headers), and keep existing
resolution of sessionId, accessToken (getAccessToken), and configData
(legacy.getConfigData) unchanged so pagination/filtering are applied correctly.

Comment on lines +56 to +89
const body: {code?: string; state?: string; sessionState?: string} = await readBody(event);
const {code, state, sessionState} = body ?? {};

if (!code) {
throw createError({statusCode: 400, statusMessage: 'Missing required parameter: code'});
}

// ── Resolve sessionId from temp session cookie ────────────────────────────
const tempCookie = getCookie(event, getTempSessionCookieName());
if (!tempCookie) {
throw createError({statusCode: 400, statusMessage: 'No active auth session found. Please restart sign-in.'});
}

let sessionId: string;
try {
const tempSession = await verifyTempSessionToken(tempCookie, sessionSecret);
sessionId = tempSession.sessionId;
} catch {
throw createError({statusCode: 400, statusMessage: 'Auth session expired or invalid. Please restart sign-in.'});
}

// ── Exchange code for tokens ──────────────────────────────────────────────
const client = AsgardeoNuxtClient.getInstance();

let tokenResponse: any;
try {
tokenResponse = await client.signIn(
{code, state, session_state: sessionState},
{},
sessionId,
);
} catch (err: any) {
return {success: false, error: err?.message ?? String(err)};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for state comparison logic inside the AsgardeoNuxtClient and `@asgardeo/node` sign-in path.
fd -t f 'AsgardeoNuxtClient' packages/nuxt/src
rg -nP --type=ts -C5 '\bstate\b' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts || true
rg -nP --type=ts -C3 '(verify|validate|compare).*state|state.*(verify|validate|compare|!==|===)' packages/nuxt/src
# Check what's stored in the temp session token and how it's verified
rg -nP --type=ts -C5 '\b(verifyTempSessionToken|issueTempSessionCookie|TempSession)' packages/nuxt/src

Repository: asgardeo/javascript

Length of output: 12918


🏁 Script executed:

# Check what fields are included when creating the temp session token
rg -nP --type=ts -B10 -A15 'createTempSessionToken|issueTempSessionCookie' packages/nuxt/src/runtime/server/utils/session.ts

# Check the full definition of the TempSession type/interface
rg -nP --type=ts 'interface TempSession|type TempSession' packages/nuxt/src

# Look for state storage in the signin.post.ts flow - where state might be stored
rg -nP --type=ts -B5 -A10 'client\.signIn' packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts

# Check if MemoryCacheStore or state validation happens inside client.signIn
fd -t f 'MemoryCacheStore' packages/
rg -nP --type=ts 'state.*validate|validate.*state' packages/nuxt/src/runtime

Repository: asgardeo/javascript

Length of output: 2211


🏁 Script executed:

# Check the full callback.get.ts implementation for state handling
cat -n packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts

# Check if state is validated anywhere else in the codebase
rg -nP --type=ts 'state.*==|state.*!=|validateState|compareState' packages/nuxt/src

# Look for any OAuth state validation patterns
rg -nP --type=ts -C3 'OAuthStateInvalid' packages/nuxt/src

# Check what `@asgardeo/node` library does (search for imports/usage)
rg -nP --type=ts '@asgardeo/node' packages/nuxt/src | head -20

Repository: asgardeo/javascript

Length of output: 8389


State CSRF validation is delegated entirely to the @asgardeo/node library without explicit verification at the endpoint level.

The endpoint accepts state from the request body (line 58) but the temp session JWT contains only sessionId and returnTo—no stored original state to validate against. The state is passed directly into client.signIn() (line 81) with the assumption that @asgardeo/node's internal MemoryCacheStore will validate it. However, no explicit validation occurs in this handler or the temp session layer.

This creates a critical CSRF/login-fixation risk if:

  • The underlying library's state validation fails, is misconfigured, or regresses
  • There is no fallback validation at the application level

Recommendation: Either store the original state in the temp session JWT and validate it here before delegating to client.signIn(), or add an explicit assertion that the library has validated state (with clear documentation of the assumption).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts` around
lines 56 - 89, The handler accepts a client-supplied state and relies solely on
AsgardeoNuxtClient.signIn() for CSRF protection; fix by persisting and
validating the original state from the temp session token before calling
client.signIn(): when creating the temp session write the generated state into
the temp session JWT, update verifyTempSessionToken() to return that saved state
(e.g., tempSession.state along with tempSession.sessionId), then in this
endpoint compare body.state to tempSession.state and throw a 400 error if they
differ or are missing (referencing getTempSessionCookieName,
verifyTempSessionToken, and AsgardeoNuxtClient.signIn); alternatively, if you
intentionally rely on the library, add an explicit assertion/logging step after
signIn that confirms state validation occurred and document that assumption.

import useAsgardeo from '../../../composables/useAsgardeo';

export type SignUpRenderProps = BaseSignUpRenderProps;
export type {SignUpRenderProps} from './v2/SignUp';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare V1 vs V2 SignUp render props shapes.
ast-grep --pattern 'export type SignUpRenderProps = $$$' 
rg -nP --type=ts -C5 'SignUpRenderProps' packages/vue/src

Repository: asgardeo/javascript

Length of output: 11925


🏁 Script executed:

# Search for usages of SignUpRenderProps in the codebase (excluding node_modules/dist)
rg -nP --type=ts --type=tsx 'SignUpRenderProps' --glob='!node_modules' --glob='!dist' 

# Check if there are any V1-specific usages or tests
fd -e ts -e tsx | xargs grep -l 'SignUpV1\|v1.*SignUp' | head -20

Repository: asgardeo/javascript

Length of output: 635


🏁 Script executed:

# Get context around SignUpRenderProps usages with proper output
rg 'SignUpRenderProps' -B 2 -A 2 --type ts packages/vue/src

# Also check the actual interface definitions side-by-side
echo "=== V2 BaseSignUpRenderProps ===" && \
sed -n '67,80p' packages/vue/src/components/presentation/sign-up/v2/BaseSignUp.ts && \
echo "" && \
echo "=== V1 BaseSignUpRenderProps ===" && \
sed -n '54,62p' packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts

Repository: asgardeo/javascript

Length of output: 6454


🏁 Script executed:

# Check the dispatcher logic in SignUp.ts to see routing behavior
sed -n '25,80p' packages/vue/src/components/presentation/sign-up/SignUp.ts

# Look for tests or examples using SignUp with V1
fd -e spec -e test | xargs grep -l 'SignUp' | head -5

# Check if there are any example/demo usages
fd -type d -name examples -o -name demo | head -5

Repository: asgardeo/javascript

Length of output: 998


🏁 Script executed:

# Check if there's a way to import both v1 and v2 types
cat packages/vue/src/components/presentation/sign-up/v1/SignUp.ts | head -35

# Check if BaseSignUpRenderProps v1 and v2 are re-exported anywhere for union purposes
rg 'BaseSignUpRenderProps' packages/vue/src/index.ts

Repository: asgardeo/javascript

Length of output: 1429


Breaking change: exported SignUpRenderProps type doesn't match default V1 behavior.

The dispatcher routes to V1 by default but exports V2's SignUpRenderProps type. Consumers using the default V1 path will receive render props with errors (V1) at runtime but the type annotation specifies fieldErrors (V2), causing silent mismatches. V1 users expecting props.errors, props.isLoading will face missing properties from the perspective of TypeScript.

Consider exporting a union type of both V1 and V2 shapes, or clarify which version is the canonical type contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/SignUp.ts` at line 25, The
exported SignUpRenderProps currently re-exports the V2 type from './v2/SignUp'
while the dispatcher defaults to V1, causing a type/shape mismatch (V1 uses
errors/isLoading, V2 uses fieldErrors). Fix by exporting a correct union or
explicit names: import both V1 and V2 prop types (e.g. V1SignUpRenderProps,
V2SignUpRenderProps from './v1/SignUp' and './v2/SignUp'), then export either a
union type SignUpRenderProps = V1SignUpRenderProps | V2SignUpRenderProps or
export both types separately (e.g. SignUpRenderPropsV1 and SignUpRenderPropsV2)
and update consumers accordingly so the exported symbol matches runtime dispatch
behavior and includes errors, fieldErrors, and isLoading.

Comment on lines +516 to +555
watch(
() => [props.isInitialized, isFlowInitialized.value] as [boolean, boolean],
([initialized, flowInit]: [boolean, boolean]) => {
// Skip if URL has OAuth code params
const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
if (urlParams.get('code') || urlParams.get('state')) return;

if (initialized && !flowInit && !initializationAttempted) {
initializationAttempted = true;

(async (): Promise<void> => {
isLoading.value = true;
apiError.value = null;
flowMessages.value = [];

try {
const rawResponse: EmbeddedFlowExecuteResponse = await props.onInitialize();
const response: EmbeddedFlowExecuteResponse = normalizeFlowResponseLocal(rawResponse);
currentFlow.value = response;
isFlowInitialized.value = true;
props.onFlowChange?.(response);

if (response.flowStatus === EmbeddedFlowStatus.Complete) {
props.onComplete?.(response);
return;
}
if (response.flowStatus === EmbeddedFlowStatus.Incomplete) {
setupFormFields(response);
}
} catch (err) {
handleError(err);
props.onError?.(err as Error);
} finally {
isLoading.value = false;
}
})();
}
},
{immediate: true},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

immediate: true watcher accesses window synchronously → SSR crash in Nuxt.

The watcher runs its callback during setup() because of immediate: true. On the server (Nuxt SSR), line 520's new URL(window.location.href).searchParams throws ReferenceError: window is not defined, taking down the whole render. Guard the window access (and the onInitialize() call below it, since it ultimately reaches window in v2/SignUp.ts as well).

Proposed fix
       ([initialized, flowInit]: [boolean, boolean]) => {
-        // Skip if URL has OAuth code params
-        const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
-        if (urlParams.get('code') || urlParams.get('state')) return;
+        // Defer initialization to the client — SSR has no window/URL params.
+        if (typeof window === 'undefined') return;
+        // Skip if URL has OAuth code params
+        const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
+        if (urlParams.get('code') || urlParams.get('state')) return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watch(
() => [props.isInitialized, isFlowInitialized.value] as [boolean, boolean],
([initialized, flowInit]: [boolean, boolean]) => {
// Skip if URL has OAuth code params
const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
if (urlParams.get('code') || urlParams.get('state')) return;
if (initialized && !flowInit && !initializationAttempted) {
initializationAttempted = true;
(async (): Promise<void> => {
isLoading.value = true;
apiError.value = null;
flowMessages.value = [];
try {
const rawResponse: EmbeddedFlowExecuteResponse = await props.onInitialize();
const response: EmbeddedFlowExecuteResponse = normalizeFlowResponseLocal(rawResponse);
currentFlow.value = response;
isFlowInitialized.value = true;
props.onFlowChange?.(response);
if (response.flowStatus === EmbeddedFlowStatus.Complete) {
props.onComplete?.(response);
return;
}
if (response.flowStatus === EmbeddedFlowStatus.Incomplete) {
setupFormFields(response);
}
} catch (err) {
handleError(err);
props.onError?.(err as Error);
} finally {
isLoading.value = false;
}
})();
}
},
{immediate: true},
);
watch(
() => [props.isInitialized, isFlowInitialized.value] as [boolean, boolean],
([initialized, flowInit]: [boolean, boolean]) => {
// Defer initialization to the client — SSR has no window/URL params.
if (typeof window === 'undefined') return;
// Skip if URL has OAuth code params
const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
if (urlParams.get('code') || urlParams.get('state')) return;
if (initialized && !flowInit && !initializationAttempted) {
initializationAttempted = true;
(async (): Promise<void> => {
isLoading.value = true;
apiError.value = null;
flowMessages.value = [];
try {
const rawResponse: EmbeddedFlowExecuteResponse = await props.onInitialize();
const response: EmbeddedFlowExecuteResponse = normalizeFlowResponseLocal(rawResponse);
currentFlow.value = response;
isFlowInitialized.value = true;
props.onFlowChange?.(response);
if (response.flowStatus === EmbeddedFlowStatus.Complete) {
props.onComplete?.(response);
return;
}
if (response.flowStatus === EmbeddedFlowStatus.Incomplete) {
setupFormFields(response);
}
} catch (err) {
handleError(err);
props.onError?.(err as Error);
} finally {
isLoading.value = false;
}
})();
}
},
{immediate: true},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v2/BaseSignUp.ts` around
lines 516 - 555, The watcher callback is running on the server because of
immediate: true and directly accesses window (new URL(window.location.href)),
causing SSR crashes; fix by preventing server-side execution: remove immediate:
true from the watch options and instead trigger the same initialization check
inside an onMounted() hook so it only runs on the client, and inside the watcher
callback (and before calling props.onInitialize) add an early return guard if
typeof window === 'undefined' to ensure no window access during SSR; reference
watch, onMounted, props.onInitialize, props.isInitialized and isFlowInitialized
when making the change.

Comment on lines +55 to +68
const handleInitialize = async (
payload?: EmbeddedFlowExecuteRequestPayload,
): Promise<EmbeddedFlowExecuteResponse> => {
const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
const applicationIdFromUrl: string | null = urlParams.get('applicationId');
const effectiveApplicationId: string | undefined = applicationId || applicationIdFromUrl || undefined;

const initialPayload: any = payload || {
flowType: EmbeddedFlowType.Registration,
...(effectiveApplicationId && {applicationId: effectiveApplicationId}),
};

return (await signUp(initialPayload)) as EmbeddedFlowExecuteResponse;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

SSR-unsafe window.location.href access (regression vs the V1 sibling).

handleInitialize is invoked synchronously by BaseSignUp's immediate: true initialization watcher, which can fire during SSR. In Nuxt, this component is consumed via @asgardeo/nuxt's server-rendered tree, so reaching for window.location.href here will throw ReferenceError: window is not defined. The V1 file at packages/vue/src/components/presentation/sign-up/v1/SignUp.ts line 67 already guards this with typeof window !== 'undefined' — please mirror that guard.

Proposed fix
-    const handleInitialize = async (
-      payload?: EmbeddedFlowExecuteRequestPayload,
-    ): Promise<EmbeddedFlowExecuteResponse> => {
-      const urlParams: URLSearchParams = new URL(window.location.href).searchParams;
-      const applicationIdFromUrl: string | null = urlParams.get('applicationId');
-      const effectiveApplicationId: string | undefined = applicationId || applicationIdFromUrl || undefined;
+    const handleInitialize = async (
+      payload?: EmbeddedFlowExecuteRequestPayload,
+    ): Promise<EmbeddedFlowExecuteResponse> => {
+      const applicationIdFromUrl: string | null =
+        typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get('applicationId') : null;
+      const effectiveApplicationId: string | undefined = applicationId || applicationIdFromUrl || undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v2/SignUp.ts` around lines
55 - 68, handleInitialize currently reads window.location.href during
synchronous initialization (triggered by BaseSignUp's immediate watcher) which
breaks SSR; update handleInitialize to guard access to window by checking typeof
window !== 'undefined' before using window.location.href, and only derive
applicationIdFromUrl when that check passes (otherwise leave
effectiveApplicationId as applicationId or undefined); ensure the rest of the
logic (constructing initialPayload and calling signUp) uses that guarded
effectiveApplicationId so SSR never references window.

@kavindadimuthu kavindadimuthu force-pushed the nuxt-sdk-start-without-playground branch from a89ee00 to 90e4b65 Compare April 27, 2026 11:14
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (14)
packages/nuxt/src/runtime/errors/asgardeo-error.ts (2)

39-67: Consider widening code typing or accepting a generic to allow consumer-defined error codes.

code: ErrorCode tightly couples AsgardeoError to the SDK-internal enum. If downstream consumers (or future SDK extensions like SCIM2/organization plugins) need to throw AsgardeoError with a code outside this enum, they'd have to either extend the enum or cast. A common pattern for SDK error classes is code: ErrorCode | (string & {}) (preserving autocomplete while allowing arbitrary strings) or making the class generic on the code type:

export class AsgardeoError<TCode extends string = ErrorCode> extends Error {
  readonly code: TCode;
  // ...
}

Not a blocker — only worth doing if extensibility is an explicit goal for this SDK's public surface. Otherwise the strict ErrorCode typing is fine and arguably safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/errors/asgardeo-error.ts` around lines 39 - 67, The
AsgardeoError class currently types the code property as the internal ErrorCode
which prevents consumers from using custom codes; update the class to accept a
wider code type by making AsgardeoError generic (e.g., AsgardeoError<TCode
extends string = ErrorCode>) and change the readonly code type to TCode, and
adjust the constructor signature to accept code: TCode (propagating TCode to any
callers), or alternatively change code to ErrorCode | (string & {}) to allow
arbitrary strings while preserving autocompletion; update references to
AsgardeoError, its constructor, and the code property accordingly.

57-62: Consider leveraging ES2022 native Error constructor with cause option and conditionally assign optional fields.

The refactoring improves code quality in two ways:

  1. Native cause integration: Pass cause to the Error constructor rather than assigning it afterward. Since your project targets ESNext and uses TypeScript 5.8.3, the ES2022 Error constructor with { cause } option is available. This integrates with engine-level formatting and makes the error chain more idiomatic.

  2. Clean property shape: Unconditionally assigning this.statusCode = opts?.statusCode and this.context = opts?.context creates own properties with undefined values even when the caller doesn't supply them. Conditional assignment keeps the property shape clean.

♻️ Proposed refactor
   constructor(
     message: string,
     code: ErrorCode,
     opts?: {
       cause?: unknown;
       context?: Record<string, unknown>;
       statusCode?: number;
     },
   ) {
-    super(message);
+    super(message, opts?.cause !== undefined ? { cause: opts.cause } : undefined);
     this.name = 'AsgardeoError';
     this.code = code;
-    this.statusCode = opts?.statusCode;
-    this.cause = opts?.cause;
-    this.context = opts?.context;
+    if (opts?.statusCode !== undefined) this.statusCode = opts.statusCode;
+    if (opts?.context !== undefined) this.context = opts.context;
 
     // Maintain correct prototype chain in transpiled environments
     Object.setPrototypeOf(this, new.target.prototype);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/errors/asgardeo-error.ts` around lines 57 - 62,
Refactor the AsgardeoError constructor to pass opts?.cause into the Error
constructor (i.e., call super(message, { cause: opts?.cause })) instead of
assigning this.cause afterwards, remove the explicit this.cause assignment, and
keep this.name and this.code as-is; also conditionally set this.statusCode and
this.context only when opts?.statusCode or opts?.context are defined (so you
don't create undefined own properties). Target the AsgardeoError
class/constructor in the diff when applying these changes.
packages/nuxt/src/runtime/server/utils/token-refresh.ts (1)

74-80: Consider clearing the session cookie when refresh fails permanently.

When the refresh token is missing/invalid, this throws a 401 but leaves the now-unusable session cookie in place. Subsequent authenticated requests will keep verifying the cookie, hit this same path, and fail again — and useServerSession won't naturally clear it. For genuine "Please sign in again" responses (especially invalid_grant from the IdP), it would be cleaner to deleteCookie(event, getSessionCookieName(), getSessionCookieOptions()) before throwing so the client gets a clean state.

Also applies to: 112-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` around lines 74 -
80, When the refresh path detects a missing/invalid refresh token (the block
checking session.refreshToken and throwing createError), delete the session
cookie before throwing so the client is left in a clean unauthenticated state;
call deleteCookie(event, getSessionCookieName(), getSessionCookieOptions())
immediately prior to throwing the 401 error. Apply the same change to the
similar error branch around the later refresh failure (the block handling
invalid_grant / permanent refresh failures) so both places remove the cookie
(use the existing getSessionCookieName() and getSessionCookieOptions() helpers
and the deleteCookie function) before throwing.
packages/nuxt/src/runtime/components/user/UserDropdown.ts (2)

48-48: Use SetupContext instead of inline any types for slots/emit.

Vue exposes SetupContext (already used in the sibling UserProfile.ts) which gives proper typing for slots and emit. The current {emit: any; slots: any} type signature loses event-name autocompletion and slot typing.

-import {type Component, type Ref, type VNode, defineComponent, h, ref} from 'vue';
+import {type Component, type Ref, type SetupContext, type VNode, defineComponent, h, ref} from 'vue';
@@
-    setup(props: {className: string}, {slots, emit}: {emit: any; slots: any}): () => VNode | VNode[] | null {
+    setup(props: {className: string}, {slots, emit}: SetupContext<['profileClick']>): () => VNode | VNode[] | null {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/user/UserDropdown.ts` at line 48,
Replace the inline any types for slots/emit in the setup signature with Vue's
SetupContext: import SetupContext from 'vue' (or use named import) and change
setup(props: {className: string}, {slots, emit}: {emit: any; slots: any}) to
accept context: SetupContext and destructure slots/emit from it (or type the
second parameter as SetupContext). Update any usages in UserDropdown.ts to use
the strongly typed emit/slots so event names and slot types are inferred
correctly (match the pattern used in UserProfile.ts).

67-70: signOut() returns a Promise — silently dropping rejections.

signOut is async (it performs a redirect via navigateTo). Not awaiting it means any error during the sign-out flow is swallowed and never surfaces in console/error reporting. Either void signOut() (to make the intent explicit and lint-clean) or wrap with .catch(console.error) for visibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/user/UserDropdown.ts` around lines 67 -
70, The onSignOut handler currently calls signOut() without handling its
returned Promise, which can swallow rejections from the async signOut (which
uses navigateTo); update the onSignOut implementation to explicitly handle the
Promise by calling void signOut().catch(console.error) (or at minimum
signOut().catch(console.error)) so errors surface and lint is satisfied when
referencing the signOut function in the onSignOut handler.
packages/nuxt/src/runtime/components/user/UserProfile.ts (1)

67-68: Both class and className are forwarded — confirm BaseUserProfile uses one consistently.

class is Vue's native attribute (will be merged onto the root element automatically) while className is being passed as a Vue prop. If BaseUserProfile re-applies className as another class binding internally, you may end up with the styled prefix duplicated or the consumer-supplied className overriding the vendor class. Worth confirming the component contract; otherwise either drop className here or merge them into a single class array.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/user/UserProfile.ts` around lines 67 -
68, Confirm whether BaseUserProfile expects a class prop or a className prop and
avoid forwarding both: inspect BaseUserProfile to see if it binds
props.className into its internal class attribute (which would duplicate or
override the vendor prefix), then either remove the forwarded className from
this call site (where withVendorCSSClassPrefix('user-profile--styled') is passed
as class) or replace the two separate forwards with a single merged class
array/object that combines withVendorCSSClassPrefix('user-profile--styled') and
props.className; specifically adjust the call that currently passes class:
withVendorCSSClassPrefix('user-profile--styled') and className: props.className
to follow BaseUserProfile’s contract so vendor classes are never duplicated or
unintentionally overridden.
packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts (1)

33-33: Pass the event to useRuntimeConfig for consistency.

Sibling handlers (e.g., the token-refresh utility) call useRuntimeConfig(event). Passing the event ensures runtime-config plugins / hooks that expect the request context behave consistently across endpoints.

-  const config: ReturnType<typeof useRuntimeConfig> = useRuntimeConfig();
+  const config: ReturnType<typeof useRuntimeConfig> = useRuntimeConfig(event);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts` at line
33, The runtime config is being fetched without the request context; change the
call to useRuntimeConfig(event) so runtime-config plugins/hooks that rely on the
request are provided the event. Update the declaration where const config:
ReturnType<typeof useRuntimeConfig> = useRuntimeConfig(); to pass the existing
event variable (useRuntimeConfig(event)), ensuring the surrounding
handler/function (the session get handler) has an event parameter in scope.
packages/nuxt/src/runtime/server/utils/session.ts (1)

86-86: Floor the JWT timestamp for spec compliance.

Date.now() / 1000 + ... produces a non-integer. RFC 7519 specifies NumericDate as "the number of seconds…" and most validators (including some downstream services) expect integer values. jose itself tolerates floats, but flooring avoids surprises with stricter consumers.

-    .setExpirationTime(Date.now() / 1000 + (params.expirySeconds ?? DEFAULT_EXPIRY_SECONDS))
+    .setExpirationTime(Math.floor(Date.now() / 1000) + (params.expirySeconds ?? DEFAULT_EXPIRY_SECONDS))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/session.ts` at line 86, The JWT "exp"
value is being set to a float by using Date.now() / 1000 + (params.expirySeconds
?? DEFAULT_EXPIRY_SECONDS); update the .setExpirationTime(...) call in
session.ts (the code that constructs the token expiry) to use an integer seconds
value by applying Math.floor to the resulting NumericDate (e.g.,
Math.floor(Date.now() / 1000 + (params.expirySeconds ??
DEFAULT_EXPIRY_SECONDS))) so the token expiration conforms to RFC 7519 and
avoids float timestamps.
packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts (1)

35-47: Consider extracting the session-verification preamble.

Lines 36–42 are identical to those in current.get.ts, index.get.ts, me.get.ts, and switch.post.ts. Pulling this into a helper (e.g. requireSession(event) returning AsgardeoSessionPayload or throwing 401) would reduce duplication and ensure consistent semantics if the unauthorized response shape ever needs to change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts` around
lines 35 - 47, Extract the repeated session-verification preamble
(useRuntimeConfig -> sessionSecret retrieval, verifyAndRehydrateSession call,
null check and throwing createError 401) into a shared helper function (e.g.
requireSession(event): AsgardeoSessionPayload) and replace the duplicate block
in this handler (export default defineEventHandler in id.get.ts) with a single
call to requireSession(event); ensure the helper uses useRuntimeConfig to read
asgardeo?.sessionSecret and throws the same createError({ statusCode: 401,
statusMessage: ... }) when verifyAndRehydrateSession returns null so semantics
remain identical to the current verifyAndRehydrateSession usage.
packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts (1)

56-73: Replace callback-based authorization URL capture with direct method call.

The signIn method currently invokes a callback synchronously with the authorization URL, requiring a post-await check to verify the callback was executed. This couples the route to implicit callback timing assumptions. The AsgardeoNuxtClient already exposes getAuthorizeRequestUrl() as a direct alternative (used in signin.post.ts); refactor to use that method instead for consistency and to eliminate the fragile callback assumption.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts` around
lines 56 - 73, Replace the callback-based capture of the authorization URL via
client.signIn with a direct call to client.getAuthorizeRequestUrl: call
getAuthorizeRequestUrl with the same sessionId (and same empty params/object
used previously) to synchronously obtain the authorizationUrl, remove the
temporary authorizationUrl callback variable and the signIn invocation, then
keep the existing null-check and createError throw if getAuthorizeRequestUrl
returns null (references: client.signIn, client.getAuthorizeRequestUrl,
sessionId, authorizationUrl, createError).
packages/nuxt/src/runtime/components/auth/SignIn.ts (1)

47-115: LGTM — minor optional cleanup on the as any casts.

(await signIn({flowId: ''} as any, {} as any)) and similar in handleOnSubmit could be tightened by typing the composable's signIn overloads (or re-using the embedded payload types from @asgardeo/browser) so callers don't need to broad-cast. Not a blocker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/SignIn.ts` around lines 47 - 115,
The casts to any in SignIn are hiding types from the signIn composable; update
the signIn overloads or import and use the embedded request/response types so
handleInitialize and handleOnSubmit call signIn with properly typed arguments
instead of "(... as any)". Specifically, replace the any-casts in
handleInitialize and handleOnSubmit by using the
EmbeddedSignInFlowInitiateRequest/Response and
EmbeddedSignInFlowHandleRequestPayload/Response (or the correct exported types
from `@asgardeo/browser`) and adjust the signIn signature in useAsgardeo (or add
wrapper overloads) so signIn({flowId: ''}) and signIn(payload, request) are
strongly typed.
packages/nuxt/src/runtime/composables/useAsgardeo.ts (1)

158-162: Optional: external: true forces a full reload even for same-origin signUpUrl.

If a consumer configures signUpUrl as a same-origin path (e.g., /custom-signup), navigateTo(..., {external: true}) triggers a hard navigation rather than client-side routing. That is fine for IDP-hosted absolute URLs, but feels heavy when signUpUrl is a Nuxt route. Consider detecting same-origin / leading-slash paths and using external: false for those.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/composables/useAsgardeo.ts` around lines 158 - 162,
The current explicit override always calls navigateTo(cfg.signUpUrl, {external:
true}) which forces a full page reload even for same-origin or app-internal
paths; change the logic in useAsgardeo (the block handling cfg.signUpUrl) to
detect whether cfg.signUpUrl is an app-internal route (e.g., starts with '/' or
is a same-origin absolute URL) and call navigateTo with external: false in that
case, otherwise keep external: true for external/IDP-hosted absolute URLs;
update only the conditional around navigateTo(cfg.signUpUrl, ...) so same-origin
paths use client-side routing while external hosts still do a hard navigation.
packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts (1)

67-114: Initialization races on cold start.

The init guard if (!client.isInitialized) runs inside the request hook. Two concurrent first requests can both read isInitialized === false, both call client.initialize(...), and the legacy client may receive overlapping init calls (only the post-await flag flip protects). In practice this is short-lived (only on cold start before the first init resolves), but a single-flight promise would harden it cleanly:

♻️ Single-flight init
+let initPromise: Promise<boolean> | null = null;
 export default defineNitroPlugin((nitro: NitroApp) => {
   nitro.hooks.hook('request', async (event: H3Event) => {
     const client: AsgardeoNuxtClient = AsgardeoNuxtClient.getInstance();
     if (!client.isInitialized) {
       // …existing config resolution…
-      try {
-        await client.initialize({…});
-      } catch (err) { … }
+      initPromise = initPromise ?? client.initialize({…}).catch((err) => {
+        log.error('Failed to initialize Asgardeo client:', err);
+        initPromise = null;
+        return false;
+      });
+      const ok = await initPromise;
+      if (!ok) return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts` around lines 67 -
114, The init guard can race on cold start because multiple requests can see
client.isInitialized === false and call client.initialize concurrently; add a
single-flight mechanism (an initializationPromise) to serialize init: when
client.isInitialized is false, set or await a shared Promise before calling
initialize so only the first request triggers client.initialize while others
await that promise; implement this either as a static/instance property on
AsgardeoNuxtClient (e.g., AsgardeoNuxtClient.initializationPromise or
client._initializationPromise) or a module-level variable in asgardeo-ssr.ts,
ensure you set the promise before calling client.initialize, clear it on
failure, and preserve existing uses of client.isInitialized and
client.initialize.
packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts (1)

117-150: Failure flow statuses silently return success: true.

The code explicitly handles only SuccessCompleted (line 117). Other statuses fall through to the final return on line 150, including terminal-failure states FailCompleted and FailIncomplete, which incorrectly return {data: response, success: true}. This forces clients to inspect flowStatus within the response data to detect failures rather than relying on the success field.

Consider explicitly branching on FailCompleted and FailIncomplete to return success: false (or throw a 4xx error), so clients receive unambiguous failure signals.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts` around
lines 117 - 150, The handler currently only treats
EmbeddedSignInFlowStatus.SuccessCompleted as success and falls through to
"return {data: response, success: true}" for all other flowStatus values;
explicitly handle failure flow statuses (EmbeddedSignInFlowStatus.FailCompleted
and EmbeddedSignInFlowStatus.FailIncomplete) by short-circuiting before the
final return and either throw a createError with an appropriate 4xx status or
return {data: response, success: false}; update the logic around
response.flowStatus checks (in the same block that references client.signIn,
issueSessionCookie, and getTempSessionCookieName) so clients receive unambiguous
failure signals instead of relying on flowStatus in the data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/nuxt/src/module.ts`:
- Around line 196-200: The registered route middleware name 'auth' in the
addRouteMiddleware call collides with common Nuxt projects; change the name to a
namespaced identifier (e.g., 'asgardeo-auth' or 'asgardeoAuth') by updating the
addRouteMiddleware invocation in module.ts (the call that currently passes name:
'auth' and path: resolve('./runtime/middleware/auth')), and update any internal
references or docs that expect the old 'auth' name (including runtime middleware
exports or README mentions) so consumers use the new namespaced middleware
identifier.

In `@packages/nuxt/src/runtime/components/actions/SignOutButton.ts`:
- Around line 45-62: In handleSignOut (SignOutButton component) remove the
thrown AsgardeoRuntimeError to avoid an unhandled promise rejection and instead
emit the wrapped error via the error event; specifically, replace the current
catch block which does emit('error', error); throw new AsgardeoRuntimeError(...)
with a single emit('error', new AsgardeoRuntimeError(...)) (or log + emit the
wrapped error) so consumers receive the AsgardeoRuntimeError payload while still
allowing the finally block to set isLoading.value = false.

In `@packages/nuxt/src/runtime/components/AsgardeoRoot.ts`:
- Around line 144-156: The catch block in the profile PATCH handler currently
returns error: String(err) which loses useful context; update the catch in
AsgardeoRoot.ts (the async profile update function that calls $fetch and uses
onUpdateProfile) to extract a meaningful message by checking for a
FetchError-like payload (err?.data?.message), then Error instances
(err.message), and fall back to JSON.stringify(err) or a default string, and
return that value as the error field instead of String(err).

In `@packages/nuxt/src/runtime/components/auth/SignUp.ts`:
- Around line 98-125: In handleComplete, stop forcing external: true for
internal app redirects: when navigating to props.afterSignUpUrl use
navigateTo(props.afterSignUpUrl) or explicitly pass external: false so
client-side routing/SPAs are preserved; and replace the brittle substring checks
on response.data.redirectURL
(response?.data?.redirectURL.includes('auth'|'oauth')) with explicit checks —
either compare the redirectURL origin against the IdP base (e.g. const idpBase =
useRuntimeConfig().public.asgardeo.baseUrl and compare new
URL(response.data.redirectURL).origin === idpBase) or test for concrete OAuth
path segments like '/oauth2/' or '/authorize' using pathname.includes, then only
treat those as IdP redirects before deciding to
navigateTo(response.data.redirectURL).

In `@packages/nuxt/src/runtime/components/organization/OrganizationList.ts`:
- Around line 53-63: The passed "class" prop to BaseOrganizationList is ignored
because BaseOrganizationList uses inheritAttrs: false and only reads
props.className; remove the unused class:
withVendorCSSClassPrefix('organization-list--styled') and instead merge that
prefixed class into the className prop (e.g. combine with props.className) when
calling BaseOrganizationList so the prefixed class is applied; alternatively, if
you prefer leaving callers as-is, update BaseOrganizationList to explicitly
merge $attrs.class with props.className in its root element rendering (ensure
you reference BaseOrganizationList, props.className, $attrs.class and
withVendorCSSClassPrefix accordingly).
- Around line 47-50: handleSelect currently awaits switchOrganization without
catching rejections which can become unhandled if callers don't await the event
handler; update the handleSelect function to wrap the await
switchOrganization(org) call in a try/catch, emit an 'error' event with the
caught error (emit('error', error)) and optionally log it, and ensure the
function still resolves normally so callers won't get an unhandled
rejection—follow the same error-emission pattern used by
SignInButton/SignOutButton and keep the initial emit('select', org) behavior.

In `@packages/nuxt/src/runtime/components/user/UserDropdown.ts`:
- Around line 49-50: The call to useUser() in UserDropdown.ts is unused and
should be fixed: either remove the call if it was dead code, or destructure and
use the returned values (e.g., const { user: providerUser, ensureUser } =
useUser()) wherever provider data or guard behavior is required; alternatively,
if the call exists solely to assert that a user provider is present, add a clear
comment explaining that intent and keep the call. Update the code around the
existing const {user, signOut} = useAsgardeo() and the useUser() invocation to
reflect the chosen approach so there is no discarded return value.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts`:
- Around line 108-126: The initialize method currently sets this.isInitialized
to true regardless of the legacy.initialize outcome; change the logic so you
call await this.legacy.initialize(authConfig, storage), capture its boolean
return (e.g., result), and only set this.isInitialized = true when that result
is truthy, then return the result; update the initialize method (references:
initialize, this.legacy.initialize, this.isInitialized) so failed initialization
does not flip the flag and future calls will retry.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts`:
- Around line 52-57: The catch blocks in the org route handlers are leaking
internal error text into the HTTP 500 statusMessage by interpolating err (see
the catch in the createError call in organizations/id.get.ts and the same
pattern in current.get.ts, index.get.ts, switch.post.ts, signin.get.ts); change
each handler to log the full error server-side (use the existing logger or
console.error) and replace the createError statusMessage with a generic message
like "Internal server error" (optionally add a correlation id/token that you
also log so clients can reference it), keeping the statusCode 500; ensure you do
not include err.message or String(err) in the response payload but preserve the
original error when logging for debugging.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts`:
- Around line 61-71: The code unsafely casts the union result of
AsgardeoNuxtClient.switchOrganization to TokenResponse; instead, inspect the
returned value before treating it as a TokenResponse: call const response =
await AsgardeoNuxtClient.getInstance().switchOrganization(...), then check
whether (response as TokenResponse).access_token || (response as
TokenResponse).accessToken exists and only then assign tokenResponse and call
issueSessionCookie; if the result is a plain Response (no tokens) handle it the
same way the Next.js equivalent does (return/forward the Response or throw a
managed error). Ensure you reference AsgardeoNuxtClient.switchOrganization, the
local variable tokenResponse, and the call to issueSessionCookie when making the
conditional change.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts`:
- Around line 81-94: The POST handler currently returns {success:false,...} on
failures (around client.signIn and issueSessionCookie) while callback.get.ts
throws createError; make the POST align with the GET by replacing the
error-return branches in the client.signIn catch and the issueSessionCookie
catch with throw createError({ statusCode: 500, statusMessage: 'Failed to
exchange token' | 'Failed to establish session', data: { error: err?.message ??
String(err), original: err } }); ensure you still call
deleteCookie(getTempSessionCookieName()) only after a successful
issueSessionCookie and keep the successful response shape identical to the GET
handler.

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts`:
- Around line 44-50: The current try/catch around
AsgardeoNuxtClient.getInstance() / client.getUser(session.sessionId) silently
signs users out on any getUser error; change the catch behavior so a
cryptographically valid session (from verifyAndRehydrateSession) does not set
isSignedIn:false. Specifically, in session.get.ts keep isSignedIn:true on
getUser failures and return { isLoading: false, isSignedIn: true, user: null }
(or alternatively rethrow for a 5xx if you prefer that behavior) instead of
returning isSignedIn:false; locate the code around
AsgardeoNuxtClient.getInstance and client.getUser to adjust the catch branch
accordingly to match asgardeo-ssr.ts semantics.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts`:
- Around line 44-46: The current safeReturnTo computation in signin.get.ts
(variable safeReturnTo derived from returnTo) allows backslash-prefixed and
whitespace/control-character containing values that browsers normalize into
protocol-relative redirects; update the validation to only accept a returnTo
that: starts with exactly one forward slash followed by a non-slash character
(reject values that match /^\/[\\/ \t\r\n\f\v]|^\/\// or contain any backslash,
control characters or whitespace), and explicitly reject any characters outside
the visible-path charset; then store that sanitized value. Also add the same
strict validation in callback.get.ts before calling sendRedirect (re-validate
the stored returnTo or re-compute safeReturnTo there) so a tampered or stored
value cannot cause an open-redirect. Ensure you reference and update the
safeReturnTo logic and the redirect path usage where sendRedirect is invoked.

In `@packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts`:
- Around line 31-34: The route handler in token.get.ts (defineEventHandler using
getValidAccessToken) is exposing the access token to the browser; change the
handler to stop returning the token to clients and instead enforce a gate:
require a validated server-only request (e.g., check a custom internal header
like X-Internal-Api or validate a CSRF token and accept only POST), or convert
this to a server-side-only proxy endpoint that uses getValidAccessToken
internally to call downstream APIs and returns proxied responses rather than the
raw accessToken. Also update README/security docs to document the trade-off if
you intentionally permit token retrieval. Ensure checks are performed early in
the defineEventHandler and fail with 403 when the guard is not satisfied.

In `@packages/nuxt/src/runtime/server/utils/session.ts`:
- Around line 31-49: The getSecret function currently accepts any non-empty
string but its production error message promises a 32-character minimum; update
getSecret to enforce this by checking the resolved secret (from the
sessionSecret parameter or process.env['ASGARDEO_SESSION_SECRET']) for a minimum
length (e.g., >= 32) when NODE_ENV === 'production' and throw the existing Error
if too short; in non-production keep the existing dev warning and default secret
behavior. Reference: getSecret and the use of
process.env['ASGARDEO_SESSION_SECRET'] / sessionSecret.
- Around line 207-211: issueSessionCookie calls AsgardeoNuxtClient.getInstance()
and immediately uses getDecodedIdToken, but the singleton can be uninitialized
on first callback requests; add a guard after obtaining the client (from
AsgardeoNuxtClient.getInstance()) to check client.isInitialized and throw a
clear Error if false (or else ensure the callback route initializes the client
first), so getDecodedIdToken is only invoked on a properly initialized
AsgardeoNuxtClient.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts`:
- Around line 99-120: The fetch to the token endpoint in token-refresh.ts
currently has no timeout and can hang; update the token fetch call (the block
that awaits fetch(tokenEndpoint, { body, headers, method: 'POST' })) to use an
AbortSignal with a short timeout (e.g., 5–10s) — either via
AbortSignal.timeout(...) or by creating an AbortController and setTimeout to
call controller.abort(), pass signal into the fetch options, and ensure the
timeout is cleared/cleanup after the response; preserve the existing catch
behavior so aborts/timeouts are handled the same way (throwing the createError
with 401) and include this change around the same symbol(s) performing the token
refresh (the tokenEndpoint fetch inside the function that populates refreshed /
returns an OIDCTokenRefreshResponse).

In `@packages/nuxt/src/runtime/utils/createRouteMatcher.ts`:
- Around line 41-51: createRouteMatcher currently escapes the alternation pipe
via the first replace, breaking explicit groups like /api/(users|posts); update
the escape step in createRouteMatcher (the patterns -> regexStr construction) to
exclude '|' from the character class passed to .replace (i.e., remove '|' from
/[.+^${}|[\]\\]/g) so alternation inside parentheses remains unescaped and the
compiled RegExp matches either alternative; also update the inline comment near
regexStr to state that '|' inside explicit groups is preserved.

---

Nitpick comments:
In `@packages/nuxt/src/runtime/components/auth/SignIn.ts`:
- Around line 47-115: The casts to any in SignIn are hiding types from the
signIn composable; update the signIn overloads or import and use the embedded
request/response types so handleInitialize and handleOnSubmit call signIn with
properly typed arguments instead of "(... as any)". Specifically, replace the
any-casts in handleInitialize and handleOnSubmit by using the
EmbeddedSignInFlowInitiateRequest/Response and
EmbeddedSignInFlowHandleRequestPayload/Response (or the correct exported types
from `@asgardeo/browser`) and adjust the signIn signature in useAsgardeo (or add
wrapper overloads) so signIn({flowId: ''}) and signIn(payload, request) are
strongly typed.

In `@packages/nuxt/src/runtime/components/user/UserDropdown.ts`:
- Line 48: Replace the inline any types for slots/emit in the setup signature
with Vue's SetupContext: import SetupContext from 'vue' (or use named import)
and change setup(props: {className: string}, {slots, emit}: {emit: any; slots:
any}) to accept context: SetupContext and destructure slots/emit from it (or
type the second parameter as SetupContext). Update any usages in UserDropdown.ts
to use the strongly typed emit/slots so event names and slot types are inferred
correctly (match the pattern used in UserProfile.ts).
- Around line 67-70: The onSignOut handler currently calls signOut() without
handling its returned Promise, which can swallow rejections from the async
signOut (which uses navigateTo); update the onSignOut implementation to
explicitly handle the Promise by calling void signOut().catch(console.error) (or
at minimum signOut().catch(console.error)) so errors surface and lint is
satisfied when referencing the signOut function in the onSignOut handler.

In `@packages/nuxt/src/runtime/components/user/UserProfile.ts`:
- Around line 67-68: Confirm whether BaseUserProfile expects a class prop or a
className prop and avoid forwarding both: inspect BaseUserProfile to see if it
binds props.className into its internal class attribute (which would duplicate
or override the vendor prefix), then either remove the forwarded className from
this call site (where withVendorCSSClassPrefix('user-profile--styled') is passed
as class) or replace the two separate forwards with a single merged class
array/object that combines withVendorCSSClassPrefix('user-profile--styled') and
props.className; specifically adjust the call that currently passes class:
withVendorCSSClassPrefix('user-profile--styled') and className: props.className
to follow BaseUserProfile’s contract so vendor classes are never duplicated or
unintentionally overridden.

In `@packages/nuxt/src/runtime/composables/useAsgardeo.ts`:
- Around line 158-162: The current explicit override always calls
navigateTo(cfg.signUpUrl, {external: true}) which forces a full page reload even
for same-origin or app-internal paths; change the logic in useAsgardeo (the
block handling cfg.signUpUrl) to detect whether cfg.signUpUrl is an app-internal
route (e.g., starts with '/' or is a same-origin absolute URL) and call
navigateTo with external: false in that case, otherwise keep external: true for
external/IDP-hosted absolute URLs; update only the conditional around
navigateTo(cfg.signUpUrl, ...) so same-origin paths use client-side routing
while external hosts still do a hard navigation.

In `@packages/nuxt/src/runtime/errors/asgardeo-error.ts`:
- Around line 39-67: The AsgardeoError class currently types the code property
as the internal ErrorCode which prevents consumers from using custom codes;
update the class to accept a wider code type by making AsgardeoError generic
(e.g., AsgardeoError<TCode extends string = ErrorCode>) and change the readonly
code type to TCode, and adjust the constructor signature to accept code: TCode
(propagating TCode to any callers), or alternatively change code to ErrorCode |
(string & {}) to allow arbitrary strings while preserving autocompletion; update
references to AsgardeoError, its constructor, and the code property accordingly.
- Around line 57-62: Refactor the AsgardeoError constructor to pass opts?.cause
into the Error constructor (i.e., call super(message, { cause: opts?.cause }))
instead of assigning this.cause afterwards, remove the explicit this.cause
assignment, and keep this.name and this.code as-is; also conditionally set
this.statusCode and this.context only when opts?.statusCode or opts?.context are
defined (so you don't create undefined own properties). Target the AsgardeoError
class/constructor in the diff when applying these changes.

In `@packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts`:
- Around line 67-114: The init guard can race on cold start because multiple
requests can see client.isInitialized === false and call client.initialize
concurrently; add a single-flight mechanism (an initializationPromise) to
serialize init: when client.isInitialized is false, set or await a shared
Promise before calling initialize so only the first request triggers
client.initialize while others await that promise; implement this either as a
static/instance property on AsgardeoNuxtClient (e.g.,
AsgardeoNuxtClient.initializationPromise or client._initializationPromise) or a
module-level variable in asgardeo-ssr.ts, ensure you set the promise before
calling client.initialize, clear it on failure, and preserve existing uses of
client.isInitialized and client.initialize.

In `@packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts`:
- Around line 35-47: Extract the repeated session-verification preamble
(useRuntimeConfig -> sessionSecret retrieval, verifyAndRehydrateSession call,
null check and throwing createError 401) into a shared helper function (e.g.
requireSession(event): AsgardeoSessionPayload) and replace the duplicate block
in this handler (export default defineEventHandler in id.get.ts) with a single
call to requireSession(event); ensure the helper uses useRuntimeConfig to read
asgardeo?.sessionSecret and throws the same createError({ statusCode: 401,
statusMessage: ... }) when verifyAndRehydrateSession returns null so semantics
remain identical to the current verifyAndRehydrateSession usage.

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts`:
- Line 33: The runtime config is being fetched without the request context;
change the call to useRuntimeConfig(event) so runtime-config plugins/hooks that
rely on the request are provided the event. Update the declaration where const
config: ReturnType<typeof useRuntimeConfig> = useRuntimeConfig(); to pass the
existing event variable (useRuntimeConfig(event)), ensuring the surrounding
handler/function (the session get handler) has an event parameter in scope.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts`:
- Around line 56-73: Replace the callback-based capture of the authorization URL
via client.signIn with a direct call to client.getAuthorizeRequestUrl: call
getAuthorizeRequestUrl with the same sessionId (and same empty params/object
used previously) to synchronously obtain the authorizationUrl, remove the
temporary authorizationUrl callback variable and the signIn invocation, then
keep the existing null-check and createError throw if getAuthorizeRequestUrl
returns null (references: client.signIn, client.getAuthorizeRequestUrl,
sessionId, authorizationUrl, createError).

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts`:
- Around line 117-150: The handler currently only treats
EmbeddedSignInFlowStatus.SuccessCompleted as success and falls through to
"return {data: response, success: true}" for all other flowStatus values;
explicitly handle failure flow statuses (EmbeddedSignInFlowStatus.FailCompleted
and EmbeddedSignInFlowStatus.FailIncomplete) by short-circuiting before the
final return and either throw a createError with an appropriate 4xx status or
return {data: response, success: false}; update the logic around
response.flowStatus checks (in the same block that references client.signIn,
issueSessionCookie, and getTempSessionCookieName) so clients receive unambiguous
failure signals instead of relying on flowStatus in the data.

In `@packages/nuxt/src/runtime/server/utils/session.ts`:
- Line 86: The JWT "exp" value is being set to a float by using Date.now() /
1000 + (params.expirySeconds ?? DEFAULT_EXPIRY_SECONDS); update the
.setExpirationTime(...) call in session.ts (the code that constructs the token
expiry) to use an integer seconds value by applying Math.floor to the resulting
NumericDate (e.g., Math.floor(Date.now() / 1000 + (params.expirySeconds ??
DEFAULT_EXPIRY_SECONDS))) so the token expiration conforms to RFC 7519 and
avoids float timestamps.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts`:
- Around line 74-80: When the refresh path detects a missing/invalid refresh
token (the block checking session.refreshToken and throwing createError), delete
the session cookie before throwing so the client is left in a clean
unauthenticated state; call deleteCookie(event, getSessionCookieName(),
getSessionCookieOptions()) immediately prior to throwing the 401 error. Apply
the same change to the similar error branch around the later refresh failure
(the block handling invalid_grant / permanent refresh failures) so both places
remove the cookie (use the existing getSessionCookieName() and
getSessionCookieOptions() helpers and the deleteCookie function) before
throwing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec2e2681-793d-4af5-aefe-6312b86b5d85

📥 Commits

Reviewing files that changed from the base of the PR and between ddff673 and b7797c6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (56)
  • packages/nuxt/package.json
  • packages/nuxt/src/index.ts
  • packages/nuxt/src/module.ts
  • packages/nuxt/src/runtime/components/AsgardeoRoot.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
  • packages/nuxt/src/runtime/components/actions/SignOutButton.ts
  • packages/nuxt/src/runtime/components/actions/SignUpButton.ts
  • packages/nuxt/src/runtime/components/auth/Callback.ts
  • packages/nuxt/src/runtime/components/auth/SignIn.ts
  • packages/nuxt/src/runtime/components/auth/SignUp.ts
  • packages/nuxt/src/runtime/components/control/Loading.ts
  • packages/nuxt/src/runtime/components/control/SignedIn.ts
  • packages/nuxt/src/runtime/components/control/SignedOut.ts
  • packages/nuxt/src/runtime/components/organization/CreateOrganization.ts
  • packages/nuxt/src/runtime/components/organization/Organization.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationList.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts
  • packages/nuxt/src/runtime/components/user/User.ts
  • packages/nuxt/src/runtime/components/user/UserDropdown.ts
  • packages/nuxt/src/runtime/components/user/UserProfile.ts
  • packages/nuxt/src/runtime/composables/useAsgardeo.ts
  • packages/nuxt/src/runtime/errors/asgardeo-error.ts
  • packages/nuxt/src/runtime/errors/error-codes.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/src/runtime/plugins/asgardeo.ts
  • packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts
  • packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts
  • packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts
  • packages/nuxt/src/runtime/server/utils/event-context.ts
  • packages/nuxt/src/runtime/server/utils/serverSession.ts
  • packages/nuxt/src/runtime/server/utils/session.ts
  • packages/nuxt/src/runtime/server/utils/token-refresh.ts
  • packages/nuxt/src/runtime/types.ts
  • packages/nuxt/src/runtime/types/augments.d.ts
  • packages/nuxt/src/runtime/utils/createRouteMatcher.ts
  • packages/nuxt/src/runtime/utils/log.ts
  • packages/nuxt/src/runtime/utils/url-validation.ts
  • packages/nuxt/tsconfig.eslint.json
✅ Files skipped from review due to trivial changes (10)
  • packages/nuxt/tsconfig.eslint.json
  • packages/nuxt/src/runtime/errors/error-codes.ts
  • packages/nuxt/src/runtime/components/organization/Organization.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • packages/nuxt/src/runtime/components/control/SignedIn.ts
  • packages/nuxt/src/runtime/components/user/User.ts
  • packages/nuxt/src/runtime/components/control/Loading.ts
  • packages/nuxt/src/runtime/utils/url-validation.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts
  • packages/nuxt/src/runtime/utils/log.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts
  • packages/nuxt/src/runtime/server/utils/serverSession.ts
  • packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts
  • packages/nuxt/src/runtime/components/auth/Callback.ts
  • packages/nuxt/src/runtime/components/organization/CreateOrganization.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts
  • packages/nuxt/src/runtime/plugins/asgardeo.ts
  • packages/nuxt/src/runtime/components/control/SignedOut.ts
  • packages/nuxt/package.json

Comment on lines +196 to +200
// Register named route middleware for page protection
addRouteMiddleware({
name: 'auth',
path: resolve('./runtime/middleware/auth'),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Generic auth route-middleware name will collide with most existing Nuxt apps.

middleware/auth.ts is one of the most common middleware names in Nuxt projects (the canonical "is the user signed in?" guard). Registering one named 'auth' from this module means any consumer that already has an auth middleware loses theirs (or gets a name collision warning), and definePageMeta({ middleware: 'auth' }) becomes ambiguous after installation. Prefix it with the package namespace, e.g., asgardeo-auth or asgardeoAuth, and document the new name. This matches how other Nuxt auth modules (e.g., nuxt-auth-utils, @sidebase/nuxt-auth) namespace their middleware.

♻️ Suggested rename
-    addRouteMiddleware({
-      name: 'auth',
-      path: resolve('./runtime/middleware/auth'),
-    });
+    addRouteMiddleware({
+      name: 'asgardeo-auth',
+      path: resolve('./runtime/middleware/auth'),
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Register named route middleware for page protection
addRouteMiddleware({
name: 'auth',
path: resolve('./runtime/middleware/auth'),
});
// Register named route middleware for page protection
addRouteMiddleware({
name: 'asgardeo-auth',
path: resolve('./runtime/middleware/auth'),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/module.ts` around lines 196 - 200, The registered route
middleware name 'auth' in the addRouteMiddleware call collides with common Nuxt
projects; change the name to a namespaced identifier (e.g., 'asgardeo-auth' or
'asgardeoAuth') by updating the addRouteMiddleware invocation in module.ts (the
call that currently passes name: 'auth' and path:
resolve('./runtime/middleware/auth')), and update any internal references or
docs that expect the old 'auth' name (including runtime middleware exports or
README mentions) so consumers use the new namespaced middleware identifier.

Comment on lines +45 to +62
const handleSignOut = async (e?: MouseEvent): Promise<void> => {
try {
isLoading.value = true;
// signOut comes from the Nuxt plugin's ASGARDEO_KEY which uses navigateTo.
await signOut();
if (e) emit('click', e);
} catch (error) {
emit('error', error);
throw new AsgardeoRuntimeError(
`Sign out failed: ${error instanceof Error ? error.message : String(error)}`,
'SignOutButton-handleSignOut-RuntimeError-001',
'nuxt',
'Something went wrong while trying to sign out. Please try again later.',
);
} finally {
isLoading.value = false;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Async event handler will produce an unhandled promise rejection on sign-out failure.

handleSignOut is wired as onClick on BaseSignOutButton. When signOut() rejects, you both emit('error', error) and throw new AsgardeoRuntimeError(...). Since DOM/Vue event listeners do not await the returned promise, the throw becomes an unhandled rejection (visible in the console / window.onunhandledrejection) — there is no upstream try/catch to receive it. Consumers also already get notified via emit('error', error), so the throw is redundant.

Recommend either logging + emitting (no throw) or surfacing the wrapped AsgardeoRuntimeError via the error event payload instead.

🐛 Suggested change
-      } catch (error) {
-        emit('error', error);
-        throw new AsgardeoRuntimeError(
-          `Sign out failed: ${error instanceof Error ? error.message : String(error)}`,
-          'SignOutButton-handleSignOut-RuntimeError-001',
-          'nuxt',
-          'Something went wrong while trying to sign out. Please try again later.',
-        );
-      } finally {
+      } catch (error) {
+        const wrapped = new AsgardeoRuntimeError(
+          `Sign out failed: ${error instanceof Error ? error.message : String(error)}`,
+          'SignOutButton-handleSignOut-RuntimeError-001',
+          'nuxt',
+          'Something went wrong while trying to sign out. Please try again later.',
+        );
+        emit('error', wrapped);
+      } finally {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSignOut = async (e?: MouseEvent): Promise<void> => {
try {
isLoading.value = true;
// signOut comes from the Nuxt plugin's ASGARDEO_KEY which uses navigateTo.
await signOut();
if (e) emit('click', e);
} catch (error) {
emit('error', error);
throw new AsgardeoRuntimeError(
`Sign out failed: ${error instanceof Error ? error.message : String(error)}`,
'SignOutButton-handleSignOut-RuntimeError-001',
'nuxt',
'Something went wrong while trying to sign out. Please try again later.',
);
} finally {
isLoading.value = false;
}
};
const handleSignOut = async (e?: MouseEvent): Promise<void> => {
try {
isLoading.value = true;
// signOut comes from the Nuxt plugin's ASGARDEO_KEY which uses navigateTo.
await signOut();
if (e) emit('click', e);
} catch (error) {
const wrapped = new AsgardeoRuntimeError(
`Sign out failed: ${error instanceof Error ? error.message : String(error)}`,
'SignOutButton-handleSignOut-RuntimeError-001',
'nuxt',
'Something went wrong while trying to sign out. Please try again later.',
);
emit('error', wrapped);
} finally {
isLoading.value = false;
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/actions/SignOutButton.ts` around lines
45 - 62, In handleSignOut (SignOutButton component) remove the thrown
AsgardeoRuntimeError to avoid an unhandled promise rejection and instead emit
the wrapped error via the error event; specifically, replace the current catch
block which does emit('error', error); throw new AsgardeoRuntimeError(...) with
a single emit('error', new AsgardeoRuntimeError(...)) (or log + emit the wrapped
error) so consumers receive the AsgardeoRuntimeError payload while still
allowing the finally block to set isLoading.value = false.

Comment on lines +144 to +156
try {
const result: {data: {user: User}; error: string; success: boolean} = await $fetch('/api/auth/user/profile', {
body: requestConfig,
method: 'PATCH',
});
if (result?.success && result.data?.user) {
onUpdateProfile(result.data.user);
}
return result;
} catch (err) {
return {data: {user: {} as User}, error: String(err), success: false};
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

String(err) loses useful error context.

For an Error instance, String(err) yields "Error: <message>"; for a thrown plain object it yields "[object Object]". Since error is surfaced to UI consumers via UserProvider, prefer extracting err.message (or the response body, when $fetch rejects with a FetchError carrying data.message).

🛠️ Proposed fix
       } catch (err) {
-        return {data: {user: {} as User}, error: String(err), success: false};
+        const message: string =
+          (err as {data?: {message?: string}; message?: string})?.data?.message ??
+          (err as {message?: string})?.message ??
+          String(err);
+        return {data: {user: {} as User}, error: message, success: false};
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const result: {data: {user: User}; error: string; success: boolean} = await $fetch('/api/auth/user/profile', {
body: requestConfig,
method: 'PATCH',
});
if (result?.success && result.data?.user) {
onUpdateProfile(result.data.user);
}
return result;
} catch (err) {
return {data: {user: {} as User}, error: String(err), success: false};
}
};
try {
const result: {data: {user: User}; error: string; success: boolean} = await $fetch('/api/auth/user/profile', {
body: requestConfig,
method: 'PATCH',
});
if (result?.success && result.data?.user) {
onUpdateProfile(result.data.user);
}
return result;
} catch (err) {
const message: string =
(err as {data?: {message?: string}; message?: string})?.data?.message ??
(err as {message?: string})?.message ??
String(err);
return {data: {user: {} as User}, error: message, success: false};
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/AsgardeoRoot.ts` around lines 144 - 156,
The catch block in the profile PATCH handler currently returns error:
String(err) which loses useful context; update the catch in AsgardeoRoot.ts (the
async profile update function that calls $fetch and uses onUpdateProfile) to
extract a meaningful message by checking for a FetchError-like payload
(err?.data?.message), then Error instances (err.message), and fall back to
JSON.stringify(err) or a default string, and return that value as the error
field instead of String(err).

Comment on lines +98 to +125
const handleComplete = async (response: EmbeddedFlowExecuteResponse): Promise<void> => {
props.onComplete?.(response);

const oauthRedirectUrl: string | undefined = (response as any)?.redirectUrl;
if (props.shouldRedirectAfterSignUp && oauthRedirectUrl) {
// Use navigateTo instead of window.location.href — SSR-safe.
await navigateTo(oauthRedirectUrl, {external: true});
return;
}

if (
props.shouldRedirectAfterSignUp &&
response?.type !== EmbeddedFlowResponseType.Redirection &&
props.afterSignUpUrl
) {
await navigateTo(props.afterSignUpUrl, {external: true});
}

if (
props.shouldRedirectAfterSignUp &&
response?.type === EmbeddedFlowResponseType.Redirection &&
response?.data?.redirectURL &&
!response.data.redirectURL.includes('oauth') &&
!response.data.redirectURL.includes('auth')
) {
await navigateTo(response.data.redirectURL, {external: true});
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Two concerns in the post-completion redirect logic.

  1. external: true on afterSignUpUrl is wrong for in-app destinations. afterSignUpUrl is typically an internal route (the whole point of the post-sign-up redirect is to land the user in the app). Forcing external: true triggers a full-document navigation and discards the Nuxt SPA state. Use external: false (or just omit the option) so client-side routing works.

  2. includes('auth') / includes('oauth') is too coarse. The substring match will reject any redirectURL whose host or path happens to contain the letters auth (e.g. authortools.example.com, /author/123, ?author=jane). Match the IdP origin explicitly (e.g. compare against useRuntimeConfig().public.asgardeo.baseUrl) or check for the OAuth path segments (/oauth2/, /authorize) rather than the bare substring.

🛠️ Proposed fix for issue 1
       if (
         props.shouldRedirectAfterSignUp &&
         response?.type !== EmbeddedFlowResponseType.Redirection &&
         props.afterSignUpUrl
       ) {
-        await navigateTo(props.afterSignUpUrl, {external: true});
+        await navigateTo(props.afterSignUpUrl);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleComplete = async (response: EmbeddedFlowExecuteResponse): Promise<void> => {
props.onComplete?.(response);
const oauthRedirectUrl: string | undefined = (response as any)?.redirectUrl;
if (props.shouldRedirectAfterSignUp && oauthRedirectUrl) {
// Use navigateTo instead of window.location.href — SSR-safe.
await navigateTo(oauthRedirectUrl, {external: true});
return;
}
if (
props.shouldRedirectAfterSignUp &&
response?.type !== EmbeddedFlowResponseType.Redirection &&
props.afterSignUpUrl
) {
await navigateTo(props.afterSignUpUrl, {external: true});
}
if (
props.shouldRedirectAfterSignUp &&
response?.type === EmbeddedFlowResponseType.Redirection &&
response?.data?.redirectURL &&
!response.data.redirectURL.includes('oauth') &&
!response.data.redirectURL.includes('auth')
) {
await navigateTo(response.data.redirectURL, {external: true});
}
};
const handleComplete = async (response: EmbeddedFlowExecuteResponse): Promise<void> => {
props.onComplete?.(response);
const oauthRedirectUrl: string | undefined = (response as any)?.redirectUrl;
if (props.shouldRedirectAfterSignUp && oauthRedirectUrl) {
// Use navigateTo instead of window.location.href — SSR-safe.
await navigateTo(oauthRedirectUrl, {external: true});
return;
}
if (
props.shouldRedirectAfterSignUp &&
response?.type !== EmbeddedFlowResponseType.Redirection &&
props.afterSignUpUrl
) {
await navigateTo(props.afterSignUpUrl);
}
if (
props.shouldRedirectAfterSignUp &&
response?.type === EmbeddedFlowResponseType.Redirection &&
response?.data?.redirectURL &&
!response.data.redirectURL.includes('oauth') &&
!response.data.redirectURL.includes('auth')
) {
await navigateTo(response.data.redirectURL, {external: true});
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/SignUp.ts` around lines 98 - 125,
In handleComplete, stop forcing external: true for internal app redirects: when
navigating to props.afterSignUpUrl use navigateTo(props.afterSignUpUrl) or
explicitly pass external: false so client-side routing/SPAs are preserved; and
replace the brittle substring checks on response.data.redirectURL
(response?.data?.redirectURL.includes('auth'|'oauth')) with explicit checks —
either compare the redirectURL origin against the IdP base (e.g. const idpBase =
useRuntimeConfig().public.asgardeo.baseUrl and compare new
URL(response.data.redirectURL).origin === idpBase) or test for concrete OAuth
path segments like '/oauth2/' or '/authorize' using pathname.includes, then only
treat those as IdP redirects before deciding to
navigateTo(response.data.redirectURL).

Comment on lines +47 to +50
const handleSelect = async (org: IOrganization): Promise<void> => {
emit('select', org);
await switchOrganization(org);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

switchOrganization rejection is uncaught — async handler will produce an unhandled rejection.

handleSelect is awaited inside BaseOrganizationList's onSelect, but if the consumer doesn't await it (and if the base component just calls it as an event handler), a switchOrganization failure will propagate as an unhandled promise rejection. Consider catching and emitting a follow-up error event (matching the pattern used by SignInButton / SignOutButton).

♻️ Suggested change
-  emits: ['select'],
+  emits: ['select', 'error'],
   ...
-    const handleSelect = async (org: IOrganization): Promise<void> => {
-      emit('select', org);
-      await switchOrganization(org);
-    };
+    const handleSelect = async (org: IOrganization): Promise<void> => {
+      emit('select', org);
+      try {
+        await switchOrganization(org);
+      } catch (error) {
+        emit('error', error);
+      }
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/organization/OrganizationList.ts` around
lines 47 - 50, handleSelect currently awaits switchOrganization without catching
rejections which can become unhandled if callers don't await the event handler;
update the handleSelect function to wrap the await switchOrganization(org) call
in a try/catch, emit an 'error' event with the caught error (emit('error',
error)) and optionally log it, and ensure the function still resolves normally
so callers won't get an unhandled rejection—follow the same error-emission
pattern used by SignInButton/SignOutButton and keep the initial emit('select',
org) behavior.

Comment on lines +31 to +34
export default defineEventHandler(async (event: H3Event) => {
const accessToken: string = await getValidAccessToken(event);
return {accessToken};
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# See how the composable consumes /api/auth/token and what it does with the token
rg -nP -C4 'getAccessToken|/api/auth/token' packages/nuxt/src

Repository: asgardeo/javascript

Length of output: 12954


🏁 Script executed:

# Check for CSRF protection, SameSite settings, and security-related code
rg -nP -C3 'csrf|sameSite|secure|httpOnly|CORS|csrf-token' packages/nuxt/src/runtime --type ts

Repository: asgardeo/javascript

Length of output: 4698


🏁 Script executed:

# Check for documentation files that explain the security model or this endpoint
fd -e md -e rst . packages/nuxt | head -20
cat packages/nuxt/README.md 2>/dev/null | grep -A5 -B5 'token\|security\|access'

Repository: asgardeo/javascript

Length of output: 6121


🏁 Script executed:

# Check if there are any middleware or guards on the token endpoint
rg -nP -C5 'defineEventHandler.*token' packages/nuxt/src/runtime/server/routes

Repository: asgardeo/javascript

Length of output: 45


🏁 Script executed:

# Check the full token.get.ts file for any comments or security logic
cat -n packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts

Repository: asgardeo/javascript

Length of output: 1550


Access token returned in JSON defeats httpOnly session cookie security.

This endpoint returns { accessToken } directly to the browser, allowing any same-origin script to fetch it via GET /api/auth/token — precisely the threat httpOnly cookies are designed to prevent. The plugin (asgardeo.ts:199-205) does call this endpoint to fetch the token for downstream API calls, and this is documented in the README.md as intentional.

However, the endpoint has no CSRF guards, custom header checks, or gating logic to restrict access. If the intended consumer is the Nuxt composable, consider:

  • Document this security trade-off prominently in the SDK readme and security guidelines.
  • Add a guard to the endpoint (e.g., require a custom header, POST-only, or same-site check) to raise the bar for XSS exfiltration.
  • Prefer a server-side proxy pattern (/api/proxy/...) so tokens never leave the server.

This design choice should be reviewed before release to ensure it aligns with the threat model.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts` around
lines 31 - 34, The route handler in token.get.ts (defineEventHandler using
getValidAccessToken) is exposing the access token to the browser; change the
handler to stop returning the token to clients and instead enforce a gate:
require a validated server-only request (e.g., check a custom internal header
like X-Internal-Api or validate a CSRF token and accept only POST), or convert
this to a server-side-only proxy endpoint that uses getValidAccessToken
internally to call downstream APIs and returns proxied responses rather than the
raw accessToken. Also update README/security docs to document the trade-off if
you intentionally permit token retrieval. Ensure checks are performed early in
the defineEventHandler and fail with 403 when the guard is not satisfied.

Comment on lines +31 to +49
function getSecret(sessionSecret?: string): Uint8Array {
const secret: string | undefined = sessionSecret || process.env['ASGARDEO_SESSION_SECRET'];

if (!secret) {
if (process.env['NODE_ENV'] === 'production') {
throw new Error(
'[asgardeo] ASGARDEO_SESSION_SECRET environment variable is required in production. ' +
'Set it to a secure random string of at least 32 characters.',
);
}
// eslint-disable-next-line no-console
console.warn(
'[asgardeo] Using default session secret for development. Set ASGARDEO_SESSION_SECRET for production.',
);
return new TextEncoder().encode('asgardeo-dev-secret-not-for-production');
}

return new TextEncoder().encode(secret);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate session-secret length in production — error message promises a guarantee that's not enforced.

The thrown error advises "at least 32 characters", but getSecret accepts any non-empty string (e.g., ASGARDEO_SESSION_SECRET=x). HS256 with a short key materially weakens session-token security and there's no compile- or runtime-time check enforcing the documented requirement.

🛡️ Proposed fix
   if (!secret) {
     if (process.env['NODE_ENV'] === 'production') {
       throw new Error(
         '[asgardeo] ASGARDEO_SESSION_SECRET environment variable is required in production. ' +
           'Set it to a secure random string of at least 32 characters.',
       );
     }
     // eslint-disable-next-line no-console
     console.warn(
       '[asgardeo] Using default session secret for development. Set ASGARDEO_SESSION_SECRET for production.',
     );
     return new TextEncoder().encode('asgardeo-dev-secret-not-for-production');
   }
 
+  if (process.env['NODE_ENV'] === 'production' && secret.length < 32) {
+    throw new Error(
+      '[asgardeo] ASGARDEO_SESSION_SECRET must be at least 32 characters in production.',
+    );
+  }
+
   return new TextEncoder().encode(secret);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getSecret(sessionSecret?: string): Uint8Array {
const secret: string | undefined = sessionSecret || process.env['ASGARDEO_SESSION_SECRET'];
if (!secret) {
if (process.env['NODE_ENV'] === 'production') {
throw new Error(
'[asgardeo] ASGARDEO_SESSION_SECRET environment variable is required in production. ' +
'Set it to a secure random string of at least 32 characters.',
);
}
// eslint-disable-next-line no-console
console.warn(
'[asgardeo] Using default session secret for development. Set ASGARDEO_SESSION_SECRET for production.',
);
return new TextEncoder().encode('asgardeo-dev-secret-not-for-production');
}
return new TextEncoder().encode(secret);
}
function getSecret(sessionSecret?: string): Uint8Array {
const secret: string | undefined = sessionSecret || process.env['ASGARDEO_SESSION_SECRET'];
if (!secret) {
if (process.env['NODE_ENV'] === 'production') {
throw new Error(
'[asgardeo] ASGARDEO_SESSION_SECRET environment variable is required in production. ' +
'Set it to a secure random string of at least 32 characters.',
);
}
// eslint-disable-next-line no-console
console.warn(
'[asgardeo] Using default session secret for development. Set ASGARDEO_SESSION_SECRET for production.',
);
return new TextEncoder().encode('asgardeo-dev-secret-not-for-production');
}
if (process.env['NODE_ENV'] === 'production' && secret.length < 32) {
throw new Error(
'[asgardeo] ASGARDEO_SESSION_SECRET must be at least 32 characters in production.',
);
}
return new TextEncoder().encode(secret);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/session.ts` around lines 31 - 49, The
getSecret function currently accepts any non-empty string but its production
error message promises a 32-character minimum; update getSecret to enforce this
by checking the resolved secret (from the sessionSecret parameter or
process.env['ASGARDEO_SESSION_SECRET']) for a minimum length (e.g., >= 32) when
NODE_ENV === 'production' and throw the existing Error if too short; in
non-production keep the existing dev warning and default secret behavior.
Reference: getSecret and the use of process.env['ASGARDEO_SESSION_SECRET'] /
sessionSecret.

Comment thread packages/nuxt/src/runtime/server/utils/session.ts
Comment on lines +99 to +120
try {
const res: Response = await fetch(tokenEndpoint, {
body,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
method: 'POST',
});

if (!res.ok) {
const errText: string = await res.text().catch(() => String(res.status));
throw new Error(`Token endpoint returned ${res.status}: ${errText}`);
}

refreshed = (await res.json()) as OIDCTokenRefreshResponse;
} catch (err: unknown) {
const msg: string = err instanceof Error ? err.message : String(err);
// eslint-disable-next-line no-console
console.error('[asgardeo] Token refresh failed:', msg);
throw createError({
statusCode: 401,
statusMessage: 'Token refresh failed. Please sign in again.',
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No timeout on the token-endpoint fetch — a hung IdP can pin a Nitro request thread.

fetch here has no AbortSignal, so a slow or unresponsive Asgardeo token endpoint will block this handler (and any caller of getValidAccessToken, including the new /api/auth/token route) until the runtime's default network timeout. Consider an explicit AbortSignal.timeout(...) (e.g., 5–10 s) so refresh failures fail fast and degrade gracefully into the existing 401 path.

🛡️ Proposed fix
-    const res: Response = await fetch(tokenEndpoint, {
-      body,
-      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
-      method: 'POST',
-    });
+    const res: Response = await fetch(tokenEndpoint, {
+      body,
+      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+      method: 'POST',
+      signal: AbortSignal.timeout(10_000),
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` around lines 99 -
120, The fetch to the token endpoint in token-refresh.ts currently has no
timeout and can hang; update the token fetch call (the block that awaits
fetch(tokenEndpoint, { body, headers, method: 'POST' })) to use an AbortSignal
with a short timeout (e.g., 5–10s) — either via AbortSignal.timeout(...) or by
creating an AbortController and setTimeout to call controller.abort(), pass
signal into the fetch options, and ensure the timeout is cleared/cleanup after
the response; preserve the existing catch behavior so aborts/timeouts are
handled the same way (throwing the createError with 401) and include this change
around the same symbol(s) performing the token refresh (the tokenEndpoint fetch
inside the function that populates refreshed / returns an
OIDCTokenRefreshResponse).

Comment on lines +41 to +51
export function createRouteMatcher(patterns: string[]): (path: string) => boolean {
const regexes: RegExp[] = patterns.map((pattern: string) => {
// Escape regex special characters except * and groups in parentheses
const regexStr: string = pattern
.replace(/[.+^${}|[\]\\]/g, '\\$&') // escape regex chars (but not *, ?, ())
.replace(/\*\*/g, '___DOUBLE_STAR___') // placeholder for **
.replace(/\*/g, '[^/]*') // single * matches one segment
.replace(/___DOUBLE_STAR___/g, '.*'); // ** matches everything

return new RegExp(`^${regexStr}$`);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documented regex-alternation feature is broken — | is escaped.

The docstring promises /api/(users|posts) "stays as-is", but the first replace includes | in its escape character class (/[.+^${}|[\]\\]/g), so | is converted to \|. The compiled regex becomes ^/api/(users\|posts)$, which only matches the literal string /api/(users|posts) and not /api/users or /api/posts. Either drop | from the escape set or remove the "Explicit regex groups" claim from the docstring.

🔧 Proposed fix (allow alternation in explicit groups)
-    const regexStr: string = pattern
-      .replace(/[.+^${}|[\]\\]/g, '\\$&') // escape regex chars (but not *, ?, ())
-      .replace(/\*\*/g, '___DOUBLE_STAR___') // placeholder for **
-      .replace(/\*/g, '[^/]*') // single * matches one segment
-      .replace(/___DOUBLE_STAR___/g, '.*'); // ** matches everything
+    const regexStr: string = pattern
+      .replace(/[.+^${}[\]\\]/g, '\\$&') // escape regex chars (but not *, ?, |, ())
+      .replace(/\*\*/g, '___DOUBLE_STAR___') // placeholder for **
+      .replace(/\*/g, '[^/]*') // single * matches one segment
+      .replace(/___DOUBLE_STAR___/g, '.*'); // ** matches everything
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 49-49: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexStr}$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/utils/createRouteMatcher.ts` around lines 41 - 51,
createRouteMatcher currently escapes the alternation pipe via the first replace,
breaking explicit groups like /api/(users|posts); update the escape step in
createRouteMatcher (the patterns -> regexStr construction) to exclude '|' from
the character class passed to .replace (i.e., remove '|' from /[.+^${}|[\]\\]/g)
so alternation inside parentheses remains unescaped and the compiled RegExp
matches either alternative; also update the inline comment near regexStr to
state that '|' inside explicit groups is preserved.

@kavindadimuthu kavindadimuthu force-pushed the nuxt-sdk-start-without-playground branch from b7797c6 to 37adcd9 Compare April 27, 2026 17:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (4)
packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts (1)

43-44: ⚠️ Potential issue | 🟠 Major

Open-redirect bypass: returnTo validation does not block backslash or whitespace-prefixed paths.

/\evil.com passes startsWith('/') && !startsWith('//') but browsers normalize \ to / in the Location path, redirecting to https://evil.com (Saltcorn GHSA-f3g8-9xv5-77gv, Vue Router #3946). Reject backslashes, control characters, whitespace, and other protocol-relative variants. The stored returnTo should also be re-validated where it is consumed in callback.get.ts (line 126) before sendRedirect, since the temp JWT carries it through to the redirect.

🛡️ Suggested change
-  // Validate returnTo is a relative path to prevent open redirect
-  const safeReturnTo = returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : undefined;
+  // Validate returnTo is a same-origin relative path to prevent open redirect.
+  // Reject protocol-relative URLs (//, /\), control chars, and any backslash/whitespace.
+  const isSafeReturnTo = (value: string): boolean =>
+    value.length > 0 &&
+    value.length < 2048 &&
+    value.startsWith('/') &&
+    !value.startsWith('//') &&
+    !value.startsWith('/\\') &&
+    !/[\s\\]/.test(value) &&
+    // eslint-disable-next-line no-control-regex
+    !/[\x00-\x1F\x7F]/.test(value);
+  const safeReturnTo: string | undefined = returnTo && isSafeReturnTo(returnTo) ? returnTo : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts` around
lines 43 - 44, The current returnTo sanitization that sets safeReturnTo (in
signin.get.ts) only checks startsWith('/') and !startsWith('//') and misses
backslashes, whitespace and control characters; update the validation for
safeReturnTo to explicitly reject any returnTo containing backslashes ('\\'),
leading/trailing whitespace, control characters (e.g. charCode <= 32), and
protocol-relative variants (e.g. starting with '//', '\\\\', or other slashes
after trimming), allowing only a strict relative-path pattern (e.g. a single
leading '/' followed by allowed URL path characters); additionally, re-validate
the stored returnTo inside callback.get.ts before calling sendRedirect (where
the temp JWT payload is consumed) to ensure the same strict check is applied at
consumption time to prevent open-redirect bypasses.
packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts (1)

40-46: ⚠️ Potential issue | 🟠 Major

Blanket catch silently signs out users on transient getUser failures.

When verifyAndRehydrateSession returns a non-null session, the cookie is cryptographically valid. Any error from getUser (network blip, IdP 5xx, transient failure) currently degrades the response to {isSignedIn: false, user: null}, falsely signing the user out client-side. This contradicts the SSR pattern in packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts (which keeps isSignedIn: true on getUser failure via Promise.allSettled). Either keep isSignedIn: true with user: null, or rethrow as 5xx so the client can distinguish "authenticated but unable to load user" from "not authenticated".

🛡️ Suggested change
   try {
     const client = AsgardeoNuxtClient.getInstance();
     const user = await client.getUser(session.sessionId);
     return {isLoading: false, isSignedIn: true, user};
-  } catch {
-    return {isLoading: false, isSignedIn: false, user: null};
+  } catch (err) {
+    console.error('[asgardeo] /api/auth/session getUser failed:', err);
+    // Session is valid; only the user lookup failed transiently.
+    return {isLoading: false, isSignedIn: true, user: null};
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts` around
lines 40 - 46, The current blanket catch in session.get.ts silently signs the
user out when AsgardeoNuxtClient.getUser fails; update the catch to preserve
authenticated state by returning {isLoading: false, isSignedIn: true, user:
null} (or rethrow as a 5xx if you prefer the alternate behavior) when
verifyAndRehydrateSession returned a non-null session; specifically, inside the
try/catch around AsgardeoNuxtClient.getInstance() and
client.getUser(session.sessionId) change the catch branch to keep isSignedIn:
true and user: null (and optionally log the error) so failures mirror the
Promise.allSettled approach used by verifyAndRehydrateSession/asgardeo-ssr.ts.
packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts (2)

56-82: ⚠️ Potential issue | 🟠 Major

State (CSRF) is not explicitly validated at the endpoint; original state isn't persisted in the temp JWT.

The endpoint accepts state from the request body and forwards it to client.signIn(...). The temp session JWT carries only sessionId (and optionally returnTo) per packages/nuxt/src/runtime/server/utils/session.ts:92-109 — there's no stored original state to compare against. CSRF/login-fixation protection is therefore entirely delegated to the SDK's internal cache. If that cache is misconfigured (e.g., shared between requests in a serverless/multi-instance deployment) or regresses, this endpoint silently accepts attacker-supplied state.

Persist the generated state into the temp session JWT at sign-in time, return it from verifyTempSessionToken, and assert body.state === tempSession.state here before calling signIn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts` around
lines 56 - 82, The callback endpoint accepts a `state` value but never validates
it against the original CSRF state stored at sign-in; update the temp session
flow to embed the generated state into the temp session JWT at sign-in time (the
code that issues the temp JWT used by getTempSessionCookieName), modify
verifyTempSessionToken to return the stored state alongside sessionId (e.g.,
tempSession.state), and in callback.post.ts before calling
AsgardeoNuxtClient.getInstance().signIn(...) assert that body.state ===
tempSession.state and throw a 400 error if they differ; this ensures
verifyTempSessionToken, getTempSessionCookieName, and signIn are used together
to enforce state validation.

80-93: ⚠️ Potential issue | 🟡 Minor

Error responses inconsistent with callback.get.ts.

callback.get.ts throws createError({statusCode: 500, ...}) on token-exchange and cookie-issuance failures (HTTP 500), while this handler returns HTTP 200 with {success: false, error}. Mixing throw vs. return-200 across two variants of the same logical endpoint complicates client-side error handling and observability (200s won't trip APM/error monitors). Pick one convention; if you keep the JSON-flag form for the XHR-driven POST, at least map issueSessionCookie failures to a 5xx via createError.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts` around
lines 80 - 93, The POST handler currently returns JSON {success:false, error} on
failures while callback.get.ts throws createError — make them consistent by
replacing the two return branches: in the client.signIn try/catch (token
exchange) and in the issueSessionCookie try/catch, throw createError({
statusCode: 500, message: 'Failed to ...', data: { error: err?.message ??
String(err) } }) instead of returning a 200 JSON payload; update the catch
blocks around client.signIn and issueSessionCookie to use createError
(preserving the original error text) so POST errors surface as HTTP 5xx like
callback.get.ts.
🟡 Minor comments (15)
packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts-99-104 (1)

99-104: ⚠️ Potential issue | 🟡 Minor

Avoid echoing raw upstream error text in statusMessage/data.

err?.message may contain internal details (IdP responses, stack fragments) and is surfaced directly to the client through data and indirectly via the templated string in other handlers. Log full server-side, return a sanitized message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts` around
lines 99 - 104, The createError call currently uses err?.message in the response
body and statusMessage which can leak upstream/internal details; instead, log
the full error server-side (e.g., console.error or processLogger.error within
the catch block that surrounds the createError invocation) and replace both data
and statusMessage with a sanitized, generic message (for example "Token exchange
failed" and "An unexpected error occurred") while keeping the statusCode 500;
ensure you do not include err?.message in the returned payload but still retain
it in server logs for debugging.
packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts-66-71 (1)

66-71: ⚠️ Potential issue | 🟡 Minor

Avoid surfacing raw upstream error text in statusMessage.

Same concern as in profile.patch.ts: the upstream error text is reflected back to the client via statusMessage. Log the full error server-side and return a generic message to the caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts` around
lines 66 - 71, The catch block in the branding route currently embeds the raw
upstream error text into the response via createError; instead, log the full
error server-side (use the existing logger or console.error with the caught err)
and change the createError call in the catch to return a generic statusMessage
like "Failed to retrieve branding preference" without including err.message or
String(err); locate the catch that throws createError in branding.get.ts (the
one referencing err and createError) and replace the exposed message with the
generic text while ensuring the full error is recorded via server-side logging.
packages/nuxt/src/runtime/components/auth/Callback.ts-95-113 (1)

95-113: ⚠️ Potential issue | 🟡 Minor

Branch reachability: storedData missing + OAuth error.

When storedData is absent and an OAuth error is present, the handler navigates to a hardcoded /?error=... (line 109) — losing whatever route the user came from. That may be intentional (no original path is recoverable), but the comment doesn't say so. Also the fallback at line 99 (errorDescription || oauthError || 'OAuth authentication failed') has an unreachable third clause because line 98 already gates on oauthError being truthy. Same is true at line 130. Trim or document.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/Callback.ts` around lines 95 - 113,
The code path handling missing storedData uses an unreachable fallback ('OAuth
authentication failed') in the expression errorDescription || oauthError ||
'OAuth authentication failed' (used around the storedData check and again later)
and also navigates to a hardcoded "/?..." losing the user's original route;
update the error construction to simply use errorDescription || oauthError
(remove the third clause) in both places (references: storedData, oauthError,
errorDescription, props.onError) and change the navigation so it preserves the
incoming path instead of hardcoding "/" — e.g. build the redirect URL from the
current location (window.location.pathname + window.location.search) or append
the error params to a preserved return path extracted if available, then call
navigate with that computed URL.
packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts-54-59 (1)

54-59: ⚠️ Potential issue | 🟡 Minor

Avoid leaking raw upstream error messages in statusMessage.

statusMessage is sent to the client and may surface internal/IdP details (stack traces, internal URLs, validation specifics). Log the full error server-side and return a generic message; optionally include a sanitized error code in data if the consumer needs to branch.

♻️ Proposed change
   } catch (err) {
+    console.error('[asgardeo] updateUserProfile failed:', err);
     throw createError({
       statusCode: 500,
-      statusMessage: `Failed to update user profile: ${err instanceof Error ? err.message : String(err)}`,
+      statusMessage: 'Failed to update user profile.',
     });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts` around
lines 54 - 59, The catch block that throws createError using the raw err message
(the err variable) should not leak upstream details to clients; instead, log the
full error server-side (e.g., serverLogger.error or console.error with err) and
change the createError payload so statusMessage is a generic string like "Failed
to update user profile" and, if needed, include a sanitized error code or token
in the data field (e.g., data: { code: 'USER_PROFILE_UPDATE_FAILED' }) so
consumers can branch without exposing internal messages; update the catch around
the profile update where createError is thrown to implement this pattern.
packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts-125-128 (1)

125-128: ⚠️ Potential issue | 🟡 Minor

Defense in depth: re-validate returnTo before redirect.

returnTo was validated when the temp JWT was created in signin.get.ts, but as flagged on that file, the validation there is bypassable (e.g., /\evil.com). Even after that's fixed, treating any value embedded in a signed token as automatically safe at the redirect site is fragile — JWTs only assert origin, not safety. Apply the same isSafeReturnTo validator here before sendRedirect, and fall through to afterSignInUrl when validation fails.

🛡️ Proposed change
-  // Redirect to returnTo (from sign-in request) or configured afterSignInUrl
-  const redirectUrl = returnTo || publicConfig.afterSignInUrl || '/';
+  // Redirect to returnTo (from sign-in request) or configured afterSignInUrl.
+  // Re-validate returnTo as defense-in-depth even though it was validated at sign-in time.
+  const isSafe = (v: string | undefined): v is string =>
+    !!v && v.startsWith('/') && !v.startsWith('//') && !v.startsWith('/\\') && !/[\s\\]/.test(v);
+  const redirectUrl = isSafe(returnTo) ? returnTo : publicConfig.afterSignInUrl || '/';
   return sendRedirect(event, redirectUrl, 302);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts` around
lines 125 - 128, Re-validate the returnTo redirect before sending it: inside the
callback handler where redirectUrl is computed (the returnTo ||
publicConfig.afterSignInUrl || '/' block) call the existing isSafeReturnTo
validator on returnTo and only use returnTo if it passes; otherwise fall back to
publicConfig.afterSignInUrl or '/'; then call sendRedirect(event,
chosenRedirect, 302). Ensure you reference the returnTo variable, isSafeReturnTo
function, publicConfig.afterSignInUrl, and sendRedirect so the check is applied
immediately before redirecting.
packages/nuxt/src/runtime/components/auth/Callback.ts-115-115 (1)

115-115: ⚠️ Potential issue | 🟡 Minor

JSON.parse exceptions surface as error_description query strings.

If sessionStorage is corrupt or written by a different version (storedData not valid JSON, or missing path/timestamp), the outer catch on line 157 will receive a SyntaxError and the raw parser message ("Unexpected token … in JSON at position 0") is written into the URL via error_description. Validate the parsed shape before destructuring, and emit a stable user-facing error code instead of leaking parser internals.

♻️ Proposed change
-        const {path, timestamp} = JSON.parse(storedData) as {path: string; timestamp: number};
-        returnPath = path || '/';
+        let parsed: {path?: string; timestamp?: number};
+        try {
+          parsed = JSON.parse(storedData);
+        } catch {
+          sessionStorage.removeItem(`asgardeo_oauth_${state}`);
+          throw new Error('OAuth state is corrupt - please try again');
+        }
+        if (typeof parsed?.timestamp !== 'number') {
+          sessionStorage.removeItem(`asgardeo_oauth_${state}`);
+          throw new Error('OAuth state is malformed - please try again');
+        }
+        const {path, timestamp} = parsed as {path: string; timestamp: number};
+        returnPath = path || '/';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/Callback.ts` at line 115, The
current JSON.parse of storedData in Callback.ts can throw or produce an object
missing expected fields, causing raw parser messages to leak via
error_description; wrap the parse in a local try/catch and validate the
resulting shape (check that parsed.path is a string and parsed.timestamp is a
number) before destructuring, and on any parse/shape failure throw/return a
stable, user-facing error identifier (e.g. "INVALID_STORED_SESSION" or
"MALFORMED_SESSION_DATA") instead of the raw SyntaxError so the outer catch
logs/redirects with that stable code; look for references to storedData and the
destructuring of {path, timestamp} to update.
packages/nuxt/src/runtime/server/utils/session.ts-114-118 (1)

114-118: ⚠️ Potential issue | 🟡 Minor

verifySessionToken doesn't enforce type === 'session' — asymmetric with verifyTempSessionToken.

createSessionToken stamps type: 'session' (line 80) and verifyTempSessionToken rejects anything not type: 'temp' (lines 130–132), but verifySessionToken accepts any successfully verified JWT signed with the session secret. A temp-session JWT (or any future short-lived token signed with the same key) would pass verification here and be cast to AsgardeoSessionPayload, leaving downstream code to operate on a payload without accessToken/scopes. Add a symmetric type check.

🛡️ Proposed fix
 export async function verifySessionToken(token: string, sessionSecret?: string): Promise<AsgardeoSessionPayload> {
   const secret = getSecret(sessionSecret);
   const {payload} = await jwtVerify(token, secret);
+  if (payload['type'] && payload['type'] !== 'session') {
+    throw new Error(`Invalid token type: expected session, got ${String(payload['type'])}`);
+  }
   return payload as AsgardeoSessionPayload;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/session.ts` around lines 114 - 118,
verifySessionToken currently accepts any JWT signed with the session secret and
should symmetrically enforce payload.type === 'session' like
verifyTempSessionToken does for 'temp'; update verifySessionToken (which calls
getSecret and jwtVerify) to check the verified payload.type and throw an error
(or reject) when it's not 'session' so that only tokens created by
createSessionToken are accepted and you don't cast incompatible payloads to
AsgardeoSessionPayload.
packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts-113-121 (1)

113-121: ⚠️ Potential issue | 🟡 Minor

Validate state is present, not just code.

Line 119 only rejects when code is missing, but a successful OAuth code-flow response is required to include both code and state (state is the CSRF binding). Forwarding state: undefined into client.signIn(...) at line 125 may silently bypass state validation depending on the SDK's behavior.

🛡️ Proposed fix
-    if (!code) {
-      throw createError({statusCode: 502, statusMessage: 'Authorization code missing from completed flow response.'});
+    if (!code || !state) {
+      throw createError({
+        statusCode: 502,
+        statusMessage: 'Authorization code or state missing from completed flow response.',
+      });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts` around
lines 113 - 121, The code only checks for a missing authorization code when
handling EmbeddedSignInFlowStatus.SuccessCompleted but must also validate that
the returned state (CSRF token) is present; update the success branch that reads
response.authData to verify both code and state (authData['code'] and
authData['state']) and throw a createError (same 502 pattern and message) if
either is missing before calling client.signIn(...), ensuring you reference
response, authData, code, state, sessionState and
EmbeddedSignInFlowStatus.SuccessCompleted so state is never forwarded as
undefined to client.signIn.
packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts-268-306 (1)

268-306: ⚠️ Potential issue | 🟡 Minor

Popup closed by user is silently swallowed — no feedback to the form.

When the user closes the OAuth popup before completing the social sign-up (line 270 detects popup.closed), cleanup() runs but no error event is emitted and no flow message is added. From the user's perspective the form just sits idle (loading was already cleared by the parent's finally), with no indication of what happened or how to retry. Consider emitting an error / adding a flow message in the popup.closed && !processed branch so the form can recover.

♻️ Suggested addition
       popupMonitor = setInterval(async () => {
         try {
           if (popup.closed) {
+            if (!processed) {
+              addMessage({
+                message: t('errors.signup.social.popup.closed') || 'Sign-up popup was closed before completion.',
+                type: 'error',
+              });
+              emit('error', new Error('Social sign-up popup closed before completion'));
+              props.onError?.(new Error('Social sign-up popup closed before completion'));
+            }
             cleanup();
             return;
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts` around
lines 268 - 306, When detecting popup.closed inside the popupMonitor interval,
ensure you handle the user-cancel case instead of silently returning: if
popup.closed && !processed then set processed = true, call cleanup(), and
surface an error to the form (for example emit a cancellation/error event or
invoke the component's flow-message helper) so the UI can show a retry message;
also log the cancellation with logger.error (include context like 'User closed
OAuth popup') and clear popupMonitor as currently done to avoid leaks — update
the popup.closed branch near popupMonitor, processed, cleanup, popup.close and
logger usage, and ensure this path mirrors the handled-error path that currently
closes the popup and marks processed.
packages/nuxt/src/runtime/plugins/asgardeo.ts-123-125 (1)

123-125: ⚠️ Potential issue | 🟡 Minor

Client-side isLoading=false without SSR verification causes authenticated users to appear signed-out in SPA or non-SSR deployments.

Line 124 unconditionally sets isLoading to false on the client side regardless of whether SSR hydration actually populated the auth state. In deployments with ssr: false (global SPA mode), hybrid rendering with ssr: false route rules, or nuxt generate (static generation), the SSR hydration block at lines 92–121 will not run, leaving authState at its default {isLoading: true, isSignedIn: false, user: null}. The client then signals "loading complete, no user" without verifying the actual session—so authenticated users with valid session cookies will briefly appear signed-out.

Gate this state transition on a hydration sentinel (e.g., a flag set by SSR in the payload) so unhydrated clients instead attempt a fallback $fetch('/api/auth/session') to resolve the real auth state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/plugins/asgardeo.ts` around lines 123 - 125, The
client-side branch that unconditionally sets authState.value.isLoading = false
(the import.meta.client block referencing authState) causes unhydrated SPA
clients to appear signed-out; change it to check a hydration sentinel that the
SSR hydration block (the code that populates authState during SSR) sets in the
payload, and only flip isLoading to false when that sentinel is present. If the
sentinel is absent, keep isLoading true and perform a fallback fetch to the
session endpoint (e.g., $fetch('/api/auth/session')) to resolve the real
session, then update authState (isSignedIn, user, isLoading) based on that
result. Ensure you update only authState.value via the existing reactive object
(authState) and reuse the same update pattern used in the SSR hydration code.
packages/nuxt/src/runtime/components/AsgardeoRoot.ts-156-163 (1)

156-163: ⚠️ Potential issue | 🟡 Minor

if (res) will swallow legitimate falsy revalidations.

Both revalidateProfile and revalidateBranding only commit the new value when the response is truthy. If the server now returns null (e.g. a tenant clearing its branding, or fetchUserProfile being toggled off in a follow-up call), local state silently stays stale rather than reflecting the new "no data" reality. Mirror what revalidateMyOrganizations / revalidateCurrentOrganization do (assign res ?? <fallback>) so a deliberate null clears local state.

♻️ Proposed change
   const revalidateProfile = async (): Promise<void> => {
     try {
       const res = await $fetch<UserProfile>('/api/auth/user/profile');
-      if (res) userProfileState.value = res;
+      userProfileState.value = res ?? null;
     } catch {
       // Non-fatal — profile stays stale until the next navigation.
     }
   };
@@
   const revalidateBranding = async (): Promise<void> => {
     try {
       const res = await $fetch<BrandingPreference | null>('/api/auth/branding');
-      if (res) brandingState.value = res;
+      brandingState.value = res ?? null;
     } catch {
       // Non-fatal — branding stays stale until the next navigation.
     }
   };

Also applies to: 215-222

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/AsgardeoRoot.ts` around lines 156 - 163,
The current revalidateProfile (and revalidateBranding at 215-222) only updates
state when the fetch result is truthy, which hides legitimate null/empty
responses; change the assignments to always commit the fetched value (using
nullish coalescing) so deliberate nulls clear local state—e.g., in
revalidateProfile set userProfileState.value = res ?? null and likewise set the
branding state to res ?? null (or the appropriate explicit fallback) instead of
only assigning when res is truthy.
packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts-130-133 (1)

130-133: ⚠️ Potential issue | 🟡 Minor

reInitialize discards the legacy client's result and always returns true.

Mirrors the same anti-pattern flagged on initialize: callers can't distinguish a successful reconfiguration from a failed one. Forward the boolean from legacy.reInitialize (and keep isInitialized in sync if a re-init can fail mid-way through, leaving the client in a half-configured state).

🛠️ Proposed fix
-  override async reInitialize(config: Partial<AsgardeoNuxtConfig>): Promise<boolean> {
-    await this.legacy.reInitialize(config as any);
-    return true;
-  }
+  override async reInitialize(config: Partial<AsgardeoNuxtConfig>): Promise<boolean> {
+    const result = await this.legacy.reInitialize(config as any);
+    // Keep isInitialized aligned with the underlying client's state.
+    this.isInitialized = !!result;
+    return !!result;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts` around lines 130 -
133, reInitialize currently awaits legacy.reInitialize but always returns true;
change it to capture and return the boolean result from this. Specifically, in
AsgardeoNuxtClient.reInitialize call const result = await
this.legacy.reInitialize(config as any) and return result (not true); also
ensure this.isInitialized is updated to reflect result (e.g., set
this.isInitialized = result) so the client state stays in sync if
re-initialization fails mid-way. Use the existing method names reInitialize,
legacy.reInitialize, and the isInitialized property to locate and update the
logic.
packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts-183-219 (1)

183-219: ⚠️ Potential issue | 🟡 Minor

Non-submit buttons still fire onSubmit and are not disabled when the form is invalid.

handleClick unconditionally calls onSubmit(component, undefined) for every BUTTON variant, but disabled only blocks the click when cfg.type === 'submit'. So a "Cancel" / "Back" / secondary action button with cfg.type !== 'submit' will (a) trigger the form-submit handler in BaseSignUp and (b) do so even while isFormValid === false. The handleSubmit handler in BaseSignUp.ts confirms this—it submits form inputs without validating form state, unlike the V2 implementation which validates before submission. This likely produces unintended flow-execute payloads for navigation-style buttons.

Either route non-submit buttons through a different handler (so they don't dispatch to onSubmit) or validate form state consistently across all submission entry points.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts`
around lines 183 - 219, handleClick currently always calls onSubmit(component,
undefined) causing non-submit buttons to trigger form submission; change
handleClick to only call onSubmit when cfg.type === 'submit' AND isFormValid is
true (e.g., if (cfg.type === 'submit' && isFormValid) onSubmit(...)), and for
non-submit buttons invoke the appropriate navigation/no-op handler instead of
onSubmit; also tighten the Button props so disabled becomes isLoading ||
!isFormValid (or at minimum include !isFormValid regardless of cfg.type) to
prevent clicks when the form is invalid. Update references:
SignUpOptionFactory.handleClick, cfg.type, Button prop disabled, and the
onSubmit call site.
packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts-39-47 (1)

39-47: ⚠️ Potential issue | 🟡 Minor

getInputName priority contradicts its JSDoc.

The implementation returns cfg.name || cfg.identifier || component.id, but the JSDoc states "V1 stores the bound parameter name in config.identifier … with config.name used as a fallback" — the reverse order. This matters: if config.identifier contains the authoritative bound-claim reference (e.g. http://wso2.org/claims/emailaddress) and config.name is only a UI label, reversing the priority will silently wire form values to the wrong claim key for components with both properties. Either align the code to the documented priority, or update the JSDoc to reflect the current name-first behavior.

♻️ One of these is wrong — pick the source of truth
 const getInputName = (component: any): string => {
   const cfg: any = component.config || {};
-  return (cfg.name as string) || (cfg.identifier as string) || (component.id as string);
+  return (cfg.identifier as string) || (cfg.name as string) || (component.id as string);
 };

…or update the JSDoc to match the current name-first behaviour if Asgardeo V1's contract truly treats name as canonical.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts`
around lines 39 - 47, The getInputName implementation contradicts its JSDoc:
change the selection priority so config.identifier is used first, then
config.name, then component.id (i.e., return cfg.identifier || cfg.name ||
component.id), and ensure you reference the same symbols (getInputName,
component.config / cfg, cfg.identifier, cfg.name, component.id) so the behavior
matches the documented Asgardeo V1 contract; alternatively, if the project
decision is to keep name-first, update the JSDoc to state that cfg.name is
authoritative and list cfg.identifier as the fallback.
packages/nuxt/src/runtime/components/AsgardeoRoot.ts-99-99 (1)

99-99: ⚠️ Potential issue | 🟡 Minor

Remove the as any cast — ThemeProvider fully supports all AsgardeoNuxtConfig.preferences.theme.mode values.

AsgardeoNuxtConfig.preferences.theme.mode declares 'light' | 'dark' | 'system' | 'class' | 'branding', and ThemeProvider accepts exactly that (as ThemeMode | 'branding' where ThemeMode = 'light' | 'dark' | 'system' | 'class'). All five modes are explicitly handled in ThemeProvider.setup() and detectThemeMode(). The cast is unnecessary and can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/AsgardeoRoot.ts` at line 99, Remove the
unnecessary "as any" cast when deriving themeMode: change the declaration that
sets themeMode from prefs?.theme?.mode (currently casted) to use the native type
directly (e.g., const themeMode = prefs?.theme?.mode ?? 'light') since
AsgardeoNuxtConfig.preferences.theme.mode already matches ThemeProvider's
accepted values; update the variable in AsgardeoRoot.ts so ThemeProvider.setup()
and detectThemeMode() receive the correctly typed value without the cast.
🧹 Nitpick comments (10)
packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts (1)

39-44: Preserve the underlying error for observability.

The bare catch discards the original error from client.getUser, so an upstream token-refresh / SCIM2 / network failure surfaces as a context-free 500. Capture the cause (h3 supports cause on createError) and/or log it server-side; you don't have to leak it to the client.

♻️ Proposed change
-  try {
-    const client = AsgardeoNuxtClient.getInstance();
-    return await client.getUser(session.sessionId);
-  } catch {
-    throw createError({statusCode: 500, statusMessage: 'Failed to retrieve user information.'});
-  }
+  try {
+    const client = AsgardeoNuxtClient.getInstance();
+    return await client.getUser(session.sessionId);
+  } catch (err) {
+    throw createError({
+      statusCode: 500,
+      statusMessage: 'Failed to retrieve user information.',
+      cause: err,
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts` around lines
39 - 44, The catch currently swallows errors from
AsgardeoNuxtClient.getInstance()/client.getUser(session.sessionId); modify the
catch to capture the original error (e.g., `err`) and pass it as the cause to
createError and/or log it server-side before throwing so observability is
preserved; update the handler around client.getUser to use the caught error as
the cause in createError and optionally call the server logger with err and a
contextual message.
packages/nuxt/src/runtime/errors/error-codes.ts (1)

39-49: Move OAuth token codes back under the OAuth group.

TokenExchangeFailed and TokenRefreshFailed carry the oauth/ prefix but appear under the Session group without a header. Either lift them under // ── OAuth ── (line 30) or add a // ── Token ── header so the comment grouping matches the value prefixes. Purely cosmetic.

♻️ Proposed regrouping
   // ── OAuth ──────────────────────────────────────────────────────────
   OAuthCallbackError = 'oauth/callback-error',
   OAuthStateInvalid = 'oauth/state-invalid',
+  TokenExchangeFailed = 'oauth/token-exchange-failed',
+  TokenRefreshFailed = 'oauth/token-refresh-failed',
   // ── Security ───────────────────────────────────────────────────────
   OpenRedirectBlocked = 'security/open-redirect-blocked',

   // ── Organization ───────────────────────────────────────────────────
   OrganizationCreateFailed = 'organization/create-failed',
   OrganizationSwitchFailed = 'organization/switch-failed',
   // ── Session ────────────────────────────────────────────────────────
   SessionExpired = 'session/expired',
   SessionInvalid = 'session/invalid',
   SessionMissing = 'session/missing',
-
   TempSessionInvalid = 'session/temp-invalid',
-  TokenExchangeFailed = 'oauth/token-exchange-failed',
-  TokenRefreshFailed = 'oauth/token-refresh-failed',
   // ── SCIM2 ──────────────────────────────────────────────────────────
   UserProfileFetchFailed = 'scim2/user-profile-fetch-failed',
   UserProfileUpdateFailed = 'scim2/user-profile-update-failed',
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/errors/error-codes.ts` around lines 39 - 49, The
OAuth-prefixed error codes TokenExchangeFailed and TokenRefreshFailed are placed
under the Session block; move these constants (TokenExchangeFailed,
TokenRefreshFailed) so they sit under the existing OAuth group header (or add a
new "// ── Token ──" header) to align the comment grouping with their 'oauth/'
prefixes—update the block order in error-codes.ts so the Token* symbols appear
directly beneath the OAuth header.
packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts (2)

37-43: Fix the upstream FlattenedSchema type instead of casting through unknown.

Casting schemas (typed Schema[]) to FlatSchemaEntry[] via unknown is a type-safety hole, and the comment already calls out the root cause: the published FlattenedSchema extends Schema instead of SchemaAttribute. Worth fixing in @asgardeo/browser so the prop can be typed as FlattenedSchema[] (or (SchemaAttribute & {schemaId: string})[]) here, dropping both the cast and the local FlatSchemaEntry workaround. The current state is fine to ship, but please file a follow-up so this doesn't become permanent.

Also applies to: 117-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts`
around lines 37 - 43, The local FlatSchemaEntry type and the unsafe cast of
schemas through unknown is a temporary workaround for an upstream type bug;
instead, update the upstream `@asgardeo/browser` FlattenedSchema type to extend
SchemaAttribute (or export a proper type equivalent to SchemaAttribute &
{schemaId: string}), then remove the local FlatSchemaEntry declaration and the
cast of schemas (currently cast via unknown) so the prop/variable can be typed
directly as FlattenedSchema[] (or (SchemaAttribute & {schemaId: string})[]);
also open a follow-up issue/PR in `@asgardeo/browser` referencing
flattenUserSchema and the incorrect FlattenedSchema inheritance so this fix is
permanent.

144-161: Remove addresses, entitlements, roles, x509Certificates from complexMultiValued set, or drive from schema.

The set incorrectly assumes all entries use the {type, value} sub-attribute structure. Only phoneNumbers, emails, ims, and photos have this shape. For addresses, the SCIM2 core schema defines sub-attributes as streetAddress, locality, region, postalCode, country, formatted, type, primary — emitting {addresses: [{type: 'work', value: rawValue}]} will be silently dropped or rejected by the server.

Currently, no flat keys for these problematic attributes are generated in the codebase (search confirms none exist). However, if support is added later (e.g., via schema configuration or UI extension), the function will silently produce invalid PATCH payloads.

Two approaches:

  • Short term: Remove these four attributes from the set so they fall through to setNestedPath and produce at least syntactically scoped objects.
  • Longer term: Drive this from the schema (detect multiValued && type === 'COMPLEX' from the matching FlatSchemaEntry) so custom extension attributes also work correctly, instead of a hardcoded SCIM-core list.

Note: phoneNumbers.mobile is already special-cased separately, so changes won't affect it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts`
around lines 144 - 161, The complexMultiValued Set in BaseUserProfile.ts
incorrectly includes attributes that are complex multi-valued (addresses,
entitlements, roles, x509Certificates) but do not use the {type,value} shape;
update the logic by removing those four strings from the complexMultiValued Set
so only 'phoneNumbers', 'emails', 'ims', and 'photos' are treated as
{type,value} and allow other multi-valued complex attributes to fall through to
setNestedPath; alternatively (preferred longer-term) replace the hardcoded Set
with schema-driven detection by looking up the matching FlatSchemaEntry for
flatKey and testing flatSchemaEntry.multiValued && flatSchemaEntry.type ===
'COMPLEX' before constructing {[head]: [{type: tail, value: rawValue}]}.
packages/vue/src/components/presentation/sign-up/v1/SignUp.ts (1)

84-90: Handle the redirect type more strictly than as any.

(response as any)?.redirectUrl works but defeats the type system. If the SDK already exposes a Redirection response variant via EmbeddedFlowResponseType.Redirection (used at line 94), narrow on response.type === EmbeddedFlowResponseType.Redirection and read the typed redirectUrl from that branch instead. Also note: when oauthRedirectUrl is truthy but shouldRedirectAfterSignUp is false, the early return on line 89 is skipped, but the second branch at lines 92–100 still requires response.type !== Redirection — so that's fine; just clean up the any.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v1/SignUp.ts` around lines
84 - 90, Replace the unsafe cast "(response as any)?.redirectUrl" with a proper
type-narrowing branch: check if response.type ===
EmbeddedFlowResponseType.Redirection and then read redirectUrl from that typed
response object (e.g., the Redirection variant) before using it for the redirect
logic tied to props.shouldRedirectAfterSignUp; remove the "as any" cast and keep
the same early-return behavior when shouldRedirectAfterSignUp is true, leaving
the other branches unchanged.
packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts (1)

46-65: Minor: redundant getInstance() call and reusable client.

AsgardeoNuxtClient.getInstance() is invoked once inside the try (line 52, only when session.organizationId is falsy) and again at line 64 unconditionally. Hoisting the client to a single const at the top of the handler is cleaner and avoids the implicit assumption that getInstance() is cheap/idempotent.

♻️ Proposed change
   const baseUrl: string = (publicConfig?.baseUrl ?? '') as string;
   let resolvedBaseUrl: string = baseUrl;
+  const client = AsgardeoNuxtClient.getInstance();

   // Attempt to resolve the org-scoped base URL from the session, if present.
   try {
     const session = await verifyAndRehydrateSession(event, sessionSecret);
     if (session) {
       if (session.organizationId) {
         resolvedBaseUrl = `${baseUrl}/o`;
       } else {
-        const client = AsgardeoNuxtClient.getInstance();
         const idToken = await client.getDecodedIdToken(session.sessionId);
         if (idToken?.['user_org']) {
           resolvedBaseUrl = `${baseUrl}/o`;
         }
       }
     }
   } catch {
     // Non-fatal — fall back to the root tenant base URL
   }

   try {
-    const client = AsgardeoNuxtClient.getInstance();
     return await client.getBrandingPreference({baseUrl: resolvedBaseUrl});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts` around
lines 46 - 65, Hoist a single AsgardeoNuxtClient instance so getInstance() is
called once: create const client = AsgardeoNuxtClient.getInstance() before the
try that calls verifyAndRehydrateSession, then use that client for
client.getDecodedIdToken(...) inside the session branch and for
client.getBrandingPreference({baseUrl: resolvedBaseUrl}) at the end; keep
verifyAndRehydrateSession and resolvedBaseUrl logic unchanged but remove the
second getInstance() call to avoid redundant instantiation.
packages/nuxt/src/runtime/components/auth/Callback.ts (1)

103-167: Optional: extract the repeated URLSearchParams construction.

The error / error_description query-string assembly is repeated four times (lines 103-107, 134-138, 150-154, 163-165). A small helper (buildQuery({error, error_description, code, nonce})) would eliminate duplication and ensure consistent encoding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/Callback.ts` around lines 103 -
167, Extract a small helper (e.g., buildQueryParams or buildQueryString) in the
Callback component that takes an object like { error?, error_description?,
code?, nonce? } and returns a URLSearchParams or query string; replace each
repeated URLSearchParams construction (the four spots that set
'error'/'error_description' and the spot that sets 'code'/'nonce', plus the
final callback_error construction) to call that helper and use its result in
navigate calls; ensure the helper only adds keys with defined values and
preserves encoding so all navigate(`${returnPath}?${...}`) usages remain
correct, and update any callers to expect the helper's return type (string or
URLSearchParams) consistently.
packages/nuxt/src/runtime/composables/useAsgardeo.ts (1)

144-174: Sign-up redirect resolution duplicates plugins/asgardeo.ts.

Lines 145–174 here are nearly identical to the redirect-flow signUp in packages/nuxt/src/runtime/plugins/asgardeo.ts (lines 154–175): same signUpUrl override → getRedirectBasedSignUpUrl → fallback chain, just with different fallback targets (/sign-up vs. /api/auth/signup). Consider extracting a shared helper (e.g., resolveRedirectSignUpUrl(publicConfig)) to avoid drift between the two implementations as redirect rules evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/composables/useAsgardeo.ts` around lines 144 - 174,
The sign-up redirect logic in the signUp handler duplicates the same flow in
plugins/asgardeo.ts (override via signUpUrl → getRedirectBasedSignUpUrl →
fallback), which risks drift; extract that shared logic into a single helper
(e.g., resolveRedirectSignUpUrl) that accepts the public asgardeo config
(applicationId, baseUrl, clientId, optional signUpUrl) and returns the final
redirect URL or null, then call this helper from both signUp implementations
(the composable and the plugin) and use their respective fallback targets
(/sign-up vs /api/auth/signup) when the helper returns null; update references
to getRedirectBasedSignUpUrl to use the new helper and ensure function names
signUp and resolveRedirectSignUpUrl are used consistently.
packages/nuxt/src/runtime/types.ts (1)

91-115: Consider adding a literal type: 'session' discriminator to AsgardeoSessionPayload.

createSessionToken (in packages/nuxt/src/runtime/server/utils/session.ts, context snippet 1) embeds type: 'session' into the signed JWT body, and AsgardeoTempSessionPayload already declares type: 'temp'. Mirroring that on AsgardeoSessionPayload would let consumers discriminate between the two payload shapes via a tagged union and prevent an accidental temp-cookie value from satisfying the session-payload type.

♻️ Proposed refinement
 export interface AsgardeoSessionPayload extends JWTPayload {
   accessToken: string;
   /** Unix timestamp (seconds) when the access token expires. Used for proactive refresh. */
   accessTokenExpiresAt?: number;
   exp: number;
   iat: number;
   /** Raw ID token string (for userinfo derivation without in-memory store). */
   idToken?: string;
   organizationId?: string;
   /** Refresh token for obtaining new access tokens without re-authentication. */
   refreshToken?: string;
   scopes: string;
   sessionId: string;
   sub: string;
+  type: 'session';
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/types.ts` around lines 91 - 115, Add a literal
discriminator to the AsgardeoSessionPayload interface so it explicitly contains
type: 'session' (mirroring AsgardeoTempSessionPayload) to enable tagged-union
discrimination; update the AsgardeoSessionPayload declaration to include type:
'session' and ensure any code that constructs session JWTs (e.g.,
createSessionToken) continues to embed that literal so TypeScript and runtime
payloads remain consistent.
packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts (1)

203-241: signIn(...args: any[]) overload dispatch is fragile.

Routing four different call shapes through positional args[N] reads — and reordering them on the legacy fall-through (signIn(args[3], args[2], arg0?.code, …) vs signIn(args[0], args[1], args[2], …)) — is bug-prone and impossible for TypeScript to validate. Consider splitting this into an explicit overload signature set (matching the four documented modes) so each call shape is type-checked independently. Mirrors the same concern that has accumulated in AsgardeoNextClient; if the Next.js client gets refactored upstream, keep them in lockstep.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts` around lines 203 -
241, The signIn(...args: any[]) method (AsgardeoNuxtClient.signIn) is fragile
because it dispatches four call shapes by indexing args[N]; replace this with
explicit TypeScript overload signatures for each documented mode (embedded-init,
embedded-execute, code-exchange, redirect) and implement a type-safe dispatcher
that narrows by the overloads or a discriminated union instead of positional
indexing; update the implementation to accept strongly typed parameters for
EmbeddedSignInFlowHandleRequestPayload / EmbeddedFlowExecuteRequestConfig and to
call getAuthorizeRequestUrl, initializeEmbeddedSignInFlow,
executeEmbeddedSignInFlow, or this.legacy.signIn with correctly-mapped named
arguments (not args[...]) so TypeScript can validate shapes and keep behavior
aligned with the AsgardeoNextClient counterpart.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cc7685a-9692-47bc-a6f6-c97945c8e6c5

📥 Commits

Reviewing files that changed from the base of the PR and between b7797c6 and 37adcd9.

📒 Files selected for processing (52)
  • packages/nuxt/src/index.ts
  • packages/nuxt/src/module.ts
  • packages/nuxt/src/runtime/components/AsgardeoRoot.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
  • packages/nuxt/src/runtime/components/actions/SignOutButton.ts
  • packages/nuxt/src/runtime/components/actions/SignUpButton.ts
  • packages/nuxt/src/runtime/components/auth/Callback.ts
  • packages/nuxt/src/runtime/components/auth/SignIn.ts
  • packages/nuxt/src/runtime/components/auth/SignUp.ts
  • packages/nuxt/src/runtime/components/control/Loading.ts
  • packages/nuxt/src/runtime/components/control/SignedIn.ts
  • packages/nuxt/src/runtime/components/control/SignedOut.ts
  • packages/nuxt/src/runtime/components/organization/CreateOrganization.ts
  • packages/nuxt/src/runtime/components/organization/Organization.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationList.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts
  • packages/nuxt/src/runtime/components/user/User.ts
  • packages/nuxt/src/runtime/components/user/UserDropdown.ts
  • packages/nuxt/src/runtime/components/user/UserProfile.ts
  • packages/nuxt/src/runtime/composables/useAsgardeo.ts
  • packages/nuxt/src/runtime/errors/asgardeo-error.ts
  • packages/nuxt/src/runtime/errors/error-codes.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/src/runtime/plugins/asgardeo.ts
  • packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts
  • packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts
  • packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts
  • packages/nuxt/src/runtime/server/utils/event-context.ts
  • packages/nuxt/src/runtime/server/utils/serverSession.ts
  • packages/nuxt/src/runtime/server/utils/session.ts
  • packages/nuxt/src/runtime/server/utils/token-refresh.ts
  • packages/nuxt/src/runtime/types.ts
  • packages/nuxt/src/runtime/types/augments.d.ts
  • packages/nuxt/src/runtime/utils/createRouteMatcher.ts
  • packages/nuxt/src/runtime/utils/log.ts
  • packages/nuxt/src/runtime/utils/url-validation.ts
  • packages/vue/src/components/presentation/sign-up/v1/BaseSignUp.ts
  • packages/vue/src/components/presentation/sign-up/v1/SignUp.ts
  • packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts
  • packages/vue/src/components/presentation/user-profile/BaseUserProfile.ts
  • packages/vue/src/plugins/AsgardeoPlugin.ts
✅ Files skipped from review due to trivial changes (8)
  • packages/nuxt/src/runtime/components/organization/Organization.ts
  • packages/nuxt/src/runtime/components/control/SignedOut.ts
  • packages/nuxt/src/runtime/components/user/UserProfile.ts
  • packages/nuxt/src/runtime/components/actions/SignOutButton.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/src/runtime/components/actions/SignUpButton.ts
  • packages/nuxt/src/runtime/types/augments.d.ts
  • packages/nuxt/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (15)
  • packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationList.ts
  • packages/nuxt/src/runtime/components/control/SignedIn.ts
  • packages/nuxt/src/runtime/components/user/UserDropdown.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts
  • packages/nuxt/src/runtime/components/organization/CreateOrganization.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts
  • packages/nuxt/src/runtime/utils/log.ts
  • packages/nuxt/src/runtime/server/utils/serverSession.ts
  • packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts
  • packages/vue/src/plugins/AsgardeoPlugin.ts
  • packages/nuxt/src/runtime/errors/asgardeo-error.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
  • packages/nuxt/src/runtime/components/auth/SignIn.ts
  • packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts

Comment on lines +63 to +70
const navigate = (path: string): void => {
if (props.onNavigate) {
props.onNavigate(path);
} else {
// navigateTo handles both SSR (sets redirect response) and client (pushState).
navigateTo(path);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What does Nuxt 3 navigateTo return and how should callers handle NavigationFailure?

💡 Result:

Nuxt 3's navigateTo function returns Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw. Callers should always await or return the result of navigateTo, especially in middleware, plugins, or server-side contexts, to ensure proper navigation flow and avoid unexpected behavior. For example: export default defineNuxtRouteMiddleware((to, from) => { if (to.path !== '/search') { return navigateTo('/search', { redirectCode: 301 }) } }) To handle NavigationFailure (a Vue Router error type for cases like aborted navigation, duplicates, redirects, or cancellations), catch the promise rejection: const result = await navigateTo('/somewhere').catch(err => { if (isNavigationFailure(err, NavigationFailureType.aborted)) { // Handle specific failure, e.g., show message } }) NavigationFailure instances include 'to' and 'from' properties reflecting the attempted navigation. In middleware, returning navigateTo propagates the result correctly without needing explicit catch in many cases.

Citations:


🏁 Script executed:

cd packages/nuxt && find . -name "Callback.ts" -type f 2>/dev/null | head -5

Repository: asgardeo/javascript

Length of output: 105


🏁 Script executed:

cat -n src/runtime/components/auth/Callback.ts | sed -n '60,180p'

Repository: asgardeo/javascript

Length of output: 135


🏁 Script executed:

cat -n packages/nuxt/src/runtime/components/auth/Callback.ts | sed -n '60,180p'

Repository: asgardeo/javascript

Length of output: 5061


Await navigateTo and handle NavigationFailure to ensure error callbacks are invoked.

navigateTo returns a Promise that may reject with a NavigationFailure (aborted guards, route redirects, cancellations). The current implementation discards the promise, so navigation failures won't trigger props.onError, breaking the component's error handling contract and preventing caller visibility into navigation issues.

Recommended fix:

const navigate = async (path: string): Promise<void> => {
  if (props.onNavigate) {
    props.onNavigate(path);
  } else {
    try {
      await navigateTo(path);
    } catch (err) {
      props.onError?.(err instanceof Error ? err : new Error(String(err)));
    }
  }
};

Update the three navigate() calls (lines 109, 140, 156) to await navigate(...) accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/auth/Callback.ts` around lines 63 - 70,
The navigate helper currently calls navigateTo without awaiting or handling
rejections, so navigation failures never trigger props.onError; change the
navigate function (the const navigate = (path: string) => { ... }
implementation) to be async, await navigateTo when props.onNavigate is not
provided, and catch errors to call props.onError (coerce non-Error to Error via
String if needed). Also update all call sites that invoke navigate(...) (the
three places that currently call navigate at the old lines) to await
navigate(...) so callers propagate/handle promise rejections correctly.

Comment on lines +345 to +359
override async getCurrentOrganization(sessionId: string): Promise<Organization | null> {
try {
const idToken: IdToken = await this.getDecodedIdToken(sessionId);
if (!idToken?.org_id) {
return null;
}
return {
id: idToken.org_id as string,
name: (idToken.org_name ?? '') as string,
orgHandle: (idToken.org_handle ?? '') as string,
};
} catch {
return null;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. See whether anywhere else in the SDK reads the same claim names.
rg -nP --type=ts -C2 '\bidToken(\?\.|\[)\s*[\x27"]?(org_id|org_name|org_handle|user_org|organization_id|organization_name)' packages

# 2. Cross-check the Next.js SDK's getCurrentOrganization implementation, which this is supposed to mirror.
fd -e ts getCurrentOrganization packages/nextjs --exec sh -c 'echo "=== {} ==="; cat {}'
fd -e ts AsgardeoNextClient packages/nextjs --exec sh -c 'echo "=== {} ==="; cat {}' | rg -nP -A6 'getCurrentOrganization'

Repository: asgardeo/javascript

Length of output: 10553


🌐 Web query:

What organization-related claims does an Asgardeo ID token contain when a user is acting within an organization? Specifically, does it use user_org, org_id, org_name, or org_handle?

💡 Result:

Asgardeo ID tokens do not document any organization-related claims named user_org, org_id, org_name, or org_handle. In the current Asgardeo docs/SDK model for the ID token payload, the exposed claims are things like sub, email, preferred_username, and tenant_domain (tenant/domain of the user). No claim names for organization context (user_org, org_id, org_name, org_handle) are listed as ID-token claims when acting within an organization. So, based on the available authoritative Asgardeo documentation/SDK payload definitions, none of those user_org/org_*/org_handle claims are claimed as part of the Asgardeo ID token organization context.

Citations:


🏁 Script executed:

# Verify the scope of the discrepancy across all SDKs and check for any documentation or configuration
rg -nP --type=ts "user_org|organization_id" packages --count | head -20
rg -nP --type=ts "org_id|org_name|org_handle" packages --count | head -20

Repository: asgardeo/javascript

Length of output: 1055


🏁 Script executed:

# Get the detailed breakdown and check if there's any documentation or mapping file that explains these claims
rg -nP --type=ts "org_id|org_name|org_handle" packages --count | head -20
rg -rn "org_id|org_name|org_handle|user_org|organization_id" packages --type=ts -l | wc -l
fd -e md -e txt . | xargs rg -l "org_id|org_name|org_handle|user_org|organization_id" 2>/dev/null | head -10

Repository: asgardeo/javascript

Length of output: 504


🏁 Script executed:

# Check the README files to see if they document these claims
rg -A 5 -B 5 "org_id|user_org|organization_id" packages/nuxt/README.md packages/browser/README.md 2>/dev/null | head -50

Repository: asgardeo/javascript

Length of output: 1755


🏁 Script executed:

# Look at more context around user_org in the README and check for any JSDoc or comments explaining the claim mappings
rg -A 10 -B 5 "user_org" packages/nuxt/README.md packages/nuxt/src/runtime/types.ts 2>/dev/null
cat packages/nuxt/src/runtime/types.ts | head -100

Repository: asgardeo/javascript

Length of output: 6779


Claim name inconsistency across all client SDKs blocks organization context detection.

getCurrentOrganization in all client SDKs (React, Vue, Nuxt, Next.js) extracts idToken.org_id / org_name / org_handle, but server-side code (issueSessionCookie, switchOrganization, signInAction, etc.) reads idToken['user_org'] || idToken['organization_id'].

This discrepancy is confirmed across the codebase and internal documentation contradicts itself: AsgardeoSSRData.resolvedBaseUrl's JSDoc explicitly states it derives from the user_org claim, while the Nuxt README says getCurrentOrganization derives from the org_id claim. However, official Asgardeo documentation lists none of these claims (user_org, org_id, org_name, org_handle, organization_id) as standard ID token claims.

The mismatch causes getCurrentOrganization to silently return null for every signed-in user in an organization, breaking the organization switcher UI across all affected SDKs.

Standardize on a single claim name across the entire SDK. Either:

  • Switch all client implementations to read user_org (matching server-side and JSDoc), or
  • Switch all server-side reads to org_id / org_name / org_handle (matching client implementations)

Additionally, verify against actual Asgardeo ID tokens and document the correct claim name(s) in all SDKs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts` around lines 345 -
359, getCurrentOrganization currently reads idToken.org_id/org_name/org_handle
while server-side functions (issueSessionCookie, switchOrganization,
signInAction) and AsgardeoSSRData.resolvedBaseUrl JSDoc use
user_org/organization_id, causing organization detection to fail; pick one
canonical claim and make all code paths consistent: either update
getCurrentOrganization in AsgardeoNuxtClient to read idToken['user_org'] (and
corresponding name/handle claims if present) or update server-side readers
(issueSessionCookie, switchOrganization, signInAction) and JSDoc to use
org_id/org_name/org_handle, and then audit other clients (React/Vue/Next) to
match and update SDK docs to state the exact claim names verified against real
Asgardeo ID tokens.

Comment on lines +106 to +111
if (!tokenResponse?.accessToken && !tokenResponse?.idToken) {
throw createError({
statusCode: 500,
statusMessage: 'Token exchange failed: Invalid response from Identity Provider.',
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm AsgardeoNuxtClient.getAccessToken behaviour and whether downstream calls require accessToken.
rg -nP -C5 '\bgetAccessToken\s*\(' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts
rg -nP -C3 'issueSessionCookie' packages/nuxt/src/runtime/server/utils/session.ts

Repository: asgardeo/javascript

Length of output: 4324


🏁 Script executed:

#!/bin/bash
# Examine issueSessionCookie implementation to understand token storage
cat -n packages/nuxt/src/runtime/server/utils/session.ts | head -250

Repository: asgardeo/javascript

Length of output: 8210


🏁 Script executed:

#!/bin/bash
# Find the legacy client implementation to understand getAccessToken behavior
fd -t f 'AsgardeoNodeClient\|auth-js-sdk' packages/nuxt/src/runtime/server --maxdepth 5
rg -n 'class.*Client|extends.*Client' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts | head -20
rg -n 'this\.legacy\s*=' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts

Repository: asgardeo/javascript

Length of output: 213


🏁 Script executed:

#!/bin/bash
# Find the legacy client and getAccessToken implementation
rg -n 'class LegacyAsgardeoNodeClient|getAccessToken' node_modules/@asgardeo/node/dist -A 3 2>/dev/null || rg -n 'export.*getAccessToken|function getAccessToken' packages --maxdepth 10 -B 2 -A 8
# Also check what the legacy client type expects
rg -n 'import.*LegacyAsgardeoNodeClient' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts

Repository: asgardeo/javascript

Length of output: 266


🏁 Script executed:

#!/bin/bash
# Check the imports to understand where LegacyAsgardeoNodeClient comes from
head -100 packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts | grep -E 'import|from'

Repository: asgardeo/javascript

Length of output: 328


🏁 Script executed:

#!/bin/bash
# Get full imports from `@asgardeo/node`
sed -n '1,50p' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts

Repository: asgardeo/javascript

Length of output: 1527


🏁 Script executed:

#!/bin/bash
# Check the full callback.get.ts file to understand context and error handling
cat -n packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts | head -150

Repository: asgardeo/javascript

Length of output: 5215


🏁 Script executed:

#!/bin/bash
# Verify what getDecodedIdToken does when idToken is missing/undefined
rg -n 'getDecodedIdToken' packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts -B 2 -A 10

Repository: asgardeo/javascript

Length of output: 2002


Require accessToken in the token validation check.

The current check (!accessToken && !idToken) allows session creation when only idToken is present. However, all downstream server-side operations—getUserProfile, updateUserProfile, getMyOrganizations, getAllOrganizations, createOrganization, getOrganization—invoke getAccessToken() and use it for Bearer token authorization in API calls. If accessToken is missing from the token response, the session would be created with an undefined token, causing every protected API call to fail.

For a redirect-based authorization-code flow, the absence of accessToken indicates an IdP error and should fail immediately rather than create a broken session. Change the check to require accessToken explicitly:

if (!tokenResponse?.accessToken) {
  throw createError({
    statusCode: 500,
    statusMessage: 'Token exchange failed: Access token missing from Identity Provider response.',
  });
}

The idToken is separately validated in issueSessionCookie (line 198 calls getDecodedIdToken).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts` around
lines 106 - 111, The token validation currently allows sessions when only
tokenResponse.idToken exists; update the conditional in the token exchange block
to require tokenResponse.accessToken (i.e. change the check to throw if
!tokenResponse?.accessToken) and adjust the createError statusMessage to "Token
exchange failed: Access token missing from Identity Provider response."; keep
idToken validation as-is since issueSessionCookie calls getDecodedIdToken for
separate ID token checks.

Comment on lines +43 to +52
let payload: UpdateMeProfileConfig;
try {
payload = await readBody<UpdateMeProfileConfig>(event);
} catch {
throw createError({statusCode: 400, statusMessage: 'Invalid request body.'});
}

try {
const client = AsgardeoNuxtClient.getInstance();
const user: User = await client.updateUserProfile(payload, session.sessionId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: SSRF and header-injection risk via untrusted request body.

The endpoint trusts the entire request body as UpdateMeProfileConfig, then forwards it into client.updateUserProfile(payload, session.sessionId). Per the relevant snippet from packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts:388-398, the config is spread into updateMeProfile({...config, baseUrl, headers: {...config.headers, Authorization: 'Bearer ...'}}), and per packages/javascript/src/api/updateMeProfile.ts:68-110 the resolved URL is url ?? baseUrl/scim2/Me.

Consequences:

  • A malicious caller can set url in the body to an attacker-controlled origin, causing the server to send the user's Bearer <accessToken> to that URL — token exfiltration / SSRF.
  • A caller can also inject headers (and fetcher is even typed as a function — though not JSON-serializable, it shows the contract is leaky), bypassing intended request shaping.

Only accept the SCIM patch value (payload) from the request body and construct the call internally; never let the client control url, headers, fetcher, or other RequestInit fields.

🛡️ Proposed fix
-  let payload: UpdateMeProfileConfig;
+  let payload: UpdateMeProfileConfig['payload'];
   try {
-    payload = await readBody<UpdateMeProfileConfig>(event);
+    const body = await readBody<{payload?: UpdateMeProfileConfig['payload']}>(event);
+    // Accept either { payload: {...} } or the raw SCIM patch value object.
+    payload = body && typeof body === 'object' && 'payload' in body ? body.payload : (body as any);
+    if (!payload || typeof payload !== 'object') {
+      throw new Error('payload is required');
+    }
   } catch {
     throw createError({statusCode: 400, statusMessage: 'Invalid request body.'});
   }

   try {
     const client = AsgardeoNuxtClient.getInstance();
-    const user: User = await client.updateUserProfile(payload, session.sessionId);
+    const user: User = await client.updateUserProfile({payload}, session.sessionId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let payload: UpdateMeProfileConfig;
try {
payload = await readBody<UpdateMeProfileConfig>(event);
} catch {
throw createError({statusCode: 400, statusMessage: 'Invalid request body.'});
}
try {
const client = AsgardeoNuxtClient.getInstance();
const user: User = await client.updateUserProfile(payload, session.sessionId);
let payload: UpdateMeProfileConfig['payload'];
try {
const body = await readBody<{payload?: UpdateMeProfileConfig['payload']}>(event);
// Accept either { payload: {...} } or the raw SCIM patch value object.
payload = body && typeof body === 'object' && 'payload' in body ? body.payload : (body as any);
if (!payload || typeof payload !== 'object') {
throw new Error('payload is required');
}
} catch {
throw createError({statusCode: 400, statusMessage: 'Invalid request body.'});
}
try {
const client = AsgardeoNuxtClient.getInstance();
const user: User = await client.updateUserProfile({payload}, session.sessionId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts` around
lines 43 - 52, The handler currently trusts the entire request body as an
UpdateMeProfileConfig and forwards it to AsgardeoNuxtClient.updateUserProfile,
enabling SSRF/header injection; instead, parse only the SCIM patch payload from
readBody (e.g., extract the "schemas"/"Operations"/"patch" structure or explicit
"patch" field), then construct a new safe UpdateMeProfileConfig locally that
includes only the allowed SCIM patch data and does NOT copy through url,
headers, or fetcher; call AsgardeoNuxtClient.getInstance().updateUserProfile
with this sanitized config plus internally-set baseUrl and the Authorization
header derived from the session (session.sessionId) so external callers cannot
override URL, headers, or request-initialization options.

Comment on lines +69 to +77
// Reject encoded protocol-relative or absolute indicators in the first segment
const decoded = decodeURIComponent(trimmed.slice(1, 5).toLowerCase());
if (decoded.startsWith('/') || decoded.startsWith('\\')) {
throw new AsgardeoError(
`Open redirect blocked: returnTo "${trimmed}" contains an encoded redirect sequence.`,
ErrorCode.OpenRedirectBlocked,
{statusCode: 400},
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

decodeURIComponent can throw URIError and bypass the typed error contract.

decodeURIComponent throws on malformed escapes (e.g. validateReturnUrl('/%'), '/%X', '/aa%2'). Because the call on line 70 is not guarded, callers that rely on catching AsgardeoError / ErrorCode.OpenRedirectBlocked will instead see an uncaught URIError, turning what should be a deterministic 400 into a 500. Treat any decode failure as a blocked redirect.

Also, the docstring at lines 30-31 says “first two chars after /” while the implementation inspects the first 4 chars (slice(1, 5)); please align one or the other.

🛡️ Proposed fix
-  // Reject encoded protocol-relative or absolute indicators in the first segment
-  const decoded = decodeURIComponent(trimmed.slice(1, 5).toLowerCase());
-  if (decoded.startsWith('/') || decoded.startsWith('\\')) {
-    throw new AsgardeoError(
-      `Open redirect blocked: returnTo "${trimmed}" contains an encoded redirect sequence.`,
-      ErrorCode.OpenRedirectBlocked,
-      {statusCode: 400},
-    );
-  }
+  // Reject encoded protocol-relative or absolute indicators in the first segment.
+  // Treat any malformed escape as blocked rather than letting URIError bubble up.
+  let decoded: string;
+  try {
+    decoded = decodeURIComponent(trimmed.slice(1, 5).toLowerCase());
+  } catch {
+    throw new AsgardeoError(
+      `Open redirect blocked: returnTo "${trimmed}" contains a malformed escape sequence.`,
+      ErrorCode.OpenRedirectBlocked,
+      {statusCode: 400},
+    );
+  }
+  if (decoded.startsWith('/') || decoded.startsWith('\\')) {
+    throw new AsgardeoError(
+      `Open redirect blocked: returnTo "${trimmed}" contains an encoded redirect sequence.`,
+      ErrorCode.OpenRedirectBlocked,
+      {statusCode: 400},
+    );
+  }

Worth adding a unit test for malformed escape inputs ('/%', '/%2', '/aa%') to lock this in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/utils/url-validation.ts` around lines 69 - 77, The
call to decodeURIComponent in validateReturnUrl is unguarded and can throw
URIError on malformed percent-escapes, causing a 500 instead of a controlled
AsgardeoError; wrap the decodeURIComponent(trimmed.slice(1, 5).toLowerCase())
call in a try/catch and on any exception throw the same AsgardeoError with
ErrorCode.OpenRedirectBlocked and {statusCode: 400}. Also align the docstring
wording (the comment near the top of the function that currently says "first two
chars after `/`") to correctly describe what the code checks (the first four
characters after `/`) so the documentation matches the implementation (or
alternatively change the slice to match the docstring—pick one consistent change
and update the docstring or slice in validateReturnUrl accordingly).

Comment on lines +250 to +260
default: {
// Asgardeo's V1 flow API also returns 'RICH_TEXT' which is not in the V1
// component-type enum (the enum predates it). Render its raw HTML so
// links and inline copy show up.
if (String(component.type).toUpperCase() === 'RICH_TEXT') {
const html: string = String(cfg.text || cfg.label || '');
return h('div', {class: 'asgardeo-signup__rich-text', innerHTML: html});
}
return null;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the React parallel factory sanitizes RICH_TEXT or uses raw dangerouslySetInnerHTML,
# so we can align the two implementations.
fd -e tsx -e ts SignUpOptionFactory packages/react --exec sh -c 'echo "=== {} ==="; cat {}'
rg -nP -C3 'RICH_TEXT|dangerouslySetInnerHTML|innerHTML' packages/react/src --type=tsx --type=ts

Repository: asgardeo/javascript

Length of output: 8457


🏁 Script executed:

#!/bin/bash
# Read the Vue file around lines 250-260 to verify innerHTML usage
echo "=== Vue SignUpOptionFactory.ts (lines 240-270) ==="
sed -n '240,270p' packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts

# Search for DOMPurify usage in both Vue and React
echo -e "\n=== DOMPurify usage (Vue) ==="
rg -i 'dompurify|sanitize' packages/vue/src --type=ts --type=vue -n

echo -e "\n=== DOMPurify usage (React) ==="
rg -i 'dompurify|sanitize' packages/react/src --type=ts -n

# Search for RICH_TEXT in React more carefully
echo -e "\n=== RICH_TEXT in React ==="
rg 'RICH_TEXT' packages/react/src -n

Repository: asgardeo/javascript

Length of output: 2212


🏁 Script executed:

#!/bin/bash
# Search for RICH_TEXT across all packages
echo "=== RICH_TEXT across codebase ==="
rg 'RICH_TEXT' . -n | head -30

# Check Vue's auth factory for similar HTML rendering
echo -e "\n=== Vue auth factory (if exists) ==="
fd -type f -name '*AuthOptionFactory*' packages/vue/src

# Check if Vue has any DOMPurify or sanitization imports
echo -e "\n=== Vue package.json dependencies (looking for sanitization libs) ==="
rg 'dompurify|sanitize|xss' packages/vue/package.json

Repository: asgardeo/javascript

Length of output: 1129


Sanitize innerHTML rendering of RICH_TEXT to prevent stored XSS.

cfg.text / cfg.label flows directly into Vue's innerHTML prop without sanitization. While the source is the tenant's Asgardeo flow definition, any admin-configurable HTML on a sign-up page (before the user authenticates) is a stored-XSS vector.

This is a Vue-specific exposure—React's SignUp factory does not render RICH_TEXT at all. DOMPurify is already a dependency in packages/vue/package.json, so wrapping the HTML with DOMPurify.sanitize() is a minimal, low-risk fix:

Suggested fix
if (String(component.type).toUpperCase() === 'RICH_TEXT') {
  const html: string = String(cfg.text || cfg.label || '');
- return h('div', {class: 'asgardeo-signup__rich-text', innerHTML: html});
+ return h('div', {class: 'asgardeo-signup__rich-text', innerHTML: DOMPurify.sanitize(html)});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.ts`
around lines 250 - 260, The RICH_TEXT branch in SignUpOptionFactory is assigning
cfg.text/cfg.label directly to innerHTML causing a stored XSS risk; import
DOMPurify in this module and call DOMPurify.sanitize(String(cfg.text ||
cfg.label || '')) before passing the value to h(..., { innerHTML: ... }) so the
rendered HTML is cleaned; update the default case in the factory (the branch
that checks String(component.type).toUpperCase() === 'RICH_TEXT') to use the
sanitized HTML and ensure the DOMPurify import is added at the top of the file.

Comment on lines +60 to +76
const handleInitialize = async (
payload?: EmbeddedFlowExecuteRequestPayload,
): Promise<EmbeddedFlowExecuteResponse> => {
// Pull the application id from the URL query (same convention as the
// React V1 SignUp), falling back to the configured value from the
// Asgardeo context.
const applicationIdFromUrl: string | null =
typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get('applicationId') : null;
const effectiveApplicationId: string | undefined = applicationId || applicationIdFromUrl || undefined;

const initialPayload: any = payload || {
flowType: EmbeddedFlowType.Registration,
...(effectiveApplicationId && {applicationId: effectiveApplicationId}),
};

return (await signUp(initialPayload)) as EmbeddedFlowExecuteResponse;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm React V1 SignUp's order of precedence for applicationId.
fd -t f 'SignUp.ts' packages/react/src/components/presentation/sign-up
rg -nP -C8 'applicationId(FromUrl|Param)?' packages/react/src/components/presentation/sign-up 2>/dev/null

Repository: asgardeo/javascript

Length of output: 201


🏁 Script executed:

#!/bin/bash
# Find React SignUp component in correct location
find . -name "SignUp.ts" -o -name "SignUp.tsx" | grep -i react | head -20

Repository: asgardeo/javascript

Length of output: 273


🏁 Script executed:

#!/bin/bash
# Also search for useAsgardeo hook definition to check applicationId type
rg "useAsgardeo|export.*useAsgardeo" --type ts --type tsx -A 10 | head -100

Repository: asgardeo/javascript

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Find files in packages/react
ls -la packages/react/src/components/presentation/ 2>/dev/null || echo "Path doesn't exist"
find packages/react/src -name "*SignUp*" 2>/dev/null

Repository: asgardeo/javascript

Length of output: 1630


🏁 Script executed:

#!/bin/bash
# Check React v1 SignUp for applicationId fallback order
sed -n '1,150p' packages/react/src/components/presentation/auth/SignUp/v1/SignUp.tsx | cat -n

Repository: asgardeo/javascript

Length of output: 4752


🏁 Script executed:

#!/bin/bash
# Check Vue v1 SignUp lines 55-80 for context
sed -n '55,80p' packages/vue/src/components/presentation/sign-up/v1/SignUp.ts | cat -n

Repository: asgardeo/javascript

Length of output: 1515


🏁 Script executed:

#!/bin/bash
# Find useAsgardeo hook to check applicationId type
rg "export.*useAsgardeo|const useAsgardeo" -A 15 --type ts | head -80

Repository: asgardeo/javascript

Length of output: 6414


🏁 Script executed:

#!/bin/bash
# Check AsgardeoContext type definition for applicationId
rg "interface AsgardeoContext|type AsgardeoContext|applicationId" packages/vue/src -A 5 -B 2 | head -100

Repository: asgardeo/javascript

Length of output: 7806


🏁 Script executed:

#!/bin/bash
# Also check React AsgardeoContextProps for reference
rg "interface AsgardeoContextProps|type AsgardeoContextProps|applicationId" packages/react/src/contexts/Asgardeo -A 5 -B 2

Repository: asgardeo/javascript

Length of output: 4753


🏁 Script executed:

#!/bin/bash
# Check the actual context provider to see how applicationId is set
rg -l "AsgardeoProvider|AsgardeoContext" packages/vue/src | head -5

Repository: asgardeo/javascript

Length of output: 270


🏁 Script executed:

#!/bin/bash
# Get full context around React v1 SignUp handleInitialize (lines 60-75)
sed -n '59,80p' packages/react/src/components/presentation/auth/SignUp/v1/SignUp.tsx | cat -n

Repository: asgardeo/javascript

Length of output: 897


Fallback order contradicts the doc-comment.

The comment says "Pull the application id from the URL query (same convention as the React V1 SignUp), falling back to the configured value from the Asgardeo context." However, applicationId || applicationIdFromUrl || undefined makes the context the primary source and the URL the fallback — the opposite of the stated intent. If the URL parameter is meant to override the configured app id, swap the operands:

-      const effectiveApplicationId: string | undefined = applicationId || applicationIdFromUrl || undefined;
+      const effectiveApplicationId: string | undefined = applicationIdFromUrl || applicationId || undefined;

Note: React v1 SignUp does not implement this feature (it initializes without applicationId), so the reference to "same convention" may need clarification.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleInitialize = async (
payload?: EmbeddedFlowExecuteRequestPayload,
): Promise<EmbeddedFlowExecuteResponse> => {
// Pull the application id from the URL query (same convention as the
// React V1 SignUp), falling back to the configured value from the
// Asgardeo context.
const applicationIdFromUrl: string | null =
typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get('applicationId') : null;
const effectiveApplicationId: string | undefined = applicationId || applicationIdFromUrl || undefined;
const initialPayload: any = payload || {
flowType: EmbeddedFlowType.Registration,
...(effectiveApplicationId && {applicationId: effectiveApplicationId}),
};
return (await signUp(initialPayload)) as EmbeddedFlowExecuteResponse;
};
const handleInitialize = async (
payload?: EmbeddedFlowExecuteRequestPayload,
): Promise<EmbeddedFlowExecuteResponse> => {
// Pull the application id from the URL query (same convention as the
// React V1 SignUp), falling back to the configured value from the
// Asgardeo context.
const applicationIdFromUrl: string | null =
typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get('applicationId') : null;
const effectiveApplicationId: string | undefined = applicationIdFromUrl || applicationId || undefined;
const initialPayload: any = payload || {
flowType: EmbeddedFlowType.Registration,
...(effectiveApplicationId && {applicationId: effectiveApplicationId}),
};
return (await signUp(initialPayload)) as EmbeddedFlowExecuteResponse;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/src/components/presentation/sign-up/v1/SignUp.ts` around lines
60 - 76, The fallback order is reversed versus the comment: in handleInitialize
change how effectiveApplicationId is derived so the URL param wins over the
Asgardeo context value; update the expression that sets effectiveApplicationId
(currently using applicationId || applicationIdFromUrl || undefined) to prefer
applicationIdFromUrl first, then applicationId, and ensure initialPayload still
spreads applicationId when effectiveApplicationId is present (refer to the
effectiveApplicationId constant and the handleInitialize function /
initialPayload construction).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

♻️ Duplicate comments (6)
packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts (1)

44-46: ⚠️ Potential issue | 🟠 Major

Open-redirect bypass via backslash still unaddressed.

/\evil.com passes the current startsWith('/') && !startsWith('//') check, but WHATWG-compliant browsers (Chrome/Edge) normalize \ to / in the Location header path, treating it as //evil.com and redirecting cross-origin (see Saltcorn GHSA-f3g8-9xv5-77gv, Vue Router #3946). Whitespace and control characters are also not rejected. The persisted returnTo is consumed in callback.get.ts without re-validation before sendRedirect.

🛡️ Proposed fix
-  // Validate returnTo is a relative path to prevent open redirect
-  const safeReturnTo: string | undefined =
-    returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : undefined;
+  // Reject protocol-relative URLs (//, /\), backslashes, control chars, and whitespace.
+  const isSafeReturnTo = (value: string): boolean =>
+    value.length > 0 &&
+    value.length < 2048 &&
+    value.startsWith('/') &&
+    !value.startsWith('//') &&
+    !value.startsWith('/\\') &&
+    // eslint-disable-next-line no-control-regex
+    !/[\s\\\x00-\x1F\x7F]/.test(value);
+
+  const safeReturnTo: string | undefined =
+    typeof returnTo === 'string' && isSafeReturnTo(returnTo) ? returnTo : undefined;

Apply the same isSafeReturnTo check at the consumption site in callback.get.ts before sendRedirect, since temp-token contents are signed but still attacker-influenced via the original query string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts` around
lines 44 - 46, The stored returnTo must be re-validated before performing the
redirect to prevent backslash/whitespace control-character open-redirects; in
the callback handling code that calls sendRedirect (the logic consuming the temp
token in callback.get.ts), apply the same safety check used earlier (or
centralize into an isSafeReturnTo function) to ensure the value starts with a
single '/' (reject leading '//' or any leading backslash or control/whitespace
characters) and only then pass it to sendRedirect, otherwise fall back to a safe
default route.
packages/nuxt/src/runtime/server/utils/session.ts (1)

31-49: ⚠️ Potential issue | 🟠 Major

Enforce the documented 32-character minimum for ASGARDEO_SESSION_SECRET in production.

The error message advises "at least 32 characters" but no length check is performed; any non-empty value is accepted, including a single character. HS256 with a short key materially weakens session-token security. Add an explicit production-time length check.

🛡️ Proposed fix
   if (!secret) {
     if (process.env['NODE_ENV'] === 'production') {
       throw new Error(
         '[asgardeo] ASGARDEO_SESSION_SECRET environment variable is required in production. ' +
           'Set it to a secure random string of at least 32 characters.',
       );
     }
     // ...
     return new TextEncoder().encode('asgardeo-dev-secret-not-for-production');
   }
 
+  if (process.env['NODE_ENV'] === 'production' && secret.length < 32) {
+    throw new Error(
+      '[asgardeo] ASGARDEO_SESSION_SECRET must be at least 32 characters in production.',
+    );
+  }
+
   return new TextEncoder().encode(secret);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/session.ts` around lines 31 - 49, The
getSecret function accepts any non-empty secret but the production error message
requires at least 32 characters; add an explicit length check when determining
the secret (from the sessionSecret parameter or
process.env['ASGARDEO_SESSION_SECRET']) and, if NODE_ENV === 'production' and
the secret length is less than 32, throw a clear Error stating the 32-character
minimum; ensure the check runs before encoding (TextEncoder().encode) and keep
the existing dev fallback behavior unchanged.
packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts (1)

48-57: ⚠️ Potential issue | 🔴 Critical

Critical: SSRF / token-exfiltration risk via untrusted request body — still unaddressed.

readBody<UpdateMeProfileConfig>(event) accepts the entire UpdateMeProfileConfig shape from the client, and AsgardeoNuxtClient.updateUserProfile then spreads it into updateMeProfile({...config, baseUrl, headers: {...config.headers, Authorization: 'Bearer ...'}}). A malicious caller can therefore send { url: 'https://attacker.example/exfil', headers: {...} } and the server will dispatch the user's bearer access token to the attacker-controlled URL. Only the SCIM payload field should be accepted from the body; url, headers, fetcher, and other request-level fields must be constructed server-side and never copied through.

🛡️ Proposed fix
-    let payload: UpdateMeProfileConfig;
+    let payload: UpdateMeProfileConfig['payload'];
     try {
-      payload = await readBody<UpdateMeProfileConfig>(event);
+      const body = await readBody<{payload?: UpdateMeProfileConfig['payload']}>(event);
+      payload = body && typeof body === 'object' && 'payload' in body
+        ? body.payload!
+        : (body as UpdateMeProfileConfig['payload']);
+      if (!payload || typeof payload !== 'object') {
+        throw new Error('payload is required');
+      }
     } catch {
       throw createError({statusCode: 400, statusMessage: 'Invalid request body.'});
     }

     try {
       const client: AsgardeoNuxtClient = AsgardeoNuxtClient.getInstance();
-      const user: User = await client.updateUserProfile(payload, session.sessionId);
+      const user: User = await client.updateUserProfile({payload}, session.sessionId);
       return {data: {user}, error: '', success: true};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts` around
lines 48 - 57, The handler currently accepts an entire UpdateMeProfileConfig
from readBody and passes it into AsgardeoNuxtClient.updateUserProfile, allowing
client-supplied url/headers/fetcher to be forwarded; change this to only
accept/whitelist the SCIM payload (e.g., payload.scim or payload.data) from the
request body and reject or strip any url, headers, fetcher or other
request-level fields, then call AsgardeoNuxtClient.updateUserProfile with a
server-constructed config that sets baseUrl, headers (including Authorization:
`Bearer ${token}`) and fetcher internally; update the type handling around
readBody, AsgardeoNuxtClient.updateUserProfile and the underlying
updateMeProfile call to enforce this whitelist so no request-controlled fields
are spread into the outbound request.
packages/nuxt/src/runtime/server/utils/token-refresh.ts (1)

100-121: ⚠️ Potential issue | 🟡 Minor

Still no timeout on the token-endpoint fetch.

A slow/unresponsive Asgardeo token endpoint will pin this Nitro request thread (and any caller of getValidAccessToken, including /api/auth/token) until the runtime's default network timeout. Add an AbortSignal.timeout(...) so refresh failures fail fast and degrade gracefully into the existing 401 path.

🛡️ Proposed fix
     const res: Response = await fetch(tokenEndpoint, {
       body,
       headers: {'Content-Type': 'application/x-www-form-urlencoded'},
       method: 'POST',
+      signal: AbortSignal.timeout(10_000),
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` around lines 100 -
121, The fetch to the token endpoint in the token-refresh logic currently has no
timeout; modify the fetch call inside the try block to pass an AbortSignal with
a short timeout (e.g. AbortSignal.timeout(5000)) so slow responses abort quickly
and fall into the existing catch/401 path; specifically, create a signal via
AbortSignal.timeout(ms) and add it to the fetch options (where tokenEndpoint,
body, headers, method are set) so that getValidAccessToken / the token-refresh
flow will reject fast on slow/unresponsive Asgardeo endpoints.
packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts (1)

44-50: ⚠️ Potential issue | 🟠 Major

Catch-all still signs valid sessions out on transient getUser failures — issue persists.

When verifyAndRehydrateSession returns a non-null session, the cookie is cryptographically valid; mapping any getUser error (network blip, IdP 5xx, etc.) to {isSignedIn: false, user: null} causes the client composable consuming this endpoint to flip the user to signed-out despite their session being good. This contradicts the resilient pattern in asgardeo-ssr.ts (Promise.allSettled — keeps isSignedIn: true). The resolution comment claims this was addressed but the catch branch is unchanged.

🛡️ Proposed fix
   try {
     const client: AsgardeoNuxtClient = AsgardeoNuxtClient.getInstance();
     const user: Awaited<ReturnType<AsgardeoNuxtClient['getUser']>> = await client.getUser(session.sessionId);
     return {isLoading: false, isSignedIn: true, user};
   } catch {
-    return {isLoading: false, isSignedIn: false, user: null};
+    // Cookie is valid; preserve signed-in state but signal user details unavailable.
+    return {isLoading: false, isSignedIn: true, user: null};
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts` around
lines 44 - 50, The current catch-all in session.get.ts turns a cryptographically
valid session into signed-out on any AsgardeoNuxtClient.getUser error; change
the error handling so that if verifyAndRehydrateSession provided a non-null
session you return { isLoading: false, isSignedIn: true, user: null } (preserve
sign-in state) instead of signing out, and only return isSignedIn: false when
verifyAndRehydrateSession yielded no session; locate the logic around
AsgardeoNuxtClient.getInstance()/getUser and verifyAndRehydrateSession and
ensure you also log the getUser error for debugging rather than swallowing it.
packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts (1)

110-115: ⚠️ Potential issue | 🟠 Major

Token-response validation still permits sessions with only an idToken.

The condition !tokenResponse?.accessToken && !tokenResponse?.idToken only fails when both are missing. When the IdP returns just an idToken, this passes, issueSessionCookie stores accessToken: undefined, and every downstream API route (getUserProfile, getMyOrganizations, getAllOrganizations, etc.) that calls getAccessToken() will produce a malformed Authorization: Bearer undefined header. For a redirect-based authorization-code flow the absence of accessToken is a hard failure.

🛡️ Proposed fix
-  if (!tokenResponse?.accessToken && !tokenResponse?.idToken) {
+  if (!tokenResponse?.accessToken) {
     throw createError({
       statusCode: 500,
-      statusMessage: 'Token exchange failed: Invalid response from Identity Provider.',
+      statusMessage: 'Token exchange failed: Access token missing from Identity Provider response.',
     });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts` around
lines 110 - 115, The current check allows a response that contains only idToken,
causing accessToken to be saved as undefined and breaking downstream calls;
update the validation around tokenResponse in the callback handler to require a
non-empty accessToken (i.e. change the condition to throw when
tokenResponse?.accessToken is falsy), ensure the error uses a clear message like
"Token exchange failed: missing access token from Identity Provider", and avoid
passing an undefined accessToken into issueSessionCookie (only pass accessToken
when present) so getAccessToken()/Authorization headers never become "Bearer
undefined".
🧹 Nitpick comments (9)
packages/nuxt/tests/unit/route-matcher.test.ts (1)

42-57: Optional: add a trailing-slash case for *.

Given * is translated to [^/]* (zero-or-more non-slash chars), /admin/* will match /admin/ (empty segment). If that's intentional, an explicit assertion would lock in the contract; if not, consider switching to [^/]+ in the implementation. Not a blocker.

it('matches the base path with a trailing slash', () => {
  const match = createRouteMatcher(['/admin/*']);
  expect(match('/admin/')).toBe(true); // or .toBe(false) if you change to [^/]+
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/tests/unit/route-matcher.test.ts` around lines 42 - 57, The
current `*` handling in createRouteMatcher treats `*` as `[^/]*` which allows an
empty segment so `/admin/*` matches `/admin/`; add an explicit unit test in
route-matcher.test.ts (using the existing describe block for 'single-segment
wildcard (*)') that asserts match('/admin/') behavior (either expect(true) if
you want to keep `[^/]*` or expect(false) if you want to disallow empty
segments), and if you prefer to disallow empty segments, update the
implementation in createRouteMatcher where it replaces `*` with `[^/]*` to
instead use `[^/]+` so a segment must contain at least one character, then run
tests to ensure consistency.
packages/nuxt/tests/unit/signup-post.test.ts (1)

115-124: Optional: also assert the error message contract.

The 502 case only checks statusCode. If you adopt a sanitized statusMessage (per the comment on signup.post.ts), this test would not catch a regression where the upstream error leaks through. Consider asserting the prefix:

   await expect((signupHandler as any)(mockEvent)).rejects.toMatchObject({
     statusCode: 502,
+    statusMessage: expect.stringMatching(/^Embedded sign-up step failed/i),
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/tests/unit/signup-post.test.ts` around lines 115 - 124, Update
the test for signupHandler to also assert the sanitized error message contract:
after triggering the signUp rejection
(mockClientInstance.signUp.mockRejectedValueOnce), await the rejected error from
(signupHandler as any)(mockEvent) and assert that error.statusMessage exists and
startsWith the sanitized prefix used in signup.post.ts (e.g., "Sign-up failed"
or the exact prefix implemented there) so the upstream error text is not leaked.
packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts (1)

44-47: Remove the as any cast—proper types are already defined in the module augmentations.

PublicRuntimeConfig.asgardeo is already properly typed in packages/nuxt/src/module.ts with afterSignInUrl and afterSignOutUrl as strings. The cast breaks that type safety. Simplify to:

const afterSignUpUrl: string = (config.public.asgardeo?.afterSignInUrl) || '/';

Same applies to payload as any on line 63—use the SDK's exported EmbeddedFlowExecuteRequestPayload type instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts` around
lines 44 - 47, Remove the unnecessary "as any" casts to restore proper typing:
change the afterSignUpUrl initialization to use
config.public.asgardeo?.afterSignInUrl (no "as any") and type afterSignUpUrl as
string; likewise replace the "payload as any" usage with the SDK's exported
EmbeddedFlowExecuteRequestPayload type instead of casting. Update references in
the signup handler where useRuntimeConfig, afterSignUpUrl, and payload are used
so they rely on the module augmentation types (PublicRuntimeConfig.asgardeo) and
the EmbeddedFlowExecuteRequestPayload type from the SDK.
packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts (1)

44-45: Drop the as any cast — extend the publicConfig type instead.

(publicConfig as any).afterSignOutUrl defeats type-checking on a config field that is otherwise referenced statically (e.g., callback.get.ts reads publicConfig.afterSignInUrl without a cast). If afterSignOutUrl is missing from the public runtime-config interface, add it there so all consumers benefit; if it is present, the cast is unnecessary.

♻️ Proposed fix
-  const fallbackUrl: string = (publicConfig as any).afterSignOutUrl || '/';
+  const fallbackUrl: string = publicConfig.afterSignOutUrl || '/';

(plus adding afterSignOutUrl?: string to the public Asgardeo runtime-config type if not already declared.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts` around
lines 44 - 45, Remove the unsafe cast "(publicConfig as any).afterSignOutUrl"
and make fallbackUrl read the strongly-typed property from publicConfig instead;
to do that, add "afterSignOutUrl?: string" to the public Asgardeo runtime-config
type (the interface that shapes config.public.asgardeo) if it is missing, then
replace the cast usage so fallbackUrl is assigned from
publicConfig.afterSignOutUrl || '/' in signout.post.ts (symbols: publicConfig,
fallbackUrl, config.public.asgardeo, afterSignOutUrl).
packages/nuxt/src/runtime/server/utils/token-refresh.ts (1)

87-87: Consider accepting a tokenEndpoint override in module config.

The token endpoint is currently hardcoded as ${publicConfig.baseUrl}/oauth2/token at line 87. For deployments with custom token paths (e.g., gateway prefixes, OIDC discovery overrides), this breaks silently. Add an optional tokenEndpoint config field that falls back to the current default, matching the pattern used for signInUrl and signUpUrl.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` at line 87, Replace
the hardcoded tokenEndpoint assignment with a lookup that accepts an optional
tokenEndpoint override from the module/public config, falling back to the
existing default `${publicConfig.baseUrl}/oauth2/token` (mirror how signInUrl
and signUpUrl are handled); update the tokenEndpoint declaration in
token-refresh.ts (the tokenEndpoint constant and any related types) to read from
the config (e.g., publicConfig.tokenEndpoint or moduleConfig.tokenEndpoint) if
present, otherwise compute the default, and ensure its type remains string so
downstream code using tokenEndpoint continues to work.
packages/nuxt/tests/unit/asgardeo-root.test.ts (1)

320-324: Tighten optimistic-update assertion.

toBeDefined() here only confirms the field exists. Since the generateFlattenedUserProfile mock returns a known shape (Line 53: {email: 'updated@example.com'}), assert against that explicitly so a future regression that sets flattenedProfile to something accidental (e.g., prev.flattenedProfile instead of the regenerated value) is caught.

🛠️ Proposed change
-    expect(userProfileState.value.flattenedProfile).toBeDefined();
+    expect(userProfileState.value.flattenedProfile).toEqual({email: 'updated@example.com'});
     expect(generateFlattenedUserProfile).toHaveBeenCalledWith(updatedUser, MOCK_USER_PROFILE.schemas);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/tests/unit/asgardeo-root.test.ts` around lines 320 - 324,
Replace the loose presence check on flattenedProfile with a strict equality
assertion: after obtaining userProfileState (variable userProfileState) assert
that userProfileState.value.flattenedProfile equals the known mock return value
from generateFlattenedUserProfile (the shape defined around line 53, e.g. {
email: 'updated@example.com' }) or call
generateFlattenedUserProfile(updatedUser, MOCK_USER_PROFILE.schemas) and assert
equality to that result; reference the generateFlattenedUserProfile mock and
MOCK_USER_PROFILE to locate the correct expected shape.
packages/nuxt/src/runtime/components/AsgardeoRoot.ts (1)

108-124: Defensively pass prev.schemas ?? [] to generateFlattenedUserProfile.

If prev has been seeded from a partial SSR payload or written by a third party that omitted schemas, prev.schemas could be undefined. Per packages/javascript/src/utils/generateFlattenedUserProfile.ts:55-140 the function calls processedSchemas.map(...) unconditionally and will throw Cannot read properties of undefined (reading 'map'), breaking the optimistic-update path.

🛡️ Proposed fix
       userProfileState.value = prev
         ? {
             ...prev,
-            flattenedProfile: generateFlattenedUserProfile(payload, prev.schemas),
+            flattenedProfile: generateFlattenedUserProfile(payload, prev.schemas ?? []),
             profile: payload,
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/components/AsgardeoRoot.ts` around lines 108 - 124,
onUpdateProfile's optimistic-update uses
generateFlattenedUserProfile(prev.schemas) which can throw if prev.schemas is
undefined; update the call inside the truthy-prev branch to pass prev.schemas ??
[] instead (i.e., call generateFlattenedUserProfile(payload, prev.schemas ??
[])) so the function always receives an array; keep the else branch and the
authState.value update as-is.
packages/nuxt/src/module.ts (1)

51-70: defu will not resolve env-var precedence the way the comment claims.

The comment on Line 50 promises "env vars (highest)". Because every env var here resolves to string | undefined, and defu strips undefined, the precedence is correct for present env vars. But a developer reading "highest wins" may set NUXT_PUBLIC_ASGARDEO_AFTER_SIGN_IN_URL="" (empty string) expecting it to clear the user-config value — instead defu will keep it as "" (truthy in object terms) and override the user config silently. Worth a sentence in the comment that explicit empty-string env vars are honoured as values.

Also, since Nuxt automatically maps NUXT_PUBLIC_ASGARDEO_* env vars onto runtimeConfig.public.asgardeo.* at startup (the standard runtimeConfig override mechanism), this manual env layer partially duplicates Nuxt's built-in resolution and may confuse the precedence story. Worth verifying you actually need the explicit reads here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/module.ts` around lines 51 - 70, The current defu usage in
the publicConfig construction (variable publicConfig) treats explicit
empty-string env vars as valid values and will therefore override userOptions
unexpectedly; update the code/comments to make this explicit and/or change the
env-layer behavior: either coerce empty strings to undefined before passing into
defu (so empty NUXT_PUBLIC_ASGARDEO_* acts like "unset") or keep the existing
semantics but add a clear comment that empty-string env vars are honoured and
will override user options; also verify whether the manual reads of process.env
are necessary given Nuxt's automatic mapping of NUXT_PUBLIC_* into
runtimeConfig.public and remove the duplication if redundant.
packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts (1)

64-64: Use the typed NitroApp instead of {hooks: {hook: Function}}.

@typescript-eslint/no-unsafe-function-type flags bare Function, and you lose call-site type safety on hook arguments. Import NitroApp from nitropack (already in deps) and type the parameter accordingly.

♻️ Proposed change
-import {defineNitroPlugin} from 'nitropack/runtime';
+import {defineNitroPlugin, type NitroApp} from 'nitropack/runtime';
@@
-export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => {
+export default defineNitroPlugin((nitro: NitroApp) => {
   nitro.hooks.hook('request', async (event: H3Event) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts` at line 64, The
plugin parameter is currently typed as a raw object ({hooks: {hook: Function}})
which triggers no-unsafe-function-type and loses call-site types; replace that
with the proper NitroApp type from nitropack: import NitroApp and change the
defineNitroPlugin signature to accept nitro: NitroApp (so locate the
defineNitroPlugin export and the nitro parameter in asgardeo-ssr.ts and update
the import and parameter type accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/nuxt/src/runtime/composables/useAsgardeo.ts`:
- Around line 59-107: The embedded-path call to $fetch in signIn currently lets
non-2xx responses reject uncaught; wrap the $fetch('/api/auth/signin', ...) call
in a try/catch, catch FetchError (or generic Error), extract a stable error
payload from err.data?.statusMessage || err.data?.message || err.message and
err.status || err.data?.statusCode, and re-throw a normalized error/object so
callers (BaseSignIn onError handlers) receive a consistent shape with at least
{message, statusCode, data}; keep the existing success path and subsequent
session fetch/useState/EmbeddedSignInFlowStatus handling unchanged.
- Around line 139-141: The navigation for afterSignUpUrl currently calls
navigateTo(res.data?.afterSignUpUrl as string, {external: false}) which will
fail for absolute URLs; update that call in useAsgardeo.ts (the branch handling
res.data?.afterSignUpUrl) to use {external: true} so absolute (cross-origin)
redirects work and match the redirect-mode branch and signOut behavior; keep the
await and return undefined behavior unchanged.

In `@packages/nuxt/src/runtime/plugins/asgardeo.ts`:
- Around line 168-193: Extract the duplicate redirect logic into a shared helper
(e.g. resolveSignUpRedirect(publicConfig)) that encapsulates the branching:
return publicConfig.signUpUrl if present, else return
getRedirectBasedSignUpUrl({...publicConfig}), and if neither returns a URL
return the agreed-upon fallback route (make both implementations use the same
string — pick /sign-up or /api/auth/signup and apply consistently). Replace the
signUp implementation in the plugin (const signUp) and the composable
(useAsgardeo().signUp) to call resolveSignUpRedirect(publicConfig) and then
await navigateTo(resolvedUrl, { external: true }); remove the duplicated
branching and ensure getRedirectBasedSignUpUrl and navigateTo are reused.
- Around line 207-236: The current ASGARDEO_KEY provider stubbed many methods
(clearSession, exchangeToken, getDecodedIdToken, getIdToken, http.request,
http.requestAll, signInSilently, switchOrganization, reInitialize, and
signInOptions/storage/platform) with silent no-ops; replace these with
development-time warnings (wrap noop replacements with if (import.meta.dev)
console.warn(...)) and, where possible, implement real wiring to Nitro auth
endpoints: implement getIdToken and getDecodedIdToken to call your Nitro
endpoints (e.g., '/api/auth/getIdToken' and '/api/auth/decodeIdToken') using the
Nuxt fetch helper available on nuxtApp (nuxtApp.$fetch or useFetch via nuxtApp)
and implement http.request/requestAll to proxy through a Nitro endpoint (e.g.,
'/api/auth/httpRequest') forwarding method/headers/body and returning parsed
responses; similarly wire exchangeToken, signInSilently, switchOrganization and
clearSession to appropriate Nitro endpoints (e.g., '/api/auth/exchange',
'/api/auth/silentSignIn', '/api/auth/switchOrg', '/api/auth/clear') and make
reInitialize perform an actual re-init call or return false only after
attempting the endpoint; for fields you cannot implement yet (signInOptions,
storage, platform) keep explicit undefined but add import.meta.dev console.warns
explaining they are unsupported in Nuxt mode; update the ASGARDEO_KEY object in
nuxtApp.vueApp.provide to call these new wrapper functions so consumers get
warnings in dev and real behavior against server endpoints in production.
- Around line 141-143: The client-only path in the asgardeo plugin
unconditionally sets authState.value.isLoading to false without seeding
isSignedIn when SSR is disabled; change the import.meta.client branch to detect
missing SSR payload using import.meta.server-rendered (i.e., when
import.meta.client && !import.meta.server-rendered) and perform a fetch to the
session endpoint ("/api/auth/session") to obtain the real auth state, merge the
returned session data into authState.value (updating isSignedIn and any other
fields) and only then set isLoading to false; update the logic around
authState.value and the client-only branch in asgardeo.ts so the session fetch
runs before flipping isLoading.

In `@packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts`:
- Around line 67-112: The non-atomic check of client.isInitialized allows
concurrent requests to call AsgardeoNuxtClient.getInstance().initialize()
simultaneously; fix by introducing and using a cached in-flight promise on the
singleton (e.g., client._initPromise) or a mutex so only one initialize() runs:
when isInitialized is false, set client._initPromise = client.initialize(...)
(or acquire mutex) and have other callers await that promise (or wait for mutex)
instead of calling initialize() again; ensure _initPromise is cleared on failure
and isInitialized is set only after successful initialization so subsequent
calls behave correctly.
- Around line 67-112: The singleton AsgardeoNuxtClient is being initialized once
with afterSignInUrl/afterSignOutUrl derived from resolveCallbackUrl(event),
which pins the redirect_uri for the whole process; remove per-request callback
URLs from the global initialization in the client.initialize call (keep baseUrl,
clientId, clientSecret, scopes) and instead compute resolveCallbackUrl(event) on
each auth request and pass it into the runtime sign-in/callback flows (e.g., via
client.signIn(...) or the authorize URL generator) so the redirect_uri is
derived per-request; update any code paths that generate the authorize URL or
call signIn/callback to accept an explicit callbackUrl override and ensure
AsgardeoNuxtClient no longer stores a process-wide
afterSignInUrl/afterSignOutUrl.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts`:
- Around line 67-88: When verification of the temp session token fails in the
catch block for verifyTempSessionToken (handling tempSessionCookie and
sessionSecret), clear the temporary session cookie before throwing the 400
createError so the client doesn't keep retrying with the same invalid cookie;
locate the catch that wraps the verifyTempSessionToken call and add logic to
remove/reset the cookie (e.g., use the same cookie name/mechanism used to read
tempSessionCookie) then rethrow the createError.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts`:
- Around line 62-69: The catch block in the signup handler currently forwards
the raw upstream error message via createError(statusMessage) after calling
client.signUp(payload), which can leak internal details; instead, log the full
error server-side (e.g., using the existing logger or console.error) and throw
createError with a generic, non-sensitive message like "Embedded sign-up step
failed" (no err.message), preserving the original err when logging for
diagnostics; update the catch surrounding client.signUp in signup.post.ts to
perform the server-side log and replace the statusMessage payload accordingly.

In `@packages/nuxt/src/runtime/server/utils/session.ts`:
- Around line 207-213: The code currently assigns userId by falling back to
sessionId when idToken.sub is missing; change this to fail loudly by validating
that idToken.sub exists (since KnownIdToken.sub is required) and throwing a
clear error if it's absent instead of using sessionId; locate the validation
around the idToken, userId and sessionId variables and replace the fallback with
an explicit check/throw. Also simplify numeric conversion for
tokenResponse.expiresIn by using Number(tokenResponse.expiresIn ?? 3600) when
computing accessTokenExpiresAt (referencing tokenResponse.expiresIn and
accessTokenExpiresAt) to ensure a clean numeric value.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts`:
- Around line 66-141: getValidAccessToken currently allows concurrent refreshes
which causes refresh-token rotation races; add deduplication by creating a
module-level Map (e.g., inflight: Map<string,
Promise<OIDCTokenRefreshResponse>>) keyed by session.sessionId (or
session.refreshToken), store the in-flight refresh Promise before making the
fetch, have concurrent callers await that Promise instead of issuing their own
fetch, ensure the fetch/parse logic from the try block is run inside that
Promise and that you delete the key in a finally block, then resume the existing
logic (createSessionToken, setCookie, return refreshed.access_token) after
awaiting the shared Promise so cookie updates cannot be overwritten by a stale
result.

---

Duplicate comments:
In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts`:
- Around line 110-115: The current check allows a response that contains only
idToken, causing accessToken to be saved as undefined and breaking downstream
calls; update the validation around tokenResponse in the callback handler to
require a non-empty accessToken (i.e. change the condition to throw when
tokenResponse?.accessToken is falsy), ensure the error uses a clear message like
"Token exchange failed: missing access token from Identity Provider", and avoid
passing an undefined accessToken into issueSessionCookie (only pass accessToken
when present) so getAccessToken()/Authorization headers never become "Bearer
undefined".

In `@packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts`:
- Around line 44-50: The current catch-all in session.get.ts turns a
cryptographically valid session into signed-out on any
AsgardeoNuxtClient.getUser error; change the error handling so that if
verifyAndRehydrateSession provided a non-null session you return { isLoading:
false, isSignedIn: true, user: null } (preserve sign-in state) instead of
signing out, and only return isSignedIn: false when verifyAndRehydrateSession
yielded no session; locate the logic around
AsgardeoNuxtClient.getInstance()/getUser and verifyAndRehydrateSession and
ensure you also log the getUser error for debugging rather than swallowing it.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts`:
- Around line 44-46: The stored returnTo must be re-validated before performing
the redirect to prevent backslash/whitespace control-character open-redirects;
in the callback handling code that calls sendRedirect (the logic consuming the
temp token in callback.get.ts), apply the same safety check used earlier (or
centralize into an isSafeReturnTo function) to ensure the value starts with a
single '/' (reject leading '//' or any leading backslash or control/whitespace
characters) and only then pass it to sendRedirect, otherwise fall back to a safe
default route.

In `@packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts`:
- Around line 48-57: The handler currently accepts an entire
UpdateMeProfileConfig from readBody and passes it into
AsgardeoNuxtClient.updateUserProfile, allowing client-supplied
url/headers/fetcher to be forwarded; change this to only accept/whitelist the
SCIM payload (e.g., payload.scim or payload.data) from the request body and
reject or strip any url, headers, fetcher or other request-level fields, then
call AsgardeoNuxtClient.updateUserProfile with a server-constructed config that
sets baseUrl, headers (including Authorization: `Bearer ${token}`) and fetcher
internally; update the type handling around readBody,
AsgardeoNuxtClient.updateUserProfile and the underlying updateMeProfile call to
enforce this whitelist so no request-controlled fields are spread into the
outbound request.

In `@packages/nuxt/src/runtime/server/utils/session.ts`:
- Around line 31-49: The getSecret function accepts any non-empty secret but the
production error message requires at least 32 characters; add an explicit length
check when determining the secret (from the sessionSecret parameter or
process.env['ASGARDEO_SESSION_SECRET']) and, if NODE_ENV === 'production' and
the secret length is less than 32, throw a clear Error stating the 32-character
minimum; ensure the check runs before encoding (TextEncoder().encode) and keep
the existing dev fallback behavior unchanged.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts`:
- Around line 100-121: The fetch to the token endpoint in the token-refresh
logic currently has no timeout; modify the fetch call inside the try block to
pass an AbortSignal with a short timeout (e.g. AbortSignal.timeout(5000)) so
slow responses abort quickly and fall into the existing catch/401 path;
specifically, create a signal via AbortSignal.timeout(ms) and add it to the
fetch options (where tokenEndpoint, body, headers, method are set) so that
getValidAccessToken / the token-refresh flow will reject fast on
slow/unresponsive Asgardeo endpoints.

---

Nitpick comments:
In `@packages/nuxt/src/module.ts`:
- Around line 51-70: The current defu usage in the publicConfig construction
(variable publicConfig) treats explicit empty-string env vars as valid values
and will therefore override userOptions unexpectedly; update the code/comments
to make this explicit and/or change the env-layer behavior: either coerce empty
strings to undefined before passing into defu (so empty NUXT_PUBLIC_ASGARDEO_*
acts like "unset") or keep the existing semantics but add a clear comment that
empty-string env vars are honoured and will override user options; also verify
whether the manual reads of process.env are necessary given Nuxt's automatic
mapping of NUXT_PUBLIC_* into runtimeConfig.public and remove the duplication if
redundant.

In `@packages/nuxt/src/runtime/components/AsgardeoRoot.ts`:
- Around line 108-124: onUpdateProfile's optimistic-update uses
generateFlattenedUserProfile(prev.schemas) which can throw if prev.schemas is
undefined; update the call inside the truthy-prev branch to pass prev.schemas ??
[] instead (i.e., call generateFlattenedUserProfile(payload, prev.schemas ??
[])) so the function always receives an array; keep the else branch and the
authState.value update as-is.

In `@packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts`:
- Line 64: The plugin parameter is currently typed as a raw object ({hooks:
{hook: Function}}) which triggers no-unsafe-function-type and loses call-site
types; replace that with the proper NitroApp type from nitropack: import
NitroApp and change the defineNitroPlugin signature to accept nitro: NitroApp
(so locate the defineNitroPlugin export and the nitro parameter in
asgardeo-ssr.ts and update the import and parameter type accordingly).

In `@packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts`:
- Around line 44-45: Remove the unsafe cast "(publicConfig as
any).afterSignOutUrl" and make fallbackUrl read the strongly-typed property from
publicConfig instead; to do that, add "afterSignOutUrl?: string" to the public
Asgardeo runtime-config type (the interface that shapes config.public.asgardeo)
if it is missing, then replace the cast usage so fallbackUrl is assigned from
publicConfig.afterSignOutUrl || '/' in signout.post.ts (symbols: publicConfig,
fallbackUrl, config.public.asgardeo, afterSignOutUrl).

In `@packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts`:
- Around line 44-47: Remove the unnecessary "as any" casts to restore proper
typing: change the afterSignUpUrl initialization to use
config.public.asgardeo?.afterSignInUrl (no "as any") and type afterSignUpUrl as
string; likewise replace the "payload as any" usage with the SDK's exported
EmbeddedFlowExecuteRequestPayload type instead of casting. Update references in
the signup handler where useRuntimeConfig, afterSignUpUrl, and payload are used
so they rely on the module augmentation types (PublicRuntimeConfig.asgardeo) and
the EmbeddedFlowExecuteRequestPayload type from the SDK.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts`:
- Line 87: Replace the hardcoded tokenEndpoint assignment with a lookup that
accepts an optional tokenEndpoint override from the module/public config,
falling back to the existing default `${publicConfig.baseUrl}/oauth2/token`
(mirror how signInUrl and signUpUrl are handled); update the tokenEndpoint
declaration in token-refresh.ts (the tokenEndpoint constant and any related
types) to read from the config (e.g., publicConfig.tokenEndpoint or
moduleConfig.tokenEndpoint) if present, otherwise compute the default, and
ensure its type remains string so downstream code using tokenEndpoint continues
to work.

In `@packages/nuxt/tests/unit/asgardeo-root.test.ts`:
- Around line 320-324: Replace the loose presence check on flattenedProfile with
a strict equality assertion: after obtaining userProfileState (variable
userProfileState) assert that userProfileState.value.flattenedProfile equals the
known mock return value from generateFlattenedUserProfile (the shape defined
around line 53, e.g. { email: 'updated@example.com' }) or call
generateFlattenedUserProfile(updatedUser, MOCK_USER_PROFILE.schemas) and assert
equality to that result; reference the generateFlattenedUserProfile mock and
MOCK_USER_PROFILE to locate the correct expected shape.

In `@packages/nuxt/tests/unit/route-matcher.test.ts`:
- Around line 42-57: The current `*` handling in createRouteMatcher treats `*`
as `[^/]*` which allows an empty segment so `/admin/*` matches `/admin/`; add an
explicit unit test in route-matcher.test.ts (using the existing describe block
for 'single-segment wildcard (*)') that asserts match('/admin/') behavior
(either expect(true) if you want to keep `[^/]*` or expect(false) if you want to
disallow empty segments), and if you prefer to disallow empty segments, update
the implementation in createRouteMatcher where it replaces `*` with `[^/]*` to
instead use `[^/]+` so a segment must contain at least one character, then run
tests to ensure consistency.

In `@packages/nuxt/tests/unit/signup-post.test.ts`:
- Around line 115-124: Update the test for signupHandler to also assert the
sanitized error message contract: after triggering the signUp rejection
(mockClientInstance.signUp.mockRejectedValueOnce), await the rejected error from
(signupHandler as any)(mockEvent) and assert that error.statusMessage exists and
startsWith the sanitized prefix used in signup.post.ts (e.g., "Sign-up failed"
or the exact prefix implemented there) so the upstream error text is not leaked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a64e0fb-9b76-4125-942a-41db044d16be

📥 Commits

Reviewing files that changed from the base of the PR and between 37adcd9 and db4636c.

📒 Files selected for processing (50)
  • packages/nuxt/src/module.ts
  • packages/nuxt/src/runtime/components/AsgardeoRoot.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
  • packages/nuxt/src/runtime/components/actions/SignOutButton.ts
  • packages/nuxt/src/runtime/components/actions/SignUpButton.ts
  • packages/nuxt/src/runtime/components/auth/SignIn.ts
  • packages/nuxt/src/runtime/components/user/UserDropdown.ts
  • packages/nuxt/src/runtime/composables/useAsgardeo.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/src/runtime/plugins/asgardeo.ts
  • packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts
  • packages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts
  • packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/session.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signin.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signout.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts
  • packages/nuxt/src/runtime/server/utils/serverSession.ts
  • packages/nuxt/src/runtime/server/utils/session.ts
  • packages/nuxt/src/runtime/server/utils/token-refresh.ts
  • packages/nuxt/src/runtime/utils/createRouteMatcher.ts
  • packages/nuxt/src/runtime/utils/log.ts
  • packages/nuxt/src/runtime/utils/url-validation.ts
  • packages/nuxt/tests/__mocks__/nuxt-app.ts
  • packages/nuxt/tests/unit/asgardeo-root.test.ts
  • packages/nuxt/tests/unit/asgardeo-ssr.test.ts
  • packages/nuxt/tests/unit/callback-post.test.ts
  • packages/nuxt/tests/unit/define-asgardeo-middleware.test.ts
  • packages/nuxt/tests/unit/error-codes.test.ts
  • packages/nuxt/tests/unit/route-matcher.test.ts
  • packages/nuxt/tests/unit/session-manager.test.ts
  • packages/nuxt/tests/unit/signin-post.test.ts
  • packages/nuxt/tests/unit/signup-post.test.ts
  • packages/nuxt/tests/unit/token-refresh.test.ts
  • packages/nuxt/tests/unit/url-validation.test.ts
  • packages/nuxt/tsconfig.eslint.json
  • packages/nuxt/vitest.config.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/nuxt/src/runtime/utils/url-validation.ts
  • packages/nuxt/tests/mocks/nuxt-app.ts
  • packages/nuxt/tests/unit/error-codes.test.ts
  • packages/nuxt/src/runtime/components/actions/SignInButton.ts
🚧 Files skipped from review as they are similar to previous changes (20)
  • packages/nuxt/vitest.config.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/token.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/user.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts
  • packages/nuxt/tests/unit/signin-post.test.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts
  • packages/nuxt/src/runtime/components/actions/SignUpButton.ts
  • packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts
  • packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts
  • packages/nuxt/src/runtime/components/user/UserDropdown.ts
  • packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts
  • packages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.ts
  • packages/nuxt/tests/unit/url-validation.test.ts
  • packages/nuxt/tests/unit/token-refresh.test.ts
  • packages/nuxt/tests/unit/define-asgardeo-middleware.test.ts
  • packages/nuxt/src/runtime/server/routes/auth/session/callback.post.ts
  • packages/nuxt/tests/unit/asgardeo-ssr.test.ts
  • packages/nuxt/src/runtime/components/actions/SignOutButton.ts
  • packages/nuxt/src/runtime/server/AsgardeoNuxtClient.ts

Comment on lines +59 to +107
const signIn = async (...args: any[]): Promise<any> => {
// Embedded-flow path: second arg is a non-null object with `flowId`.
const arg0: unknown = args[0];
const isEmbedded: boolean = typeof arg0 === 'object' && arg0 !== null && 'flowId' in arg0;

if (isEmbedded) {
const payload: Record<string, unknown> = arg0 as Record<string, unknown>;
const request: Record<string, unknown> = (args[1] ?? {}) as Record<string, unknown>;
const res: {data: any; success: boolean} = await $fetch<{data: any; success: boolean}>('/api/auth/signin', {
body: {payload, request},
method: 'POST',
});

// Flow complete — server has set the session cookie. Refresh the client
// auth state so `useAsgardeo().isSignedIn` flips to true *immediately*
// (without waiting for a full page reload). Then return a synthetic
// SuccessCompleted response so `BaseSignIn` emits its `success` event
// and the wrapper component (`<AsgardeoSignIn>`) drives navigation via
// `onSuccess`.
//
// `authData` is intentionally empty: the auth code / state were already
// consumed server-side in `signin.post.ts`, so there is nothing to
// forward to the client. Keeping it `{}` also stops the wrapper's
// `handleSuccess` from appending stray query params to `afterSignInUrl`.
if (res.data?.afterSignInUrl) {
if (import.meta.client) {
try {
const session: AsgardeoAuthState = await $fetch<AsgardeoAuthState>('/api/auth/session');
const authState: Ref<AsgardeoAuthState> = useState<AsgardeoAuthState>('asgardeo:auth');
authState.value = session;
} catch {
// Best-effort — the cookie is set; a navigation will recover state.
}
}
return {
authData: {},
flowStatus: EmbeddedSignInFlowStatus.SuccessCompleted,
};
}
return res.data;
}

// Redirect flow.
const options: Record<string, unknown> | undefined = arg0 as Record<string, unknown> | undefined;
const returnTo: string | undefined = typeof options?.['returnTo'] === 'string' ? options['returnTo'] : undefined;
const url: string = returnTo ? `/api/auth/signin?returnTo=${encodeURIComponent(returnTo)}` : '/api/auth/signin';
await navigateTo(url, {external: true});
return undefined;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Embedded signIn swallows server errors via $fetch.

$fetch rejects with FetchError on non-2xx responses, but there's no try/catch around the call on Line 67. A server-side createError({statusCode: 502, ...}) from signin.post.ts (e.g., "Authorization code missing", "Token exchange failed") will propagate as an unhandled rejection through BaseSignIn's onSubmit/onInitialize, which may surface as a generic error rather than the meaningful server message.

Consider catching FetchError, extracting err.data?.statusMessage/err.data?.message, and re-throwing with a stable error shape so BaseSignIn's onError consumers get useful diagnostics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/composables/useAsgardeo.ts` around lines 59 - 107,
The embedded-path call to $fetch in signIn currently lets non-2xx responses
reject uncaught; wrap the $fetch('/api/auth/signin', ...) call in a try/catch,
catch FetchError (or generic Error), extract a stable error payload from
err.data?.statusMessage || err.data?.message || err.message and err.status ||
err.data?.statusCode, and re-throw a normalized error/object so callers
(BaseSignIn onError handlers) receive a consistent shape with at least {message,
statusCode, data}; keep the existing success path and subsequent session
fetch/useState/EmbeddedSignInFlowStatus handling unchanged.

Comment on lines +139 to +141
if (res.data?.afterSignUpUrl) {
await navigateTo(res.data.afterSignUpUrl as string, {external: false});
return undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

afterSignUpUrl navigation uses external: false, but the URL may be absolute.

The server signup.post.ts returns afterSignUpUrl derived from config.public.asgardeo.afterSignInUrl || '/'. If a consumer ever configures afterSignInUrl as an absolute URL (even just https://app.example.com/dashboard), navigateTo(url, {external: false}) will throw a navigation error in strict mode (or silently fail to navigate cross-origin). For consistency with the redirect-mode branch on Line 105 and signOut on Line 111, this should be {external: true}navigateTo already handles internal paths correctly with external: true.

🛠️ Proposed fix
       if (res.data?.afterSignUpUrl) {
-        await navigateTo(res.data.afterSignUpUrl as string, {external: false});
+        await navigateTo(res.data.afterSignUpUrl as string, {external: true});
         return undefined;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (res.data?.afterSignUpUrl) {
await navigateTo(res.data.afterSignUpUrl as string, {external: false});
return undefined;
if (res.data?.afterSignUpUrl) {
await navigateTo(res.data.afterSignUpUrl as string, {external: true});
return undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/composables/useAsgardeo.ts` around lines 139 - 141,
The navigation for afterSignUpUrl currently calls
navigateTo(res.data?.afterSignUpUrl as string, {external: false}) which will
fail for absolute URLs; update that call in useAsgardeo.ts (the branch handling
res.data?.afterSignUpUrl) to use {external: true} so absolute (cross-origin)
redirects work and match the redirect-mode branch and signOut behavior; keep the
await and return undefined behavior unchanged.

Comment thread packages/nuxt/src/runtime/plugins/asgardeo.ts
Comment on lines +168 to +193
// Redirect-based sign-up — mirrors `AsgardeoReactClient.signUp` (no-arg
// overload). The composable's `signUp` shadows this for SDK consumers, but
// base components in `@asgardeo/vue` (e.g. `BaseSignUpButton`) call into
// the context's `signUp` directly when no Nuxt-aware override is in scope.
const signUp = async (): Promise<void> => {
if (publicConfig.signUpUrl) {
await navigateTo(publicConfig.signUpUrl, {external: true});
return;
}

const redirectUrl: string = getRedirectBasedSignUpUrl({
applicationId: publicConfig.applicationId,
baseUrl: publicConfig.baseUrl,
clientId: publicConfig.clientId,
} as any);

if (redirectUrl) {
await navigateTo(redirectUrl, {external: true});
return;
}

// Last-resort fallback for unrecognised baseUrls — keeps the historical
// behaviour of hitting the (POST-only) Nitro route, which will surface a
// 405 in the network tab and make the misconfiguration obvious.
await navigateTo('/api/auth/signup', {external: true});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

signUp logic is duplicated with the composable; extract a shared helper.

The redirect-based sign-up branching (signUpUrl override → getRedirectBasedSignUpUrl/api/auth/signup fallback) is nearly identical to the implementation in packages/nuxt/src/runtime/composables/useAsgardeo.ts (Lines 146–182). Drift between the two will be a recurring source of bugs (one diverged behaviour between <AsgardeoSignUpButton> and useAsgardeo().signUp()). Extract into a single resolveSignUpRedirect(publicConfig) helper used by both.

Also note: the composable's last-resort fallback navigates to /sign-up (no API call), while this plugin's fallback navigates to /api/auth/signup — those are inconsistent and will surface different errors to users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/plugins/asgardeo.ts` around lines 168 - 193,
Extract the duplicate redirect logic into a shared helper (e.g.
resolveSignUpRedirect(publicConfig)) that encapsulates the branching: return
publicConfig.signUpUrl if present, else return
getRedirectBasedSignUpUrl({...publicConfig}), and if neither returns a URL
return the agreed-upon fallback route (make both implementations use the same
string — pick /sign-up or /api/auth/signup and apply consistently). Replace the
signUp implementation in the plugin (const signUp) and the composable
(useAsgardeo().signUp) to call resolveSignUpRedirect(publicConfig) and then
await navigateTo(resolvedUrl, { external: true }); remove the duplicated
branching and ensure getRedirectBasedSignUpUrl and navigateTo are reused.

Comment on lines +207 to +236
nuxtApp.vueApp.provide(ASGARDEO_KEY, {
afterSignInUrl: publicConfig.afterSignInUrl,
applicationId: publicConfig.applicationId,
baseUrl: publicConfig.baseUrl,
clearSession: noop,
clientId: publicConfig.clientId,
exchangeToken: noop,
getAccessToken,
getDecodedIdToken: noop,
getIdToken: noop,
http: {request: noop, requestAll: noop},
instanceId: 0,
isInitialized,
isLoading,
isSignedIn,
organization: organizationRef,
organizationHandle: publicConfig.organizationHandle,
platform: undefined,
reInitialize: async () => false,
signIn,
signInOptions: undefined,
signInSilently: noop,
signInUrl: publicConfig.signInUrl,
signOut,
signUp,
signUpUrl: publicConfig.signUpUrl,
storage: undefined,
switchOrganization: noop,
user,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Multiple ASGARDEO_KEY capabilities are silent no-ops.

clearSession, exchangeToken, getDecodedIdToken, getIdToken, http.request/requestAll, signInSilently, switchOrganization, reInitialize, and signInOptions/storage/platform: undefined are all stubbed out. Anything in @asgardeo/vue that reaches into the context for these (e.g., a base component performing an authenticated http.request, or useAsgardeo().getIdToken()) will silently return undefined instead of failing loudly, which makes misuse extremely hard to debug.

At minimum:

  • Have the no-ops console.warn in dev (import.meta.dev) so developers see they're using an unsupported method in Nuxt mode, and
  • Prefer wiring getDecodedIdToken/getIdToken/http.request to corresponding /api/auth/* Nitro endpoints rather than stubbing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/plugins/asgardeo.ts` around lines 207 - 236, The
current ASGARDEO_KEY provider stubbed many methods (clearSession, exchangeToken,
getDecodedIdToken, getIdToken, http.request, http.requestAll, signInSilently,
switchOrganization, reInitialize, and signInOptions/storage/platform) with
silent no-ops; replace these with development-time warnings (wrap noop
replacements with if (import.meta.dev) console.warn(...)) and, where possible,
implement real wiring to Nitro auth endpoints: implement getIdToken and
getDecodedIdToken to call your Nitro endpoints (e.g., '/api/auth/getIdToken' and
'/api/auth/decodeIdToken') using the Nuxt fetch helper available on nuxtApp
(nuxtApp.$fetch or useFetch via nuxtApp) and implement http.request/requestAll
to proxy through a Nitro endpoint (e.g., '/api/auth/httpRequest') forwarding
method/headers/body and returning parsed responses; similarly wire
exchangeToken, signInSilently, switchOrganization and clearSession to
appropriate Nitro endpoints (e.g., '/api/auth/exchange',
'/api/auth/silentSignIn', '/api/auth/switchOrg', '/api/auth/clear') and make
reInitialize perform an actual re-init call or return false only after
attempting the endpoint; for fields you cannot implement yet (signInOptions,
storage, platform) keep explicit undefined but add import.meta.dev console.warns
explaining they are unsupported in Nuxt mode; update the ASGARDEO_KEY object in
nuxtApp.vueApp.provide to call these new wrapper functions so consumers get
warnings in dev and real behavior against server endpoints in production.

Comment on lines +67 to +88
if (!tempSessionCookie) {
throw createError({
statusCode: 400,
statusMessage: 'No temporary session found. Please start the sign-in flow again.',
});
}

let sessionId: string;
let returnTo: string | undefined;
try {
const tempSession: Awaited<ReturnType<typeof verifyTempSessionToken>> = await verifyTempSessionToken(
tempSessionCookie,
sessionSecret,
);
sessionId = tempSession.sessionId;
returnTo = tempSession.returnTo;
} catch {
throw createError({
statusCode: 400,
statusMessage: 'Invalid or expired temporary session. Please start the sign-in flow again.',
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear the temp session cookie when verification fails.

When the temp cookie is malformed/expired, you throw 400 but leave the cookie on the client. Subsequent retries will hit the same failure path until the cookie naturally expires, since signin.get.ts may not always overwrite it before the user reloads /api/auth/callback. Deleting on failure ensures the user gets a clean restart on retry.

🧹 Proposed fix
   } catch {
+    deleteCookie(event, getTempSessionCookieName(), getTempSessionCookieOptions());
     throw createError({
       statusCode: 400,
       statusMessage: 'Invalid or expired temporary session. Please start the sign-in flow again.',
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!tempSessionCookie) {
throw createError({
statusCode: 400,
statusMessage: 'No temporary session found. Please start the sign-in flow again.',
});
}
let sessionId: string;
let returnTo: string | undefined;
try {
const tempSession: Awaited<ReturnType<typeof verifyTempSessionToken>> = await verifyTempSessionToken(
tempSessionCookie,
sessionSecret,
);
sessionId = tempSession.sessionId;
returnTo = tempSession.returnTo;
} catch {
throw createError({
statusCode: 400,
statusMessage: 'Invalid or expired temporary session. Please start the sign-in flow again.',
});
}
if (!tempSessionCookie) {
throw createError({
statusCode: 400,
statusMessage: 'No temporary session found. Please start the sign-in flow again.',
});
}
let sessionId: string;
let returnTo: string | undefined;
try {
const tempSession: Awaited<ReturnType<typeof verifyTempSessionToken>> = await verifyTempSessionToken(
tempSessionCookie,
sessionSecret,
);
sessionId = tempSession.sessionId;
returnTo = tempSession.returnTo;
} catch {
deleteCookie(event, getTempSessionCookieName(), getTempSessionCookieOptions());
throw createError({
statusCode: 400,
statusMessage: 'Invalid or expired temporary session. Please start the sign-in flow again.',
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/callback.get.ts` around
lines 67 - 88, When verification of the temp session token fails in the catch
block for verifyTempSessionToken (handling tempSessionCookie and sessionSecret),
clear the temporary session cookie before throwing the 400 createError so the
client doesn't keep retrying with the same invalid cookie; locate the catch that
wraps the verifyTempSessionToken call and add logic to remove/reset the cookie
(e.g., use the same cookie name/mechanism used to read tempSessionCookie) then
rethrow the createError.

Comment on lines +64 to +91
// ── Resolve sessionId ─────────────────────────────────────────────────────
// Priority: live session cookie → temp session cookie → new random id.
let sessionId: string;

const liveSession: Awaited<ReturnType<typeof useServerSession>> = await useServerSession(event);
if (liveSession?.sessionId) {
sessionId = liveSession.sessionId;
} else {
const tempCookie: string | undefined = getCookie(event, getTempSessionCookieName());
if (tempCookie) {
try {
const tempSession: Awaited<ReturnType<typeof verifyTempSessionToken>> = await verifyTempSessionToken(
tempCookie,
sessionSecret,
);
sessionId = tempSession.sessionId;
} catch {
// Expired / tampered — mint a fresh one below.
sessionId = generateSessionId();
}
} else {
sessionId = generateSessionId();
}

// Persist the sessionId in a temp cookie so the callback can look it up.
const tempToken: string = await createTempSessionToken(sessionId, sessionSecret);
setCookie(event, getTempSessionCookieName(), tempToken, getTempSessionCookieOptions());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Temp session ID rotates on every concurrent request without a temp cookie.

If a client sends two parallel POST /api/auth/signin calls during the embedded-flow initiation (e.g., quick double-click on "Sign In"), neither has the temp cookie yet, so both mint fresh sessionIds via generateSessionId() and each writes its own cookie (the second Set-Cookie wins). The first request's session state in client is then orphaned, and the IdP step bound to the first sessionId can never be completed.

Consider deferring setCookie until after the flow step succeeds, or atomically reusing the cookie value if it appears in event.headers between read and write.

Comment on lines +62 to +69
try {
response = await client.signUp(payload as any);
} catch (err: any) {
throw createError({
statusCode: 502,
statusMessage: `Embedded sign-up step failed: ${err?.message ?? String(err)}`,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't forward raw upstream error messages to the client.

statusMessage is returned to the caller (HTTP status line and h3's default JSON error body). Embedding err?.message ?? String(err) can disclose internal details — stack frames, hostnames, IPs from network errors, validation hints from the upstream IDP, etc. Log the full error server-side and return a generic message to the client.

🛡️ Proposed fix
   } catch (err: any) {
+    // Log full error server-side; do not echo to client.
+    // eslint-disable-next-line no-console
+    console.error('[asgardeo] Embedded sign-up step failed:', err);
     throw createError({
       statusCode: 502,
-      statusMessage: `Embedded sign-up step failed: ${err?.message ?? String(err)}`,
+      statusMessage: 'Embedded sign-up step failed.',
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
response = await client.signUp(payload as any);
} catch (err: any) {
throw createError({
statusCode: 502,
statusMessage: `Embedded sign-up step failed: ${err?.message ?? String(err)}`,
});
}
try {
response = await client.signUp(payload as any);
} catch (err: any) {
// Log full error server-side; do not echo to client.
// eslint-disable-next-line no-console
console.error('[asgardeo] Embedded sign-up step failed:', err);
throw createError({
statusCode: 502,
statusMessage: 'Embedded sign-up step failed.',
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/routes/auth/session/signup.post.ts` around
lines 62 - 69, The catch block in the signup handler currently forwards the raw
upstream error message via createError(statusMessage) after calling
client.signUp(payload), which can leak internal details; instead, log the full
error server-side (e.g., using the existing logger or console.error) and throw
createError with a generic, non-sensitive message like "Embedded sign-up step
failed" (no err.message), preserving the original err when logging for
diagnostics; update the catch surrounding client.signUp in signup.post.ts to
perform the server-side log and replace the statusMessage payload accordingly.

Comment on lines +207 to +213
const idToken: IdToken = await client.getDecodedIdToken(sessionId, tokenResponse.idToken);

const userId: string = (idToken.sub || sessionId) as string;
const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as string | undefined;
const expiresInSeconds: number = parseInt(tokenResponse.expiresIn ?? '3600', 10);
const accessTokenExpiresAt: number =
Math.floor(Date.now() / 1000) + (Number.isFinite(expiresInSeconds) ? expiresInSeconds : 3600);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the declared type of TokenResponse.expiresIn in `@asgardeo/node`.
rg -nP --type=ts -C2 'expiresIn\??\s*:'

Repository: asgardeo/javascript

Length of output: 9641


🏁 Script executed:

# First, examine the actual file being reviewed
head -n 220 packages/nuxt/src/runtime/server/utils/session.ts | tail -n 50

Repository: asgardeo/javascript

Length of output: 1896


🏁 Script executed:

# Find TokenResponse type definition and imports in nuxt package
rg -n "TokenResponse|import.*@asgardeo" packages/nuxt/src/runtime/server/utils/session.ts -B2 -A2

Repository: asgardeo/javascript

Length of output: 393


🏁 Script executed:

# Check if TokenResponse is defined in the SDK or codebase
rg -n "type TokenResponse|interface TokenResponse" packages/

Repository: asgardeo/javascript

Length of output: 271


🏁 Script executed:

# Look for OIDC validation or comments about required claims in nuxt package
rg -n "sub.*claim|OIDC|required.*claim" packages/nuxt/src/runtime/server/ -i

Repository: asgardeo/javascript

Length of output: 913


🏁 Script executed:

# Read TokenResponse interface to check expiresIn type
sed -n '27,80p' packages/javascript/src/models/token.ts

Repository: asgardeo/javascript

Length of output: 1643


🏁 Script executed:

# Check IdToken type definition
rg -n "type IdToken|interface IdToken" packages/javascript/src/ -A 20

Repository: asgardeo/javascript

Length of output: 1097


🏁 Script executed:

# Check KnownIdToken interface to see if sub is required
rg -n "interface KnownIdToken" packages/javascript/src/ -A 40

Repository: asgardeo/javascript

Length of output: 2661


🏁 Script executed:

# Also check if there's any documentation about OIDC compliance or sub claim requirement
rg -n "sub.*required|required.*sub" packages/ -i

Repository: asgardeo/javascript

Length of output: 500


Don't silently fall back userId to sessionId.

The sub claim is a required, non-optional field in the ID token (KnownIdToken.sub: string), and per OIDC compliance, should always be present. If missing, treating the opaque session ID as a user identifier will misattribute session/audit data downstream. Fail loudly instead.

🛡️ Proposed fix
-  const userId: string = (idToken.sub || sessionId) as string;
+  if (!idToken.sub) {
+    throw new Error('[asgardeo] ID token is missing the required `sub` claim.');
+  }
+  const userId: string = idToken.sub;

Also, tokenResponse.expiresIn is typed as string, so parseInt(tokenResponse.expiresIn ?? '3600', 10) is correct. However, prefer Number(tokenResponse.expiresIn ?? 3600) for cleaner numeric conversion without radix parsing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const idToken: IdToken = await client.getDecodedIdToken(sessionId, tokenResponse.idToken);
const userId: string = (idToken.sub || sessionId) as string;
const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as string | undefined;
const expiresInSeconds: number = parseInt(tokenResponse.expiresIn ?? '3600', 10);
const accessTokenExpiresAt: number =
Math.floor(Date.now() / 1000) + (Number.isFinite(expiresInSeconds) ? expiresInSeconds : 3600);
const idToken: IdToken = await client.getDecodedIdToken(sessionId, tokenResponse.idToken);
if (!idToken.sub) {
throw new Error('[asgardeo] ID token is missing the required `sub` claim.');
}
const userId: string = idToken.sub;
const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as string | undefined;
const expiresInSeconds: number = parseInt(tokenResponse.expiresIn ?? '3600', 10);
const accessTokenExpiresAt: number =
Math.floor(Date.now() / 1000) + (Number.isFinite(expiresInSeconds) ? expiresInSeconds : 3600);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/session.ts` around lines 207 - 213,
The code currently assigns userId by falling back to sessionId when idToken.sub
is missing; change this to fail loudly by validating that idToken.sub exists
(since KnownIdToken.sub is required) and throwing a clear error if it's absent
instead of using sessionId; locate the validation around the idToken, userId and
sessionId variables and replace the fallback with an explicit check/throw. Also
simplify numeric conversion for tokenResponse.expiresIn by using
Number(tokenResponse.expiresIn ?? 3600) when computing accessTokenExpiresAt
(referencing tokenResponse.expiresIn and accessTokenExpiresAt) to ensure a clean
numeric value.

Comment on lines +66 to +141
export async function getValidAccessToken(event: H3Event): Promise<string> {
const session: AsgardeoSessionPayload = await requireServerSession(event);
const now: number = Math.floor(Date.now() / 1000);

// If no expiry metadata (old session pre-Phase-2) or token still fresh, return as-is.
if (!session.accessTokenExpiresAt || session.accessTokenExpiresAt - REFRESH_SKEW_SECONDS > now) {
return session.accessToken;
}

// Token is expired (or within skew window). Attempt silent refresh.
if (!session.refreshToken) {
throw createError({
statusCode: 401,
statusMessage: 'Session expired. Please sign in again.',
});
}

const config: ReturnType<typeof useRuntimeConfig> = useRuntimeConfig(event);
const publicConfig: typeof config.public.asgardeo = config.public.asgardeo;
const privateConfig: typeof config.asgardeo = config.asgardeo;

const tokenEndpoint: string = `${publicConfig.baseUrl}/oauth2/token`;

const body: URLSearchParams = new URLSearchParams({
client_id: publicConfig.clientId,
grant_type: 'refresh_token',
refresh_token: session.refreshToken,
});

if (privateConfig?.clientSecret) {
body.set('client_secret', privateConfig.clientSecret);
}

let refreshed: OIDCTokenRefreshResponse;
try {
const res: Response = await fetch(tokenEndpoint, {
body,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
method: 'POST',
});

if (!res.ok) {
const errText: string = await res.text().catch(() => String(res.status));
throw new Error(`Token endpoint returned ${res.status}: ${errText}`);
}

refreshed = (await res.json()) as OIDCTokenRefreshResponse;
} catch (err: unknown) {
const msg: string = err instanceof Error ? err.message : String(err);
// eslint-disable-next-line no-console
console.error('[asgardeo] Token refresh failed:', msg);
throw createError({
statusCode: 401,
statusMessage: 'Token refresh failed. Please sign in again.',
});
}

// Re-issue session JWT with the refreshed tokens.
const newSessionToken: string = await createSessionToken(
{
accessToken: refreshed.access_token,
accessTokenExpiresAt: now + (refreshed.expires_in ?? 3600),
idToken: refreshed.id_token ?? session.idToken,
organizationId: session.organizationId,
refreshToken: refreshed.refresh_token ?? session.refreshToken,
scopes: refreshed.scope ?? session.scopes,
sessionId: session.sessionId,
userId: session.sub,
},
privateConfig?.sessionSecret,
);

setCookie(event, getSessionCookieName(), newSessionToken, getSessionCookieOptions());

return refreshed.access_token;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Concurrent refresh hazard with refresh-token rotation.

Two requests arriving while the access token is within the skew window will both pass the freshness check, both POST grant_type=refresh_token, and — if the IdP rotates refresh tokens (Asgardeo does by default) — exactly one wins. The losing request gets a 400 invalid_grant and the user is bounced to Token refresh failed. Please sign in again. Worse, if the loser's response races behind the winner's setCookie, you can also overwrite the freshly issued cookie with stale tokens.

Consider deduplicating in-flight refreshes with a Promise cache keyed by session.sessionId (or by session.refreshToken) so concurrent callers await the same refresh. A simple module-level Map<string, Promise<string>> cleared in a finally is enough for a single-process Nitro instance; for multi-instance deployments a short-lived shared lock would be more robust.

const inflight = new Map<string, Promise<OIDCTokenRefreshResponse>>();

const refreshPromise =
  inflight.get(session.sessionId) ??
  (async () => {
    try {
      // existing fetch + parse logic here
      return refreshed;
    } finally {
      inflight.delete(session.sessionId);
    }
  })();
inflight.set(session.sessionId, refreshPromise);
const refreshed = await refreshPromise;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/runtime/server/utils/token-refresh.ts` around lines 66 -
141, getValidAccessToken currently allows concurrent refreshes which causes
refresh-token rotation races; add deduplication by creating a module-level Map
(e.g., inflight: Map<string, Promise<OIDCTokenRefreshResponse>>) keyed by
session.sessionId (or session.refreshToken), store the in-flight refresh Promise
before making the fetch, have concurrent callers await that Promise instead of
issuing their own fetch, ensure the fetch/parse logic from the try block is run
inside that Promise and that you delete the key in a finally block, then resume
the existing logic (createSessionToken, setCookie, return
refreshed.access_token) after awaiting the shared Promise so cookie updates
cannot be overwritten by a stale result.

@kavindadimuthu kavindadimuthu force-pushed the nuxt-sdk-start-without-playground branch from db4636c to 6bae96f Compare April 27, 2026 19:19
@asgardeo-github-bot
Copy link
Copy Markdown

⚠️ No Changeset found

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go.

If these changes should result in a version bump, you need to add a changeset.

Refer Release Documentation to learn how to add a changeset.

@brionmario brionmario merged commit 9fded01 into asgardeo:main Apr 28, 2026
7 of 9 checks passed
@kavindadimuthu kavindadimuthu mentioned this pull request Apr 28, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redirect-based support for the Nuxt SDK

3 participants