diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index 8570b0ba404..689f765e6a2 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -4,6 +4,7 @@ export const API_VERSION = 'v1'; export const USER_AGENT = `${PACKAGE_NAME}@${PACKAGE_VERSION}`; export const MAX_CACHE_LAST_UPDATED_AT_SECONDS = 5 * 60; export const SUPPORTED_BAPI_VERSION = '2025-04-10'; +export const SUPPORTED_HANDSHAKE_FORMAT = 'nonce'; const Attributes = { AuthToken: '__clerkAuthToken', @@ -21,6 +22,7 @@ const Cookies = { Handshake: '__clerk_handshake', DevBrowser: '__clerk_db_jwt', RedirectCount: '__clerk_redirect_count', + HandshakeFormat: '__clerk_handshake_format', HandshakeNonce: '__clerk_handshake_nonce', } as const; @@ -33,9 +35,9 @@ const QueryParameters = { Handshake: Cookies.Handshake, HandshakeHelp: '__clerk_help', LegacyDevBrowser: '__dev_session', - HandshakeReason: '__clerk_hs_reason', - HandshakeNonce: Cookies.HandshakeNonce, HandshakeFormat: 'format', + HandshakeNonce: Cookies.HandshakeNonce, + HandshakeReason: '__clerk_hs_reason', } as const; const Headers = { diff --git a/packages/backend/src/tokens/__tests__/handshake.test.ts b/packages/backend/src/tokens/__tests__/handshake.test.ts index 51c26f45ba1..fe2eb32a70c 100644 --- a/packages/backend/src/tokens/__tests__/handshake.test.ts +++ b/packages/backend/src/tokens/__tests__/handshake.test.ts @@ -430,6 +430,42 @@ describe('HandshakeService', () => { expect(url.searchParams.get('__clerk_api_version')).toBe('2025-04-10'); expect(url.searchParams.get(constants.QueryParameters.SuffixedCookies)).toMatch(/^(true|false)$/); expect(url.searchParams.get(constants.QueryParameters.HandshakeReason)).toBe('test-reason'); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe('nonce'); + }); + + it('should include handshake format parameter', () => { + const headers = handshakeService.buildRedirectToHandshake('test-reason'); + const location = headers.get(constants.Headers.Location); + if (!location) { + throw new Error('Location header is missing'); + } + const url = new URL(location); + + // Verify the handshake format parameter is present + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe('nonce'); + }); + + it('should include handshake format parameter in development mode', () => { + const developmentContext = { + ...mockAuthenticateContext, + instanceType: 'development', + devBrowserToken: 'dev-browser-token', + } as AuthenticateContext; + + const developmentHandshakeService = new HandshakeService( + developmentContext, + mockOptions, + mockOrganizationMatcher, + ); + const headers = developmentHandshakeService.buildRedirectToHandshake('test-reason'); + const location = headers.get(constants.Headers.Location); + + if (!location) { + throw new Error('Location header is missing'); + } + const url = new URL(location); + + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe('nonce'); }); }); diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index cea50da15b0..a6733576603 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -27,6 +27,7 @@ interface AuthenticateContext extends AuthenticateRequestOptions { // handshake-related values devBrowserToken: string | undefined; + handshakeFormat: 'nonce' | 'token' | undefined; handshakeNonce: string | undefined; handshakeRedirectLoopCounter: number; handshakeToken: string | undefined; @@ -218,6 +219,10 @@ class AuthenticateContext implements AuthenticateContext { this.handshakeRedirectLoopCounter = Number(this.getCookie(constants.Cookies.RedirectCount)) || 0; this.handshakeNonce = this.getQueryParam(constants.QueryParameters.HandshakeNonce) || this.getCookie(constants.Cookies.HandshakeNonce); + this.handshakeFormat = + (this.getQueryParam(constants.QueryParameters.HandshakeFormat) as 'nonce' | 'token') || + (this.getCookie(constants.Cookies.HandshakeFormat) as 'nonce' | 'token') || + 'nonce'; } private getQueryParam(name: string) { @@ -288,6 +293,23 @@ class AuthenticateContext implements AuthenticateContext { private sessionExpired(jwt: Jwt | undefined): boolean { return !!jwt && jwt?.payload.exp <= (Date.now() / 1000) >> 0; } + + /** + * Checks if the current context can handle nonce-based handshakes + * by reading the handshake format from cookies or query parameters + * @returns true if nonce handshakes are supported, false otherwise + */ + public canHandleNonceHandshake(): boolean { + return this.handshakeFormat === 'nonce'; + } + + /** + * Gets the handshake format from the request context, defaulting to 'token' if not specified + * @returns The handshake format ('nonce' or 'token') + */ + public getHandshakeFormat(): 'nonce' | 'token' { + return this.handshakeFormat || 'token'; + } } export type { AuthenticateContext }; diff --git a/packages/backend/src/tokens/handshake.ts b/packages/backend/src/tokens/handshake.ts index 18ba6dc6080..e8c9278de71 100644 --- a/packages/backend/src/tokens/handshake.ts +++ b/packages/backend/src/tokens/handshake.ts @@ -1,4 +1,4 @@ -import { constants, SUPPORTED_BAPI_VERSION } from '../constants'; +import { constants, SUPPORTED_BAPI_VERSION, SUPPORTED_HANDSHAKE_FORMAT } from '../constants'; import { TokenVerificationError, TokenVerificationErrorAction, TokenVerificationErrorReason } from '../errors'; import type { VerifyJwtOptions } from '../jwt'; import { assertHeaderAlgorithm, assertHeaderType } from '../jwt/assertions'; @@ -149,7 +149,12 @@ export class HandshakeService { this.authenticateContext.usesSuffixedCookies().toString(), ); url.searchParams.append(constants.QueryParameters.HandshakeReason, reason); - url.searchParams.append(constants.QueryParameters.HandshakeFormat, 'nonce'); + /** + * Appends the supported handshake format parameter to the URL + * This parameter indicates the format of the handshake response that the client expects + * and implicitly signals that this backend version supports nonce handshakes + */ + url.searchParams.append(constants.QueryParameters.HandshakeFormat, SUPPORTED_HANDSHAKE_FORMAT); if (this.authenticateContext.instanceType === 'development' && this.authenticateContext.devBrowserToken) { url.searchParams.append(constants.QueryParameters.DevBrowser, this.authenticateContext.devBrowserToken); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index d212f568432..1e72e3607c4 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -4,6 +4,7 @@ import { constants } from '../constants'; import type { TokenCarrier } from '../errors'; import { MachineTokenVerificationError, TokenVerificationError, TokenVerificationErrorReason } from '../errors'; import { decodeJwt } from '../jwt/verifyJwt'; +import { enhanceOAuthRedirectUrl } from '../util/handshakeUtils'; import { assertValidSecretKey } from '../util/optionsAssertions'; import { isDevelopmentFromSecretKey } from '../util/shared'; import type { AuthenticateContext } from './authenticateContext'; @@ -141,6 +142,73 @@ export const authenticateRequest: AuthenticateRequest = (async ( const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options); assertValidSecretKey(authenticateContext.secretKey); + /** + * Merges headers from the RequestState with a handshake format cookie. + * Creates a new Headers object with the configured handshake format and adds all headers from the result. + * Also modifies OAuth callback URLs to include the handshake format parameter. + * + * @param result - The RequestState containing headers to merge + * @returns The RequestState with merged headers + */ + function mergeHeaders(result: RequestState): RequestState { + const headers = new Headers(); + const handshakeFormatValue = authenticateContext.handshakeFormat || 'nonce'; + + let domain = ''; + try { + if (authenticateContext.frontendApi) { + const host = authenticateContext.frontendApi.startsWith('http') + ? new URL(authenticateContext.frontendApi).hostname + : authenticateContext.frontendApi; + + if (host.startsWith('clerk.')) { + domain = host.replace(/^clerk\./, ''); + } else if (host.includes('.clerk.')) { + domain = host.split('.clerk.')[1]; + } else if (host.includes('.')) { + const parts = host.split('.'); + if (parts.length >= 2) { + domain = parts.slice(-2).join('.'); + } + } + } + + if (!domain) { + domain = authenticateContext.domain || ''; + } + } catch { + domain = authenticateContext.domain || ''; + } + + headers.append( + 'Set-Cookie', + `${constants.Cookies.HandshakeFormat}=${handshakeFormatValue}; Path=/; SameSite=None; Secure; Domain=${domain};`, + ); + + // Check if this is a redirect response that might contain OAuth URLs in the Location header + const locationHeader = result.headers.get(constants.Headers.Location); + if (locationHeader) { + // Enhance OAuth redirect URLs to include the handshake format parameter + const enhancedUrl = enhanceOAuthRedirectUrl(locationHeader, authenticateContext); + if (enhancedUrl !== locationHeader) { + headers.set(constants.Headers.Location, enhancedUrl); + } + } + + for (const [key, value] of result.headers.entries()) { + // Don't duplicate the Location header if we already processed it above + if (key.toLowerCase() === 'location' && locationHeader) { + const enhancedUrl = enhanceOAuthRedirectUrl(locationHeader, authenticateContext); + if (enhancedUrl !== locationHeader) { + continue; // Skip since we already set the enhanced header + } + } + headers.append(key, value); + } + result.headers = headers; + return result; + } + // Default tokenType is session_token for backwards compatibility. const acceptsToken = options.acceptsToken ?? TokenType.SessionToken; @@ -746,12 +814,12 @@ export const authenticateRequest: AuthenticateRequest = (async ( if (authenticateContext.tokenInHeader) { if (acceptsToken === 'any') { - return authenticateAnyRequestWithTokenInHeader(); + return await authenticateAnyRequestWithTokenInHeader(); } if (acceptsToken === TokenType.SessionToken) { - return authenticateRequestWithTokenInHeader(); + return mergeHeaders(await authenticateRequestWithTokenInHeader()); } - return authenticateMachineRequestWithTokenInHeader(); + return await authenticateMachineRequestWithTokenInHeader(); } // Machine requests cannot have the token in the cookie, it must be in header. @@ -767,7 +835,7 @@ export const authenticateRequest: AuthenticateRequest = (async ( }); } - return authenticateRequestWithTokenInCookie(); + return mergeHeaders(await authenticateRequestWithTokenInCookie()); }) as AuthenticateRequest; /** diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index 2b95dfb6c23..06dd070c54e 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -58,6 +58,14 @@ export type AuthenticateRequestOptions = { * If the activation can't be performed, either because an organization doesn't exist or the user lacks access, the active organization in the session won't be changed. Ultimately, it's the responsibility of the page to verify that the resources are appropriate to render given the URL and handle mismatches appropriately (e.g., by returning a 404). */ organizationSyncOptions?: OrganizationSyncOptions; + /** + * Specifies the handshake format to be used during OAuth authentication flows. + * When set to 'nonce', the backend signals to the frontend that it can handle nonce-based handshakes + * during OAuth flow resolution. + * + * @default 'token' + */ + handshakeFormat?: 'nonce' | 'token'; /** * @internal */ diff --git a/packages/backend/src/util/__tests__/handshakeUtils.test.ts b/packages/backend/src/util/__tests__/handshakeUtils.test.ts new file mode 100644 index 00000000000..91bc1cdb6b4 --- /dev/null +++ b/packages/backend/src/util/__tests__/handshakeUtils.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from 'vitest'; +import { constants, SUPPORTED_HANDSHAKE_FORMAT } from '../../constants'; +import type { AuthenticateContext } from '../../tokens/authenticateContext'; +import { + appendHandshakeFormatToOAuthCallback, + createHandshakeFormatHeaders, + enhanceOAuthRedirectUrl, + getHandshakeFormatCookie, +} from '../handshakeUtils'; + +describe('handshakeUtils', () => { + const mockAuthenticateContextWithNonce = { + canHandleNonceHandshake: () => true, + getHandshakeFormat: () => 'nonce' as const, + } as AuthenticateContext; + + const mockAuthenticateContextWithToken = { + canHandleNonceHandshake: () => false, + getHandshakeFormat: () => 'token' as const, + } as AuthenticateContext; + + describe('appendHandshakeFormatToOAuthCallback', () => { + it('should append handshake format parameter when nonce is supported', () => { + const originalUrl = 'https://example.com/oauth/callback'; + const modifiedUrl = appendHandshakeFormatToOAuthCallback(originalUrl, mockAuthenticateContextWithNonce); + + const url = new URL(modifiedUrl); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe(SUPPORTED_HANDSHAKE_FORMAT); + }); + + it('should not append handshake format parameter when token is used', () => { + const originalUrl = 'https://example.com/oauth/callback'; + const modifiedUrl = appendHandshakeFormatToOAuthCallback(originalUrl, mockAuthenticateContextWithToken); + + const url = new URL(modifiedUrl); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBeNull(); + }); + + it('should preserve existing query parameters', () => { + const originalUrl = 'https://example.com/oauth/callback?existing=param&other=value'; + const modifiedUrl = appendHandshakeFormatToOAuthCallback(originalUrl, mockAuthenticateContextWithNonce); + + const url = new URL(modifiedUrl); + expect(url.searchParams.get('existing')).toBe('param'); + expect(url.searchParams.get('other')).toBe('value'); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe(SUPPORTED_HANDSHAKE_FORMAT); + }); + }); + + describe('enhanceOAuthRedirectUrl', () => { + it('should enhance OAuth callback URLs directly', () => { + const originalUrl = 'https://example.com/oauth_callback'; + const enhancedUrl = enhanceOAuthRedirectUrl(originalUrl, mockAuthenticateContextWithNonce); + + const url = new URL(enhancedUrl); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe(SUPPORTED_HANDSHAKE_FORMAT); + }); + + it('should enhance OAuth callback URLs with oauth-callback path', () => { + const originalUrl = 'https://example.com/oauth-callback'; + const enhancedUrl = enhanceOAuthRedirectUrl(originalUrl, mockAuthenticateContextWithNonce); + + const url = new URL(enhancedUrl); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe(SUPPORTED_HANDSHAKE_FORMAT); + }); + + it('should enhance URLs with redirect_url containing oauth callback', () => { + const callbackUrl = 'https://app.example.com/oauth_callback'; + const originalUrl = `https://provider.com/oauth/authorize?redirect_url=${encodeURIComponent(callbackUrl)}`; + const enhancedUrl = enhanceOAuthRedirectUrl(originalUrl, mockAuthenticateContextWithNonce); + + const url = new URL(enhancedUrl); + const enhancedCallbackUrl = url.searchParams.get('redirect_url'); + expect(enhancedCallbackUrl).toBeDefined(); + + const callbackUrlObj = new URL(enhancedCallbackUrl as string); + expect(callbackUrlObj.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe( + SUPPORTED_HANDSHAKE_FORMAT, + ); + }); + + it('should not modify URL when nonce is not supported', () => { + const originalUrl = 'https://example.com/oauth_callback'; + const enhancedUrl = enhanceOAuthRedirectUrl(originalUrl, mockAuthenticateContextWithToken); + + expect(enhancedUrl).toBe(originalUrl); + }); + + it('should return original URL when redirectUrl is empty', () => { + const enhancedUrl = enhanceOAuthRedirectUrl('', mockAuthenticateContextWithNonce); + + expect(enhancedUrl).toBe(''); + }); + + it('should handle malformed URLs gracefully', () => { + const malformedUrl = 'not-a-valid-url'; + const enhancedUrl = enhanceOAuthRedirectUrl(malformedUrl, mockAuthenticateContextWithNonce); + + expect(enhancedUrl).toBe(malformedUrl); + }); + + it('should preserve existing parameters in OAuth callback URLs', () => { + const originalUrl = 'https://example.com/oauth_callback?state=xyz&code=abc'; + const enhancedUrl = enhanceOAuthRedirectUrl(originalUrl, mockAuthenticateContextWithNonce); + + const url = new URL(enhancedUrl); + expect(url.searchParams.get('state')).toBe('xyz'); + expect(url.searchParams.get('code')).toBe('abc'); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe(SUPPORTED_HANDSHAKE_FORMAT); + }); + }); + + describe('getHandshakeFormatCookie', () => { + it('should return cookie string when nonce is supported', () => { + const cookie = getHandshakeFormatCookie(mockAuthenticateContextWithNonce); + + expect(cookie).toBe( + `${constants.Cookies.HandshakeFormat}=${SUPPORTED_HANDSHAKE_FORMAT}; Path=/; SameSite=None; Secure`, + ); + }); + + it('should return null when token is used', () => { + const cookie = getHandshakeFormatCookie(mockAuthenticateContextWithToken); + + expect(cookie).toBeNull(); + }); + }); + + describe('createHandshakeFormatHeaders', () => { + it('should create headers with Set-Cookie when nonce is supported', () => { + const headers = createHandshakeFormatHeaders(mockAuthenticateContextWithNonce); + + const setCookieHeader = headers.get('Set-Cookie'); + expect(setCookieHeader).toBe( + `${constants.Cookies.HandshakeFormat}=${SUPPORTED_HANDSHAKE_FORMAT}; Path=/; SameSite=None; Secure`, + ); + }); + + it('should create empty headers when token is used', () => { + const headers = createHandshakeFormatHeaders(mockAuthenticateContextWithToken); + + const setCookieHeader = headers.get('Set-Cookie'); + expect(setCookieHeader).toBeNull(); + }); + + it('should return Headers instance', () => { + const headers = createHandshakeFormatHeaders(mockAuthenticateContextWithNonce); + + expect(headers).toBeInstanceOf(Headers); + }); + }); +}); diff --git a/packages/backend/src/util/handshakeUtils.ts b/packages/backend/src/util/handshakeUtils.ts new file mode 100644 index 00000000000..f425b3275c3 --- /dev/null +++ b/packages/backend/src/util/handshakeUtils.ts @@ -0,0 +1,94 @@ +import { constants, SUPPORTED_HANDSHAKE_FORMAT } from '../constants'; +import type { AuthenticateContext } from '../tokens/authenticateContext'; + +/** + * Appends handshake format query parameter to an OAuth callback URL + * so that FAPI knows the backend can handle nonce-based handshakes + * + * @param url - The OAuth callback URL to modify + * @param authenticateContext - The authentication context containing handshake format info + * @returns The modified URL with handshake format parameter + */ +export function appendHandshakeFormatToOAuthCallback(url: string, authenticateContext: AuthenticateContext): string { + const callbackUrl = new URL(url); + + // If the backend can handle nonce handshakes, add the format parameter + if (authenticateContext.canHandleNonceHandshake()) { + callbackUrl.searchParams.set(constants.QueryParameters.HandshakeFormat, SUPPORTED_HANDSHAKE_FORMAT); + } + + return callbackUrl.toString(); +} + +/** + * Enhances an OAuth redirect URL by appending handshake format parameter to the callback URL + * This is used when creating external accounts or initiating OAuth flows + * + * @param redirectUrl - The redirect URL to modify (contains oauth_callback) + * @param authenticateContext - The authentication context + * @returns The enhanced redirect URL with handshake format parameter + */ +export function enhanceOAuthRedirectUrl(redirectUrl: string, authenticateContext: AuthenticateContext): string { + if (!redirectUrl || !authenticateContext.canHandleNonceHandshake()) { + return redirectUrl; + } + + try { + const url = new URL(redirectUrl); + + // Check if this URL contains oauth_callback path - if so, add the format parameter + if (url.pathname.includes('oauth_callback') || url.pathname.includes('oauth-callback')) { + url.searchParams.set(constants.QueryParameters.HandshakeFormat, SUPPORTED_HANDSHAKE_FORMAT); + return url.toString(); + } + + // For other OAuth-related URLs, check if they have callback-related query parameters + const callbackUrl = url.searchParams.get('redirect_url') || url.searchParams.get('redirectUrl'); + if (callbackUrl) { + try { + const enhancedCallback = appendHandshakeFormatToOAuthCallback(callbackUrl, authenticateContext); + url.searchParams.set('redirect_url', enhancedCallback); + return url.toString(); + } catch { + // If callback URL parsing fails, return original URL + return redirectUrl; + } + } + } catch { + // If URL parsing fails, return original URL + return redirectUrl; + } + + return redirectUrl; +} + +/** + * Gets the handshake format cookie value + * + * @param authenticateContext - The authentication context + * @returns The cookie string if nonce is supported, null otherwise + */ +export function getHandshakeFormatCookie(authenticateContext: AuthenticateContext): string | null { + if (!authenticateContext.canHandleNonceHandshake()) { + return null; + } + + return `${constants.Cookies.HandshakeFormat}=${SUPPORTED_HANDSHAKE_FORMAT}; Path=/; SameSite=None; Secure`; +} + +/** + * Creates headers with handshake format cookie for OAuth flows + * + * @param authenticateContext - The authentication context + * @returns Headers object with Set-Cookie header if nonce is supported + */ +export function createHandshakeFormatHeaders(authenticateContext: AuthenticateContext): Headers { + const headers = new Headers(); + const cookie = getHandshakeFormatCookie(authenticateContext); + + if (cookie) { + headers.append('Set-Cookie', cookie); + } + + return headers; +} diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index dcdba03d856..34456d1be6d 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -16,6 +16,7 @@ import { createSessionCookie } from './cookies/session'; import { getCookieSuffix } from './cookieSuffix'; import type { DevBrowser } from './devBrowser'; import { createDevBrowser } from './devBrowser'; + import { SessionCookiePoller } from './SessionCookiePoller'; // TODO(@dimkl): make AuthCookieService singleton since it handles updating cookies using a poller diff --git a/packages/types/src/factors.ts b/packages/types/src/factors.ts index 7505beac359..bb88ce046e6 100644 --- a/packages/types/src/factors.ts +++ b/packages/types/src/factors.ts @@ -106,16 +106,19 @@ export type OAuthConfig = OauthFactor & { actionCompleteRedirectUrl: string; oidcPrompt?: string; oidcLoginHint?: string; + handshakeFormat?: 'nonce' | 'token'; }; export type SamlConfig = SamlFactor & { redirectUrl: string; actionCompleteRedirectUrl: string; + handshakeFormat?: 'nonce' | 'token'; }; export type EnterpriseSSOConfig = EnterpriseSSOFactor & { redirectUrl: string; actionCompleteRedirectUrl: string; + handshakeFormat?: 'nonce' | 'token'; oidcPrompt?: string; }; diff --git a/packages/types/src/redirects.ts b/packages/types/src/redirects.ts index 2418931b5bf..edd759d8ebb 100644 --- a/packages/types/src/redirects.ts +++ b/packages/types/src/redirects.ts @@ -87,6 +87,11 @@ export type AuthenticateWithRedirectParams = { */ legalAccepted?: boolean; + /** + * Whether to use handshake nonce or handshake token + */ + handshakeFormat?: 'nonce' | 'token'; + /** * Optional for `oauth_` or `enterprise_sso` strategies. The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 0f8ccdbde66..a4ef8d67f22 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -195,6 +195,7 @@ export type SignInCreateParams = ( identifier?: string; oidcPrompt?: string; oidcLoginHint?: string; + handshakeFormat?: 'nonce' | 'token'; } | { strategy: TicketStrategy;