Introduce modern Asgardeo Nuxt.js SDK#477
Conversation
- 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>
… authentication handling
…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>
…orts and remove unused code
…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>
…xt SDK playground
…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>
…nd Vue SDK playgrounds
…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.
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (74)
📝 WalkthroughWalkthroughAdds a full-featured Nuxt SDK ( Changes
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🟠 MajorAvoid leaking internal error messages to the client.
Inlining
err.message(orString(err)) intostatusMessagecan 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 | 🟠 MajorDon't silently swallow
getUserfailures.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:
- Operator blindness: there's no log to diagnose why authenticated users suddenly appear signed-out in production.
- Possible hydration thrash: if the client treats
isSignedIn: falseas 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 | 🟠 MajorConfig error in production is masked as 401.
sessionSecretpassed toverifyAndRehydrateSessionis optional. When undefined,getSecret()throws an explicit error in production (ASGARDEO_SESSION_SECRET environment variable is required), but this error is caught by theverifyAndRehydrateSessiontry-catch block, returningnulland 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
sessionSecretbefore callingverifyAndRehydrateSession, 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 | 🟠 MajorPass
eventtouseRuntimeConfiginside 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, andswitch.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 thatbranding.get.tsalready 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 | 🟠 MajorAvoid leaking raw error details into
statusMessage.
statusMessageis sent as the HTTP reason phrase, which is single-line ASCII. Interpolatingerr.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.messagecontains 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 inpackages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts(lines 47–50) andpackages/nuxt/src/runtime/server/routes/auth/user/profile.patch.ts(lines 55–58) — consider centralizing this in a small helper used by allauth/*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 | 🟠 MajorDon'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) andsignin.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
returnTovalidation has bypass paths and an unsafe cast.Two concerns:
- Backslash bypass. Several browsers (notably some older Chromium/Edge builds and most user-agents that normalize backslashes) treat
/\evil.comas//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.- Type coercion.
getQuery(event)returnsRecord<string, string | string[] | undefined>; if a client sends?returnTo=/a&returnTo=/b, this cast yields an array at runtime andreturnTo.startsWiththrows. 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
decodeURIComponentcan throwURIErroron malformed inputs.If an attacker (or simply a buggy client) submits a
returnTolike/%E0abcor/%XYZ,decodeURIComponentthrowsURIError. That escapesvalidateReturnUrlas an uncaught exception (not anAsgardeoError), so:
- Callers using
validateReturnUrldirectly will see a generic 500 instead of a structuredOpenRedirectBlocked400.safeReturnUrldoes still fall back (because itscatchis untyped), but the intent — “invalid encoding ⇒ rejected as redirect attack” — is implicit and easy to break later.Wrap the decode in
try/catchand 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 | 🟠 MajorPossible token leakage via URL query params after sign-in.
handleSuccessappends every entry ofauthDataas a query parameter onafterSignInUrl. IfauthDatacarries token-shaped material (e.g.idToken,accessToken,code,state), those values end up inwindow.history, server access logs, and anyRefererheader 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 | 🟠 MajorRemove the redundant re-fetching of
config/sessionSecret.
configandsessionSecretare already in scope from lines 40–41. Re-declaring them inside the secondtryblock viaconstshadows the outer variables and re-invokesuseRuntimeConfig()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
noopASGARDEO_KEY actions silently break base components that call them directly.
clearSession,exchangeToken,getDecodedIdToken,getIdToken,signInSilently,switchOrganization, andhttp.request/requestAllall returnundefined. 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 tosignIn/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
switchOrganizationandgetDecodedIdTokenthat mirror the existingsignOutshape, 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 | 🟠 MajorMulti-valued complex attribute handling ignores extension schemas.
Two related concerns in
buildScimPatchValue:
Hardcoded list limitation: The
complexMultiValuedset (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 likecustomMultiAttr.workfall through to the nested-object path and produce an invalid{customMultiAttr: {work: ...}}PATCH that the server silently ignores.Missing schemaId wrapping: The early return at Line 159 places
[head]: [{type, value}]at the root unconditionally, even when the attribute'sschemaIdis 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 | 🟠 MajorRefresh-token POST has no timeout — can hang Nitro workers indefinitely
fetch(tokenEndpoint, ...)runs on the request path with noAbortController/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 | 🟠 MajorSingleton bakes
afterSignInUrlfrom the first requestThe
AsgardeoNuxtClientis a process-level singleton (getInstance()withisInitializedguard). In the SSR plugin,client.initialize()is called only once when!client.isInitialized(line 68), and passesafterSignInUrl: resolveCallbackUrl(event)derived from the incoming request's origin. After the first initialization,isInitializedis 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_mismatcherrors at the IdP.Pass the callback URL per-flow (resolved from the live
eventat sign-in initiation) rather than at client initialization, or configure a single canonicalafterSignInUrlin 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 | 🟠 MajorAdd per-session locking to prevent concurrent refresh-token race
Two requests for the same session arriving within the skew window (common with parallel
$fetchcalls) will both attempt to refresh using the samerefresh_token. Most OIDC providers including Asgardeo rotate refresh tokens on each use, so the second request fails withinvalid_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 | 🟠 MajorUse
validateReturnUrl()to block encoded open-redirect bypasses
returnTois 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.compasses the basic filter but decodes to//evil.com(protocol-relative URL = open redirect). The codebase already hasvalidateReturnUrl()inurl-validation.tsthat explicitly blocks encoded sequences (lines 74–80), but it is not imported or used here.Add
validateReturnUrl()to both routes:
signin.get.tsL48: Replace basic check withvalidateReturnUrl(returnTo)orsafeReturnUrl(returnTo).callback.get.tsL126: ApplyvalidateReturnUrl(returnTo)before redirect (defense-in-depth, even though JWT is signed).- If
publicConfig.afterSignInUrlis user-configurable, validate it too.Also emit raw provider error messages to logs only; sanitize for client response
Line 102 exposes
err?.messagein 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
verifySessionTokendoesn't enforcetype === 'session'— type confusion possible.
createSessionTokensetstype: 'session'(Line 78) andcreateTempSessionTokensetstype: 'temp'(Line 99), and both are signed with the same secret.verifyTempSessionTokencorrectly rejects mismatched types (Line 135-137), butverifySessionTokendoes not — meaning a valid temp-session JWT (15-minute TTL, onlysessionIdpayload) would be accepted by anything callinguseServerSession/requireServerSession/ SSR-plugin verification. The downstream code then reads fields likeaccessToken,sub,scopesthat 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 | 🟠 MajorOAuth 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 runscleanup(), but it never callshandleError,props.onError, nor emits'error'. From the parent component's perspective,handleSubmitalready returned (becausehandleRedirectionIfNeededreturnedtrueand the finally ranisLoading = 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 | 🟠 MajorGuard
new URL(props.afterSignUpUrl)against non-absolute URLs.The origin check correctly accepts both
afterSignUpUrlorigin andwindow.location.origin, matching the React SDK pattern. However,new URL()will throw a TypeError ifprops.afterSignUpUrlis 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 thatafterSignUpUrlis 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 | 🟠 MajorUse
external: falseforafterSignUpUrl, and conditionally setexternal: trueonly for absolute URLs.The
afterSignUpUrl(andoauthRedirectUrlif relative) should useexternal: falsefor client-side navigation. The sameafterSignUpUrlis correctly handled withexternal: falsein the Nuxt composable (useAsgardeo.ts), but this component incorrectly usesexternal: true, causing unnecessary full-page reloads. The server returns this URL as a relative path (e.g.,/or from config), and Nuxt'snavigateToshould handle it as an internal route by default.For the OAuth redirect case, gate the
externaloption on whether the URL starts withhttp(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 | 🟠 MajorPopup OAuth:
messageHandler/popupMonitorleak on component unmount; relativeafterSignUpUrlwill throw innew URL(...).origin.Two issues with
handleRedirectionIfNeeded:
cleanup()only runs on the OAuth-callback success/error paths and when the polling loop detectspopup.closed. If the user navigates away (route change in Nuxt), the component unmounts but themessagelistener onwindowand the 1-secondsetIntervalremain attached, holding a closure over the (now orphan) flow state. Register anonBeforeUnmountcleanup that invokes the samecleanup()for any in-flight popups.Line 346-348:
const expectedOrigin: string = props.afterSignUpUrl ? new URL(props.afterSignUpUrl).origin : window.location.origin;
props.afterSignUpUrldefaults to/in many setups (and the V2 SignUp wrapper allows arbitrary user values).new URL('/')throwsTypeError: 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 insidehandleRedirectionIfNeeded, and call it fromonBeforeUnmountand 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 | 🟠 MajorSubstring filter
!includes('oauth') && !includes('auth')is broken and will silently swallow valid redirects.Two problems:
'oauth'already contains the substring'auth', so the second predicate fully subsumes the first — the'oauth'check is dead.- The
'auth'substring matches far more than intended:/authentication/...,/author/...,/api/auth/..., evenhttps://example.com/auth-success— all valid post-signup destinations get silently skipped, leaving the user stranded after aRedirection-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
baseUrlor against the well-known authorize path.Also note: this third
ifis reachable concurrently with the second one only ifresponse?.type === EmbeddedFlowResponseType.Redirection(mutually exclusive with the second block's negation), but the missingreturnafter the second block makes that invariant non-obvious — consider addingreturnfor 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
getInputNameprecedence contradicts its own documentation—fix the fallback order.The comment on lines 41–42 states that
config.identifieris the bound parameter name (e.g.http://wso2.org/claims/emailaddress) withconfig.nameas a fallback. The implementation on lines 44–47 does the opposite: it checkscfg.namefirst and only falls back tocfg.identifier.This same bug also exists in the Vue
BaseSignUp.tscomponent'scollectInputNames()function, which uses these field names to key the form submission payload. If the server's V1 contract expects inputs keyed byconfig.identifier(the SCIM claim URI) but the client sends them underconfig.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.tsline 286 in thecollectInputNames()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
isInitializedis set even whenlegacy.initializefails.If the legacy client returns
false(or initialization is partial),this.isInitialized = trueis still committed unconditionally. Subsequent calls then short-circuit at Line 111-113 and returntrue, 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_infallback masks already-expired tokens.At Line 159-162,
expiresInSecondscorrectly clamps to0for an expired session viaMath.max(0, ...). But Line 170 then writesString(expiresInSeconds || 3600), which treats0as falsy and reports a fresh 3600s window to the legacy storage manager. DownstreamgetAccessTokenlogic that consultsexpires_inagainstcreated_atwill 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
accessTokenExpiresAtis genuinely absent, while honoring an explicit0(expired) for sessions whoseaccessTokenExpiresAtis 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.
| 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}); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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=tsRepository: 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 -1Repository: 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.tsRepository: asgardeo/javascript
Length of output: 124
🏁 Script executed:
# Find the AsgardeoAuthState type definition
rg -n 'type AsgardeoAuthState|interface AsgardeoAuthState' -A 10Repository: asgardeo/javascript
Length of output: 363
🏁 Script executed:
# Search for organizationId usage to see where it comes from
rg -n 'organizationId' -B 2 -A 2Repository: asgardeo/javascript
Length of output: 50377
🏁 Script executed:
# Search for scopes attribute on user object
rg -n '"scopes"|\'scopes\'' -B 2 -A 2Repository: asgardeo/javascript
Length of output: 132
🏁 Script executed:
rg -n 'type User|interface User' -A 15 packages/nuxt/src/runtime/types.tsRepository: 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 -80Repository: 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 2Repository: 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.tsRepository: 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 10Repository: 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 importRepository: 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.tsRepository: 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 -100Repository: 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 -20Repository: 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 3Repository: 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.tsRepository: 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 -5Repository: 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 -3Repository: 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.tsRepository: 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 -80Repository: 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 -15Repository: 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 15Repository: 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 3Repository: 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 -20Repository: 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 -40Repository: 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 10Repository: 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 -30Repository: 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: trueto reject all authenticated users (even those with a valid org)requireScopeschecks 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.
| 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}`}, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 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 -10Repository: 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 -100Repository: asgardeo/javascript
Length of output: 8508
🏁 Script executed:
cat packages/nextjs/src/server/actions/getAllOrganizations.tsRepository: asgardeo/javascript
Length of output: 1638
🏁 Script executed:
cat packages/javascript/src/api/getAllOrganizations.tsRepository: 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?: stringlimit?: 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.
| 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)}; | ||
| } |
There was a problem hiding this comment.
🧩 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/srcRepository: 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/runtimeRepository: 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 -20Repository: 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'; |
There was a problem hiding this comment.
🧩 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/srcRepository: 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 -20Repository: 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.tsRepository: 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 -5Repository: 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.tsRepository: 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.
| 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}, | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
a89ee00 to
90e4b65
Compare
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (14)
packages/nuxt/src/runtime/errors/asgardeo-error.ts (2)
39-67: Consider wideningcodetyping or accepting a generic to allow consumer-defined error codes.
code: ErrorCodetightly couplesAsgardeoErrorto the SDK-internal enum. If downstream consumers (or future SDK extensions like SCIM2/organization plugins) need to throwAsgardeoErrorwith a code outside this enum, they'd have to either extend the enum or cast. A common pattern for SDK error classes iscode: 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
ErrorCodetyping 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 nativeErrorconstructor withcauseoption and conditionally assign optional fields.The refactoring improves code quality in two ways:
Native
causeintegration: Passcauseto theErrorconstructor rather than assigning it afterward. Since your project targetsESNextand uses TypeScript 5.8.3, the ES2022Errorconstructor with{ cause }option is available. This integrates with engine-level formatting and makes the error chain more idiomatic.Clean property shape: Unconditionally assigning
this.statusCode = opts?.statusCodeandthis.context = opts?.contextcreates own properties withundefinedvalues 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
useServerSessionwon't naturally clear it. For genuine "Please sign in again" responses (especiallyinvalid_grantfrom the IdP), it would be cleaner todeleteCookie(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: UseSetupContextinstead of inlineanytypes forslots/emit.Vue exposes
SetupContext(already used in the siblingUserProfile.ts) which gives proper typing forslotsandemit. 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.
signOutis async (it performs a redirect vianavigateTo). Not awaiting it means any error during the sign-out flow is swallowed and never surfaces in console/error reporting. Eithervoid 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: BothclassandclassNameare forwarded — confirm BaseUserProfile uses one consistently.
classis Vue's native attribute (will be merged onto the root element automatically) whileclassNameis being passed as a Vue prop. IfBaseUserProfilere-appliesclassNameas anotherclassbinding internally, you may end up with the styled prefix duplicated or the consumer-suppliedclassNameoverriding the vendor class. Worth confirming the component contract; otherwise either dropclassNamehere or merge them into a singleclassarray.🤖 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 theeventtouseRuntimeConfigfor 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.joseitself 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, andswitch.post.ts. Pulling this into a helper (e.g.requireSession(event)returningAsgardeoSessionPayloador 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
signInmethod 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. TheAsgardeoNuxtClientalready exposesgetAuthorizeRequestUrl()as a direct alternative (used insignin.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 theas anycasts.
(await signIn({flowId: ''} as any, {} as any))and similar inhandleOnSubmitcould be tightened by typing the composable'ssignInoverloads (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: trueforces a full reload even for same-originsignUpUrl.If a consumer configures
signUpUrlas 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 whensignUpUrlis a Nuxt route. Consider detecting same-origin / leading-slash paths and usingexternal: falsefor 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 therequesthook. Two concurrent first requests can both readisInitialized === false, both callclient.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 returnsuccess: true.The code explicitly handles only
SuccessCompleted(line 117). Other statuses fall through to the final return on line 150, including terminal-failure statesFailCompletedandFailIncomplete, which incorrectly return{data: response, success: true}. This forces clients to inspectflowStatuswithin the response data to detect failures rather than relying on thesuccessfield.Consider explicitly branching on
FailCompletedandFailIncompleteto returnsuccess: 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (56)
packages/nuxt/package.jsonpackages/nuxt/src/index.tspackages/nuxt/src/module.tspackages/nuxt/src/runtime/components/AsgardeoRoot.tspackages/nuxt/src/runtime/components/actions/SignInButton.tspackages/nuxt/src/runtime/components/actions/SignOutButton.tspackages/nuxt/src/runtime/components/actions/SignUpButton.tspackages/nuxt/src/runtime/components/auth/Callback.tspackages/nuxt/src/runtime/components/auth/SignIn.tspackages/nuxt/src/runtime/components/auth/SignUp.tspackages/nuxt/src/runtime/components/control/Loading.tspackages/nuxt/src/runtime/components/control/SignedIn.tspackages/nuxt/src/runtime/components/control/SignedOut.tspackages/nuxt/src/runtime/components/organization/CreateOrganization.tspackages/nuxt/src/runtime/components/organization/Organization.tspackages/nuxt/src/runtime/components/organization/OrganizationList.tspackages/nuxt/src/runtime/components/organization/OrganizationProfile.tspackages/nuxt/src/runtime/components/organization/OrganizationSwitcher.tspackages/nuxt/src/runtime/components/user/User.tspackages/nuxt/src/runtime/components/user/UserDropdown.tspackages/nuxt/src/runtime/components/user/UserProfile.tspackages/nuxt/src/runtime/composables/useAsgardeo.tspackages/nuxt/src/runtime/errors/asgardeo-error.tspackages/nuxt/src/runtime/errors/error-codes.tspackages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.tspackages/nuxt/src/runtime/plugins/asgardeo.tspackages/nuxt/src/runtime/server/AsgardeoNuxtClient.tspackages/nuxt/src/runtime/server/plugins/asgardeo-ssr.tspackages/nuxt/src/runtime/server/routes/auth/branding/branding.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/current.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/id.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/index.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/index.post.tspackages/nuxt/src/runtime/server/routes/auth/organizations/me.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.tspackages/nuxt/src/runtime/server/routes/auth/session/callback.get.tspackages/nuxt/src/runtime/server/routes/auth/session/callback.post.tspackages/nuxt/src/runtime/server/routes/auth/session/session.get.tspackages/nuxt/src/runtime/server/routes/auth/session/signin.get.tspackages/nuxt/src/runtime/server/routes/auth/session/signin.post.tspackages/nuxt/src/runtime/server/routes/auth/session/signout.post.tspackages/nuxt/src/runtime/server/routes/auth/session/signup.post.tspackages/nuxt/src/runtime/server/routes/auth/session/token.get.tspackages/nuxt/src/runtime/server/routes/auth/user/profile.get.tspackages/nuxt/src/runtime/server/routes/auth/user/profile.patch.tspackages/nuxt/src/runtime/server/routes/auth/user/user.get.tspackages/nuxt/src/runtime/server/utils/event-context.tspackages/nuxt/src/runtime/server/utils/serverSession.tspackages/nuxt/src/runtime/server/utils/session.tspackages/nuxt/src/runtime/server/utils/token-refresh.tspackages/nuxt/src/runtime/types.tspackages/nuxt/src/runtime/types/augments.d.tspackages/nuxt/src/runtime/utils/createRouteMatcher.tspackages/nuxt/src/runtime/utils/log.tspackages/nuxt/src/runtime/utils/url-validation.tspackages/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
| // Register named route middleware for page protection | ||
| addRouteMiddleware({ | ||
| name: 'auth', | ||
| path: resolve('./runtime/middleware/auth'), | ||
| }); |
There was a problem hiding this comment.
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.
| // 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.
| 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; | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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}; | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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).
| 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}); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Two concerns in the post-completion redirect logic.
-
external: trueonafterSignUpUrlis wrong for in-app destinations.afterSignUpUrlis typically an internal route (the whole point of the post-sign-up redirect is to land the user in the app). Forcingexternal: truetriggers a full-document navigation and discards the Nuxt SPA state. Useexternal: false(or just omit the option) so client-side routing works. -
includes('auth') / includes('oauth')is too coarse. The substring match will reject anyredirectURLwhose host or path happens to contain the lettersauth(e.g.authortools.example.com,/author/123,?author=jane). Match the IdP origin explicitly (e.g. compare againstuseRuntimeConfig().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.
| 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).
| const handleSelect = async (org: IOrganization): Promise<void> => { | ||
| emit('select', org); | ||
| await switchOrganization(org); | ||
| }; |
There was a problem hiding this comment.
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.
| export default defineEventHandler(async (event: H3Event) => { | ||
| const accessToken: string = await getValidAccessToken(event); | ||
| return {accessToken}; | ||
| }); |
There was a problem hiding this comment.
🧩 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/srcRepository: 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 tsRepository: 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/routesRepository: 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.tsRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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.', | ||
| }); | ||
| } |
There was a problem hiding this comment.
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).
| 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}$`); | ||
| }); |
There was a problem hiding this comment.
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.
b7797c6 to
37adcd9
Compare
There was a problem hiding this comment.
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 | 🟠 MajorOpen-redirect bypass:
returnTovalidation does not block backslash or whitespace-prefixed paths.
/\evil.compassesstartsWith('/') && !startsWith('//')but browsers normalize\to/in theLocationpath, redirecting tohttps://evil.com(Saltcorn GHSA-f3g8-9xv5-77gv, Vue Router#3946). Reject backslashes, control characters, whitespace, and other protocol-relative variants. The storedreturnToshould also be re-validated where it is consumed incallback.get.ts(line 126) beforesendRedirect, 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 | 🟠 MajorBlanket catch silently signs out users on transient
getUserfailures.When
verifyAndRehydrateSessionreturns a non-null session, the cookie is cryptographically valid. Any error fromgetUser(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 inpackages/nuxt/src/runtime/server/plugins/asgardeo-ssr.ts(which keepsisSignedIn: trueongetUserfailure viaPromise.allSettled). Either keepisSignedIn: truewithuser: 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 | 🟠 MajorState (CSRF) is not explicitly validated at the endpoint; original state isn't persisted in the temp JWT.
The endpoint accepts
statefrom the request body and forwards it toclient.signIn(...). The temp session JWT carries onlysessionId(and optionallyreturnTo) perpackages/nuxt/src/runtime/server/utils/session.ts:92-109— there's no stored originalstateto 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
stateinto the temp session JWT at sign-in time, return it fromverifyTempSessionToken, and assertbody.state === tempSession.statehere before callingsignIn.🤖 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 | 🟡 MinorError responses inconsistent with
callback.get.ts.
callback.get.tsthrowscreateError({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 mapissueSessionCookiefailures to a 5xx viacreateError.🤖 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 | 🟡 MinorAvoid echoing raw upstream error text in
statusMessage/data.
err?.messagemay contain internal details (IdP responses, stack fragments) and is surfaced directly to the client throughdataand 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 | 🟡 MinorAvoid surfacing raw upstream error text in
statusMessage.Same concern as in
profile.patch.ts: the upstream error text is reflected back to the client viastatusMessage. 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 | 🟡 MinorBranch reachability: storedData missing + OAuth error.
When
storedDatais 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 onoauthErrorbeing 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 | 🟡 MinorAvoid leaking raw upstream error messages in
statusMessage.
statusMessageis 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 indataif 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 | 🟡 MinorDefense in depth: re-validate
returnTobefore redirect.
returnTowas validated when the temp JWT was created insignin.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 sameisSafeReturnTovalidator here beforesendRedirect, and fall through toafterSignInUrlwhen 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.parseexceptions surface aserror_descriptionquery strings.If
sessionStorageis corrupt or written by a different version (storedDatanot valid JSON, or missingpath/timestamp), the outercatchon line 157 will receive aSyntaxErrorand the raw parser message ("Unexpected token … in JSON at position 0") is written into the URL viaerror_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
verifySessionTokendoesn't enforcetype === 'session'— asymmetric withverifyTempSessionToken.
createSessionTokenstampstype: 'session'(line 80) andverifyTempSessionTokenrejects anything nottype: 'temp'(lines 130–132), butverifySessionTokenaccepts 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 toAsgardeoSessionPayload, leaving downstream code to operate on a payload withoutaccessToken/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 | 🟡 MinorValidate
stateis present, not justcode.Line 119 only rejects when
codeis missing, but a successful OAuth code-flow response is required to include bothcodeandstate(state is the CSRF binding). Forwardingstate: undefinedintoclient.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 | 🟡 MinorPopup 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 noerrorevent 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'sfinally), with no indication of what happened or how to retry. Consider emitting an error / adding a flow message in thepopup.closed && !processedbranch 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 | 🟡 MinorClient-side
isLoading=falsewithout SSR verification causes authenticated users to appear signed-out in SPA or non-SSR deployments.Line 124 unconditionally sets
isLoadingtofalseon the client side regardless of whether SSR hydration actually populated the auth state. In deployments withssr: false(global SPA mode), hybrid rendering withssr: falseroute rules, ornuxt generate(static generation), the SSR hydration block at lines 92–121 will not run, leavingauthStateat 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
revalidateProfileandrevalidateBrandingonly commit the new value when the response is truthy. If the server now returnsnull(e.g. a tenant clearing its branding, orfetchUserProfilebeing toggled off in a follow-up call), local state silently stays stale rather than reflecting the new "no data" reality. Mirror whatrevalidateMyOrganizations/revalidateCurrentOrganizationdo (assignres ?? <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
reInitializediscards the legacy client's result and always returnstrue.Mirrors the same anti-pattern flagged on
initialize: callers can't distinguish a successful reconfiguration from a failed one. Forward the boolean fromlegacy.reInitialize(and keepisInitializedin 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 | 🟡 MinorNon-submit buttons still fire
onSubmitand are not disabled when the form is invalid.
handleClickunconditionally callsonSubmit(component, undefined)for every BUTTON variant, butdisabledonly blocks the click whencfg.type === 'submit'. So a "Cancel" / "Back" / secondary action button withcfg.type !== 'submit'will (a) trigger the form-submit handler inBaseSignUpand (b) do so even whileisFormValid === false. ThehandleSubmithandler inBaseSignUp.tsconfirms 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
getInputNamepriority contradicts its JSDoc.The implementation returns
cfg.name || cfg.identifier || component.id, but the JSDoc states "V1 stores the bound parameter name inconfig.identifier… withconfig.nameused as a fallback" — the reverse order. This matters: ifconfig.identifiercontains the authoritative bound-claim reference (e.g.http://wso2.org/claims/emailaddress) andconfig.nameis 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 currentname-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 treatsnameas 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 | 🟡 MinorRemove the
as anycast —ThemeProviderfully supports allAsgardeoNuxtConfig.preferences.theme.modevalues.
AsgardeoNuxtConfig.preferences.theme.modedeclares'light' | 'dark' | 'system' | 'class' | 'branding', andThemeProvideraccepts exactly that (asThemeMode | 'branding'whereThemeMode = 'light' | 'dark' | 'system' | 'class'). All five modes are explicitly handled inThemeProvider.setup()anddetectThemeMode(). 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
catchdiscards the original error fromclient.getUser, so an upstream token-refresh / SCIM2 / network failure surfaces as a context-free 500. Capture the cause (h3 supportscauseoncreateError) 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.
TokenExchangeFailedandTokenRefreshFailedcarry theoauth/prefix but appear under theSessiongroup 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 upstreamFlattenedSchematype instead of casting throughunknown.Casting
schemas(typedSchema[]) toFlatSchemaEntry[]viaunknownis a type-safety hole, and the comment already calls out the root cause: the publishedFlattenedSchemaextendsSchemainstead ofSchemaAttribute. Worth fixing in@asgardeo/browserso the prop can be typed asFlattenedSchema[](or(SchemaAttribute & {schemaId: string})[]) here, dropping both the cast and the localFlatSchemaEntryworkaround. 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: Removeaddresses,entitlements,roles,x509CertificatesfromcomplexMultiValuedset, or drive from schema.The set incorrectly assumes all entries use the
{type, value}sub-attribute structure. OnlyphoneNumbers,emails,ims, andphotoshave this shape. Foraddresses, the SCIM2 core schema defines sub-attributes asstreetAddress,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
setNestedPathand produce at least syntactically scoped objects.- Longer term: Drive this from the schema (detect
multiValued && type === 'COMPLEX'from the matchingFlatSchemaEntry) so custom extension attributes also work correctly, instead of a hardcoded SCIM-core list.Note:
phoneNumbers.mobileis 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 thanas any.
(response as any)?.redirectUrlworks but defeats the type system. If the SDK already exposes aRedirectionresponse variant viaEmbeddedFlowResponseType.Redirection(used at line 94), narrow onresponse.type === EmbeddedFlowResponseType.Redirectionand read the typedredirectUrlfrom that branch instead. Also note: whenoauthRedirectUrlis truthy butshouldRedirectAfterSignUpis false, the earlyreturnon line 89 is skipped, but the second branch at lines 92–100 still requiresresponse.type !== Redirection— so that's fine; just clean up theany.🤖 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: redundantgetInstance()call and reusable client.
AsgardeoNuxtClient.getInstance()is invoked once inside the try (line 52, only whensession.organizationIdis falsy) and again at line 64 unconditionally. Hoisting the client to a singleconstat the top of the handler is cleaner and avoids the implicit assumption thatgetInstance()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 repeatedURLSearchParamsconstruction.The
error/error_descriptionquery-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 duplicatesplugins/asgardeo.ts.Lines 145–174 here are nearly identical to the redirect-flow
signUpinpackages/nuxt/src/runtime/plugins/asgardeo.ts(lines 154–175): samesignUpUrloverride →getRedirectBasedSignUpUrl→ fallback chain, just with different fallback targets (/sign-upvs./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 literaltype: 'session'discriminator toAsgardeoSessionPayload.
createSessionToken(inpackages/nuxt/src/runtime/server/utils/session.ts, context snippet 1) embedstype: 'session'into the signed JWT body, andAsgardeoTempSessionPayloadalready declarestype: 'temp'. Mirroring that onAsgardeoSessionPayloadwould 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, …)vssignIn(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 inAsgardeoNextClient; 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
📒 Files selected for processing (52)
packages/nuxt/src/index.tspackages/nuxt/src/module.tspackages/nuxt/src/runtime/components/AsgardeoRoot.tspackages/nuxt/src/runtime/components/actions/SignInButton.tspackages/nuxt/src/runtime/components/actions/SignOutButton.tspackages/nuxt/src/runtime/components/actions/SignUpButton.tspackages/nuxt/src/runtime/components/auth/Callback.tspackages/nuxt/src/runtime/components/auth/SignIn.tspackages/nuxt/src/runtime/components/auth/SignUp.tspackages/nuxt/src/runtime/components/control/Loading.tspackages/nuxt/src/runtime/components/control/SignedIn.tspackages/nuxt/src/runtime/components/control/SignedOut.tspackages/nuxt/src/runtime/components/organization/CreateOrganization.tspackages/nuxt/src/runtime/components/organization/Organization.tspackages/nuxt/src/runtime/components/organization/OrganizationList.tspackages/nuxt/src/runtime/components/organization/OrganizationProfile.tspackages/nuxt/src/runtime/components/organization/OrganizationSwitcher.tspackages/nuxt/src/runtime/components/user/User.tspackages/nuxt/src/runtime/components/user/UserDropdown.tspackages/nuxt/src/runtime/components/user/UserProfile.tspackages/nuxt/src/runtime/composables/useAsgardeo.tspackages/nuxt/src/runtime/errors/asgardeo-error.tspackages/nuxt/src/runtime/errors/error-codes.tspackages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.tspackages/nuxt/src/runtime/plugins/asgardeo.tspackages/nuxt/src/runtime/server/AsgardeoNuxtClient.tspackages/nuxt/src/runtime/server/plugins/asgardeo-ssr.tspackages/nuxt/src/runtime/server/routes/auth/branding/branding.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.tspackages/nuxt/src/runtime/server/routes/auth/session/callback.get.tspackages/nuxt/src/runtime/server/routes/auth/session/callback.post.tspackages/nuxt/src/runtime/server/routes/auth/session/session.get.tspackages/nuxt/src/runtime/server/routes/auth/session/signin.get.tspackages/nuxt/src/runtime/server/routes/auth/session/signin.post.tspackages/nuxt/src/runtime/server/routes/auth/session/signout.post.tspackages/nuxt/src/runtime/server/routes/auth/session/token.get.tspackages/nuxt/src/runtime/server/routes/auth/user/profile.patch.tspackages/nuxt/src/runtime/server/routes/auth/user/user.get.tspackages/nuxt/src/runtime/server/utils/event-context.tspackages/nuxt/src/runtime/server/utils/serverSession.tspackages/nuxt/src/runtime/server/utils/session.tspackages/nuxt/src/runtime/server/utils/token-refresh.tspackages/nuxt/src/runtime/types.tspackages/nuxt/src/runtime/types/augments.d.tspackages/nuxt/src/runtime/utils/createRouteMatcher.tspackages/nuxt/src/runtime/utils/log.tspackages/nuxt/src/runtime/utils/url-validation.tspackages/vue/src/components/presentation/sign-up/v1/BaseSignUp.tspackages/vue/src/components/presentation/sign-up/v1/SignUp.tspackages/vue/src/components/presentation/sign-up/v1/options/SignUpOptionFactory.tspackages/vue/src/components/presentation/user-profile/BaseUserProfile.tspackages/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
| const navigate = (path: string): void => { | ||
| if (props.onNavigate) { | ||
| props.onNavigate(path); | ||
| } else { | ||
| // navigateTo handles both SSR (sets redirect response) and client (pushState). | ||
| navigateTo(path); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 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:
- 1: https://nuxt.com/docs/3.x/api/utils/navigate-to
- 2: https://www.mintlify.com/nuxt/nuxt/api/utils/navigate-to
- 3: https://nuxt.com/docs/3.x/api/composables/use-router
- 4: https://v3.router.vuejs.org/guide/advanced/navigation-failures.html
🏁 Script executed:
cd packages/nuxt && find . -name "Callback.ts" -type f 2>/dev/null | head -5Repository: 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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/asgardeo/asgardeo-auth-react-sdk/blob/master/API.md
- 2: https://github.com/asgardeo/asgardeo-auth-react-sdk/blob/main/API.md
- 3: https://github.com/asgardeo/asgardeo-auth-js-core/blob/master/README.md
- 4: https://wso2.com/asgardeo/docs/references/app-settings/oidc-settings-for-app
🏁 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 -20Repository: 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 -10Repository: 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 -50Repository: 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 -100Repository: 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.
| if (!tokenResponse?.accessToken && !tokenResponse?.idToken) { | ||
| throw createError({ | ||
| statusCode: 500, | ||
| statusMessage: 'Token exchange failed: Invalid response from Identity Provider.', | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 -250Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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 -150Repository: 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 10Repository: 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.
| 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); |
There was a problem hiding this comment.
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
urlin the body to an attacker-controlled origin, causing the server to send the user'sBearer <accessToken>to that URL — token exfiltration / SSRF. - A caller can also inject
headers(andfetcheris 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.
| 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.
| // 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}, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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).
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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=tsRepository: 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 -nRepository: 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.jsonRepository: 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.
| 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; | ||
| }; |
There was a problem hiding this comment.
🧩 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/nullRepository: 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 -20Repository: 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 -100Repository: 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/nullRepository: 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 -nRepository: 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 -nRepository: 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 -80Repository: 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 -100Repository: 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 2Repository: 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 -5Repository: 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 -nRepository: 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.
| 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).
There was a problem hiding this comment.
Actionable comments posted: 12
♻️ Duplicate comments (6)
packages/nuxt/src/runtime/server/routes/auth/session/signin.get.ts (1)
44-46:⚠️ Potential issue | 🟠 MajorOpen-redirect bypass via backslash still unaddressed.
/\evil.compasses the currentstartsWith('/') && !startsWith('//')check, but WHATWG-compliant browsers (Chrome/Edge) normalize\to/in theLocationheader path, treating it as//evil.comand redirecting cross-origin (see Saltcorn GHSA-f3g8-9xv5-77gv, Vue Router#3946). Whitespace and control characters are also not rejected. The persistedreturnTois consumed incallback.get.tswithout re-validation beforesendRedirect.🛡️ 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
isSafeReturnTocheck at the consumption site incallback.get.tsbeforesendRedirect, 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 | 🟠 MajorEnforce the documented 32-character minimum for
ASGARDEO_SESSION_SECRETin 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 | 🔴 CriticalCritical: SSRF / token-exfiltration risk via untrusted request body — still unaddressed.
readBody<UpdateMeProfileConfig>(event)accepts the entireUpdateMeProfileConfigshape from the client, andAsgardeoNuxtClient.updateUserProfilethen spreads it intoupdateMeProfile({...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 SCIMpayloadfield 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 | 🟡 MinorStill 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 anAbortSignal.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 | 🟠 MajorCatch-all still signs valid sessions out on transient
getUserfailures — issue persists.When
verifyAndRehydrateSessionreturns a non-null session, the cookie is cryptographically valid; mapping anygetUsererror (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 inasgardeo-ssr.ts(Promise.allSettled — keepsisSignedIn: 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 | 🟠 MajorToken-response validation still permits sessions with only an
idToken.The condition
!tokenResponse?.accessToken && !tokenResponse?.idTokenonly fails when both are missing. When the IdP returns just anidToken, this passes,issueSessionCookiestoresaccessToken: undefined, and every downstream API route (getUserProfile,getMyOrganizations,getAllOrganizations, etc.) that callsgetAccessToken()will produce a malformedAuthorization: Bearer undefinedheader. For a redirect-based authorization-code flow the absence ofaccessTokenis 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 onsignup.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 theas anycast—proper types are already defined in the module augmentations.
PublicRuntimeConfig.asgardeois already properly typed inpackages/nuxt/src/module.tswithafterSignInUrlandafterSignOutUrlas strings. The cast breaks that type safety. Simplify to:const afterSignUpUrl: string = (config.public.asgardeo?.afterSignInUrl) || '/';Same applies to
payload as anyon line 63—use the SDK's exportedEmbeddedFlowExecuteRequestPayloadtype 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 theas anycast — extend thepublicConfigtype instead.
(publicConfig as any).afterSignOutUrldefeats type-checking on a config field that is otherwise referenced statically (e.g.,callback.get.tsreadspublicConfig.afterSignInUrlwithout a cast). IfafterSignOutUrlis 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?: stringto 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 atokenEndpointoverride in module config.The token endpoint is currently hardcoded as
${publicConfig.baseUrl}/oauth2/tokenat line 87. For deployments with custom token paths (e.g., gateway prefixes, OIDC discovery overrides), this breaks silently. Add an optionaltokenEndpointconfig field that falls back to the current default, matching the pattern used forsignInUrlandsignUpUrl.🤖 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 thegenerateFlattenedUserProfilemock returns a known shape (Line 53:{email: 'updated@example.com'}), assert against that explicitly so a future regression that setsflattenedProfileto something accidental (e.g.,prev.flattenedProfileinstead 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 passprev.schemas ?? []togenerateFlattenedUserProfile.If
prevhas been seeded from a partial SSR payload or written by a third party that omittedschemas,prev.schemascould beundefined. Perpackages/javascript/src/utils/generateFlattenedUserProfile.ts:55-140the function callsprocessedSchemas.map(...)unconditionally and will throwCannot 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:defuwill 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, anddefustripsundefined, the precedence is correct for present env vars. But a developer reading "highest wins" may setNUXT_PUBLIC_ASGARDEO_AFTER_SIGN_IN_URL=""(empty string) expecting it to clear the user-config value — insteaddefuwill 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 ontoruntimeConfig.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 typedNitroAppinstead of{hooks: {hook: Function}}.
@typescript-eslint/no-unsafe-function-typeflags bareFunction, and you lose call-site type safety on hook arguments. ImportNitroAppfromnitropack(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
📒 Files selected for processing (50)
packages/nuxt/src/module.tspackages/nuxt/src/runtime/components/AsgardeoRoot.tspackages/nuxt/src/runtime/components/actions/SignInButton.tspackages/nuxt/src/runtime/components/actions/SignOutButton.tspackages/nuxt/src/runtime/components/actions/SignUpButton.tspackages/nuxt/src/runtime/components/auth/SignIn.tspackages/nuxt/src/runtime/components/user/UserDropdown.tspackages/nuxt/src/runtime/composables/useAsgardeo.tspackages/nuxt/src/runtime/middleware/defineAsgardeoMiddleware.tspackages/nuxt/src/runtime/plugins/asgardeo.tspackages/nuxt/src/runtime/server/AsgardeoNuxtClient.tspackages/nuxt/src/runtime/server/plugins/asgardeo-ssr.tspackages/nuxt/src/runtime/server/routes/auth/branding/branding.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/current.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/id.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/index.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/index.post.tspackages/nuxt/src/runtime/server/routes/auth/organizations/me.get.tspackages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.tspackages/nuxt/src/runtime/server/routes/auth/session/callback.get.tspackages/nuxt/src/runtime/server/routes/auth/session/callback.post.tspackages/nuxt/src/runtime/server/routes/auth/session/session.get.tspackages/nuxt/src/runtime/server/routes/auth/session/signin.get.tspackages/nuxt/src/runtime/server/routes/auth/session/signin.post.tspackages/nuxt/src/runtime/server/routes/auth/session/signout.post.tspackages/nuxt/src/runtime/server/routes/auth/session/signup.post.tspackages/nuxt/src/runtime/server/routes/auth/session/token.get.tspackages/nuxt/src/runtime/server/routes/auth/user/profile.get.tspackages/nuxt/src/runtime/server/routes/auth/user/profile.patch.tspackages/nuxt/src/runtime/server/routes/auth/user/user.get.tspackages/nuxt/src/runtime/server/utils/serverSession.tspackages/nuxt/src/runtime/server/utils/session.tspackages/nuxt/src/runtime/server/utils/token-refresh.tspackages/nuxt/src/runtime/utils/createRouteMatcher.tspackages/nuxt/src/runtime/utils/log.tspackages/nuxt/src/runtime/utils/url-validation.tspackages/nuxt/tests/__mocks__/nuxt-app.tspackages/nuxt/tests/unit/asgardeo-root.test.tspackages/nuxt/tests/unit/asgardeo-ssr.test.tspackages/nuxt/tests/unit/callback-post.test.tspackages/nuxt/tests/unit/define-asgardeo-middleware.test.tspackages/nuxt/tests/unit/error-codes.test.tspackages/nuxt/tests/unit/route-matcher.test.tspackages/nuxt/tests/unit/session-manager.test.tspackages/nuxt/tests/unit/signin-post.test.tspackages/nuxt/tests/unit/signup-post.test.tspackages/nuxt/tests/unit/token-refresh.test.tspackages/nuxt/tests/unit/url-validation.test.tspackages/nuxt/tsconfig.eslint.jsonpackages/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
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| if (res.data?.afterSignUpUrl) { | ||
| await navigateTo(res.data.afterSignUpUrl as string, {external: false}); | ||
| return undefined; |
There was a problem hiding this comment.
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.
| 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.
| // 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}); | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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, | ||
| }); |
There was a problem hiding this comment.
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.warnin dev (import.meta.dev) so developers see they're using an unsupported method in Nuxt mode, and - Prefer wiring
getDecodedIdToken/getIdToken/http.requestto 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.
| 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.', | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // ── 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()); | ||
| } |
There was a problem hiding this comment.
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.
| 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)}`, | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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); |
There was a problem hiding this comment.
🧩 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 50Repository: 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 -A2Repository: 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/ -iRepository: asgardeo/javascript
Length of output: 913
🏁 Script executed:
# Read TokenResponse interface to check expiresIn type
sed -n '27,80p' packages/javascript/src/models/token.tsRepository: asgardeo/javascript
Length of output: 1643
🏁 Script executed:
# Check IdToken type definition
rg -n "type IdToken|interface IdToken" packages/javascript/src/ -A 20Repository: 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 40Repository: 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/ -iRepository: 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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
db4636c to
6bae96f
Compare
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. |
Purpose
This pull request introduces modern Nuxt.Js SDK with nuxt 3 for asgardeo integrations as
@asgardeo/nuxtpackage, 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:
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.package.jsonfor@asgardeo/nuxtto expose new exports for server, errors, and utils, added new dependencies and peer dependencies, and bumped the version to0.1.0-alpha.0. [1] [2] [3] [4]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:
updateMeProfileso that user profile updates now strip the userstore prefix fromuserName, ensuring UI consistency after optimistic updates. [1] [2]generateFlattenedUserProfileutility 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
Checklist
Security checks
Summary by CodeRabbit