diff --git a/.changeset/afraid-pans-own.md b/.changeset/afraid-pans-own.md new file mode 100644 index 00000000..59661ade --- /dev/null +++ b/.changeset/afraid-pans-own.md @@ -0,0 +1,7 @@ +--- +'@asgardeo/nextjs': patch +'@asgardeo/react': patch +--- + +- Avoid race condition in oauth callback and route protection +- Handle empty payload in sign-in action diff --git a/packages/nextjs/src/server/actions/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts index 537dd6d2..69ba6353 100644 --- a/packages/nextjs/src/server/actions/signInAction.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -20,12 +20,12 @@ import {cookies} from 'next/headers'; import { - CookieConfig, generateSessionId, EmbeddedSignInFlowStatus, EmbeddedSignInFlowHandleRequestPayload, EmbeddedFlowExecuteRequestConfig, EmbeddedSignInFlowInitiateResponse, + isEmpty, } from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; import SessionManager from '../../utils/SessionManager'; @@ -96,7 +96,7 @@ const signInAction = async ( } // If no payload provided, redirect to sign-in URL for redirect-based sign-in. - if (!payload) { + if (!payload || isEmpty(payload)) { const defaultSignInUrl = await client.getAuthorizeRequestUrl({}, sessionId); return {success: true, data: {signInUrl: String(defaultSignInUrl)}}; } diff --git a/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts b/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts index da6d91fd..c7d7a5c8 100644 --- a/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts +++ b/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts @@ -140,11 +140,43 @@ const asgardeoMiddleware = ( return async (request: NextRequest): Promise => { const resolvedOptions = typeof options === 'function' ? options(request) : options || {}; + const url: URL = new URL(request.url); + const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state'); + + let isValidOAuthCallback: boolean = false; + if (hasCallbackParams) { + // OAuth callbacks should not contain error parameters that indicate failed auth + const hasError: boolean = url.searchParams.has('error'); + + if (!hasError) { + // Validate that there's a temporary session that initiated this OAuth flow + const tempSessionToken: string | undefined = request.cookies.get( + SessionManager.getTempSessionCookieName(), + )?.value; + if (tempSessionToken) { + try { + // Verify the temporary session exists and is valid + await SessionManager.verifyTempSession(tempSessionToken); + isValidOAuthCallback = true; + } catch { + // Invalid temp session - this is not a legitimate OAuth callback + isValidOAuthCallback = false; + } + } + } + } + const sessionId = await getSessionIdFromRequestMiddleware(request); const isAuthenticated = await hasValidSession(request); const asgardeo: AsgardeoMiddlewareContext = { protectRoute: async (options?: {redirect?: string}): Promise => { + // Skip protection if this is a validated OAuth callback - let the callback handler process it first + // This prevents race conditions where middleware redirects before OAuth callback completes + if (isValidOAuthCallback) { + return; + } + if (!isAuthenticated) { const referer = request.headers.get('referer'); // TODO: Make this configurable or call the signIn() from here.