Skip to content

Commit c8379a0

Browse files
committed
feat: oauth hadnshake nonce support
1 parent 5491491 commit c8379a0

File tree

8 files changed

+42
-3
lines changed

8 files changed

+42
-3
lines changed

packages/backend/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const API_VERSION = 'v1';
44
export const USER_AGENT = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
55
export const MAX_CACHE_LAST_UPDATED_AT_SECONDS = 5 * 60;
66
export const SUPPORTED_BAPI_VERSION = '2025-04-10';
7+
export const SUPPORTED_HANDSHAKE_FORMAT = 'nonce';
78

89
const Attributes = {
910
AuthToken: '__clerkAuthToken',
@@ -21,6 +22,7 @@ const Cookies = {
2122
Handshake: '__clerk_handshake',
2223
DevBrowser: '__clerk_db_jwt',
2324
RedirectCount: '__clerk_redirect_count',
25+
HandshakeFormat: '__clerk_handshake_format',
2426
HandshakeNonce: '__clerk_handshake_nonce',
2527
} as const;
2628

@@ -34,6 +36,7 @@ const QueryParameters = {
3436
HandshakeHelp: '__clerk_help',
3537
LegacyDevBrowser: '__dev_session',
3638
HandshakeReason: '__clerk_hs_reason',
39+
HandshakeFormat: Cookies.HandshakeFormat,
3740
HandshakeNonce: Cookies.HandshakeNonce,
3841
} as const;
3942

packages/backend/src/tokens/authenticateContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface AuthenticateContext extends AuthenticateRequestOptions {
2525
clientUat: number;
2626
// handshake-related values
2727
devBrowserToken: string | undefined;
28+
handshakeFormat: 'nonce' | 'token' | undefined;
2829
handshakeNonce: string | undefined;
2930
handshakeToken: string | undefined;
3031
handshakeRedirectLoopCounter: number;

packages/backend/src/tokens/handshake.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { constants, SUPPORTED_BAPI_VERSION } from '../constants';
1+
import { constants, SUPPORTED_BAPI_VERSION, SUPPORTED_HANDSHAKE_FORMAT } from '../constants';
22
import { TokenVerificationError, TokenVerificationErrorAction, TokenVerificationErrorReason } from '../errors';
33
import type { VerifyJwtOptions } from '../jwt';
44
import { assertHeaderAlgorithm, assertHeaderType } from '../jwt/assertions';
@@ -145,6 +145,7 @@ export class HandshakeService {
145145
this.authenticateContext.usesSuffixedCookies().toString(),
146146
);
147147
url.searchParams.append(constants.QueryParameters.HandshakeReason, reason);
148+
url.searchParams.append(constants.QueryParameters.HandshakeFormat, SUPPORTED_HANDSHAKE_FORMAT);
148149

149150
if (this.authenticateContext.instanceType === 'development' && this.authenticateContext.devBrowserToken) {
150151
url.searchParams.append(constants.QueryParameters.DevBrowser, this.authenticateContext.devBrowserToken);

packages/backend/src/tokens/request.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ export async function authenticateRequest(
7676
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
7777
assertValidSecretKey(authenticateContext.secretKey);
7878

79+
/**
80+
* Merges headers from the RequestState with a default handshake cookie.
81+
* Creates a new Headers object with a nonce cookie and adds all headers from the result.
82+
*
83+
* @param result - The RequestState containing headers to merge
84+
* @returns The RequestState with merged headers
85+
*/
86+
function mergeHeaders(result: RequestState): RequestState {
87+
const headers = new Headers();
88+
headers.append('Set-Cookie', `${constants.Cookies.HandshakeFormat}=nonce; Path=/; SameSite=Lax;`);
89+
for (const [key, value] of result.headers.entries()) {
90+
headers.append(key, value);
91+
}
92+
result.headers = headers;
93+
return result;
94+
}
95+
7996
if (authenticateContext.isSatellite) {
8097
assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey);
8198
if (authenticateContext.signInUrl && authenticateContext.origin) {
@@ -533,10 +550,10 @@ export async function authenticateRequest(
533550
}
534551

535552
if (authenticateContext.sessionTokenInHeader) {
536-
return authenticateRequestWithTokenInHeader();
553+
return mergeHeaders(await authenticateRequestWithTokenInHeader());
537554
}
538555

539-
return authenticateRequestWithTokenInCookie();
556+
return mergeHeaders(await authenticateRequestWithTokenInCookie());
540557
}
541558

542559
/**

packages/backend/src/tokens/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export type AuthenticateRequestOptions = {
4545
* 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).
4646
*/
4747
organizationSyncOptions?: OrganizationSyncOptions;
48+
/**
49+
* Specifies the handshake format to be used during OAuth authentication flows.
50+
* When set to 'nonce', the backend signals to the frontend that it can handle nonce-based handshakes
51+
* during OAuth flow resolution.
52+
*
53+
* @default 'token'
54+
*/
55+
handshakeFormat?: 'nonce' | 'token';
4856
/**
4957
* @internal
5058
*/

packages/types/src/factors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,19 @@ export type OAuthConfig = OauthFactor & {
106106
actionCompleteRedirectUrl: string;
107107
oidcPrompt?: string;
108108
oidcLoginHint?: string;
109+
handshakeFormat?: 'nonce' | 'token';
109110
};
110111

111112
export type SamlConfig = SamlFactor & {
112113
redirectUrl: string;
113114
actionCompleteRedirectUrl: string;
115+
handshakeFormat?: 'nonce' | 'token';
114116
};
115117

116118
export type EnterpriseSSOConfig = EnterpriseSSOFactor & {
117119
redirectUrl: string;
118120
actionCompleteRedirectUrl: string;
121+
handshakeFormat?: 'nonce' | 'token';
119122
};
120123

121124
export type PhoneCodeSecondFactorConfig = {

packages/types/src/redirects.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ export type AuthenticateWithRedirectParams = {
8181
* Whether the user has accepted the legal requirements.
8282
*/
8383
legalAccepted?: boolean;
84+
85+
/**
86+
* Whether to use handshake nonce or handshake token
87+
*/
88+
handshakeFormat?: 'nonce' | 'token';
8489
};
8590

8691
export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null };

packages/types/src/signIn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export type SignInCreateParams = (
195195
identifier?: string;
196196
oidcPrompt?: string;
197197
oidcLoginHint?: string;
198+
handshakeFormat?: 'nonce' | 'token';
198199
}
199200
| {
200201
strategy: TicketStrategy;

0 commit comments

Comments
 (0)