Skip to content

Commit 10f3dda

Browse files
authored
chore(backend,nextjs): Improve token handling and optimize verification (#6123)
1 parent c25271d commit 10f3dda

File tree

10 files changed

+246
-182
lines changed

10 files changed

+246
-182
lines changed

.changeset/twenty-beds-serve.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/backend': minor
3+
'@clerk/nextjs': minor
4+
---
5+
6+
- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
7+
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.
8+

packages/backend/src/tokens/__tests__/request.test-d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
2626
// Array of token types
2727
expectTypeOf(
2828
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
29-
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
29+
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();
3030

3131
// Any token type
3232
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();

packages/backend/src/tokens/__tests__/request.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
1616
import { AuthErrorReason, AuthStatus } from '../authStatus';
1717
import { OrganizationMatcher } from '../organizationMatcher';
1818
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
19-
import type { MachineTokenType } from '../tokenTypes';
19+
import { type MachineTokenType, TokenType } from '../tokenTypes';
2020
import type { AuthenticateRequestOptions } from '../types';
2121

2222
const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
@@ -236,7 +236,7 @@ expect.extend({
236236
toBeMachineUnauthenticated(
237237
received,
238238
expected: {
239-
tokenType: MachineTokenType;
239+
tokenType: MachineTokenType | null;
240240
reason: AuthReason;
241241
message: string;
242242
},
@@ -246,6 +246,7 @@ expect.extend({
246246
received.tokenType === expected.tokenType &&
247247
received.reason === expected.reason &&
248248
received.message === expected.message &&
249+
!received.isAuthenticated &&
249250
!received.token;
250251

251252
if (pass) {
@@ -264,15 +265,11 @@ expect.extend({
264265
toBeMachineUnauthenticatedToAuth(
265266
received,
266267
expected: {
267-
tokenType: MachineTokenType;
268+
tokenType: MachineTokenType | null;
268269
},
269270
) {
270271
const pass =
271-
received.tokenType === expected.tokenType &&
272-
!received.claims &&
273-
!received.subject &&
274-
!received.name &&
275-
!received.id;
272+
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;
276273

277274
if (pass) {
278275
return {
@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
12031200
});
12041201

12051202
// Test each token type with parameterized tests
1206-
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
1203+
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];
12071204

12081205
describe.each(tokenTypes)('%s Authentication', tokenType => {
12091206
const mockToken = mockTokens[tokenType];
@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
12401237
});
12411238
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
12421239
tokenType,
1240+
isAuthenticated: false,
12431241
});
12441242
});
12451243
});
@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
12891287
});
12901288
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
12911289
tokenType: 'api_key',
1290+
isAuthenticated: false,
12921291
});
12931292
});
12941293

@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
13031302
});
13041303
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
13051304
tokenType: 'oauth_token',
1305+
isAuthenticated: false,
13061306
});
13071307
});
13081308

@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
13171317
});
13181318
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
13191319
tokenType: 'machine_token',
1320+
isAuthenticated: false,
13201321
});
13211322
});
13221323

@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
13281329
tokenType: 'machine_token',
13291330
reason: AuthErrorReason.TokenTypeMismatch,
13301331
message: '',
1332+
isAuthenticated: false,
13311333
});
13321334
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
13331335
tokenType: 'machine_token',
1336+
isAuthenticated: false,
13341337
});
13351338
});
13361339
});
@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
13601363
);
13611364

13621365
expect(requestState).toBeMachineUnauthenticated({
1363-
tokenType: 'machine_token',
1366+
tokenType: null,
13641367
reason: AuthErrorReason.TokenTypeMismatch,
13651368
message: '',
13661369
});
13671370
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
1368-
tokenType: 'machine_token',
1371+
tokenType: null,
1372+
isAuthenticated: false,
13691373
});
13701374
});
13711375
});

packages/backend/src/tokens/authObjects.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
428428
* Returns an auth object matching the requested token type(s).
429429
*
430430
* If the parsed token type does not match any in acceptsToken, returns:
431-
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
431+
* - an invalid token auth object if the token is not in the accepted array
432+
* - an unauthenticated machine object for machine tokens, or
432433
* - a signed-out session object otherwise.
433434
*
434435
* This ensures the returned object always matches the developer's intent.
435436
*/
436-
export function getAuthObjectForAcceptedToken({
437+
export const getAuthObjectForAcceptedToken = ({
437438
authObject,
438439
acceptsToken = TokenType.SessionToken,
439440
}: {
440441
authObject: AuthObject;
441442
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
442-
}): AuthObject {
443+
}): AuthObject => {
444+
// 1. any token: return as-is
443445
if (acceptsToken === 'any') {
444446
return authObject;
445447
}
446448

449+
// 2. array of tokens: must match one of the accepted types
447450
if (Array.isArray(acceptsToken)) {
448451
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
449-
// If the token is not in the accepted array, return invalid token auth object
450452
return invalidTokenAuthObject();
451453
}
452454
return authObject;
453455
}
454456

455-
// Single value: Intent based
457+
// 3. single token: must match exactly, else return appropriate unauthenticated object
456458
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
457459
if (isMachineTokenType(acceptsToken)) {
458460
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
459461
}
460462
return signedOutAuthObject(authObject.debug);
461463
}
462464

465+
// 4. default: return as-is
463466
return authObject;
464-
}
467+
};

packages/backend/src/tokens/authStatus.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
55
import type { AuthenticateContext } from './authenticateContext';
66
import type {
77
AuthenticatedMachineObject,
8+
InvalidTokenAuthObject,
89
SignedInAuthObject,
910
SignedOutAuthObject,
1011
UnauthenticatedMachineObject,
1112
} from './authObjects';
1213
import {
1314
authenticatedMachineObject,
15+
invalidTokenAuthObject,
1416
signedInAuthObject,
1517
signedOutAuthObject,
1618
unauthenticatedMachineObject,
@@ -27,13 +29,15 @@ export const AuthStatus = {
2729

2830
export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];
2931

30-
type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
31-
? Authenticated extends true
32-
? (opts?: PendingSessionOptions) => SignedInAuthObject
33-
: () => SignedOutAuthObject
34-
: Authenticated extends true
35-
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
36-
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
32+
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
33+
? () => InvalidTokenAuthObject
34+
: T extends SessionTokenType
35+
? Authenticated extends true
36+
? (opts?: PendingSessionOptions) => SignedInAuthObject
37+
: () => SignedOutAuthObject
38+
: Authenticated extends true
39+
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
40+
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;
3741

3842
export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
3943
status: typeof AuthStatus.SignedIn;
@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
5862
toAuth: ToAuth<T, true>;
5963
};
6064

61-
export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
65+
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
6266
status: typeof AuthStatus.SignedOut;
6367
reason: AuthReason;
6468
message: string;
@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea
120124

121125
export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;
122126

123-
export type RequestState<T extends TokenType = SessionTokenType> =
124-
| AuthenticatedState<T>
127+
export type RequestState<T extends TokenType | null = SessionTokenType> =
128+
| AuthenticatedState<T extends null ? never : T>
125129
| UnauthenticatedState<T>
126130
| (T extends SessionTokenType ? HandshakeState : never);
127131

@@ -240,6 +244,29 @@ export function handshake(
240244
});
241245
}
242246

247+
export function signedOutInvalidToken(): UnauthenticatedState<null> {
248+
const authObject = invalidTokenAuthObject();
249+
return withDebugHeaders({
250+
status: AuthStatus.SignedOut,
251+
reason: AuthErrorReason.TokenTypeMismatch,
252+
message: '',
253+
proxyUrl: '',
254+
publishableKey: '',
255+
isSatellite: false,
256+
domain: '',
257+
signInUrl: '',
258+
signUpUrl: '',
259+
afterSignInUrl: '',
260+
afterSignUpUrl: '',
261+
isSignedIn: false,
262+
isAuthenticated: false,
263+
tokenType: null,
264+
toAuth: () => authObject,
265+
headers: new Headers(),
266+
token: null,
267+
});
268+
}
269+
243270
const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
244271
requestState: T,
245272
): T => {

packages/backend/src/tokens/request.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
1010
import { createAuthenticateContext } from './authenticateContext';
1111
import type { SignedInAuthObject } from './authObjects';
1212
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
13-
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
13+
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
1414
import { createClerkRequest } from './clerkRequest';
1515
import { getCookieName, getCookieValue } from './cookie';
1616
import { HandshakeService } from './handshake';
@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
8888
return null;
8989
}
9090

91+
function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
92+
let parsedTokenType: TokenType | null = null;
93+
const { tokenInHeader } = authenticateContext;
94+
if (tokenInHeader) {
95+
if (isMachineTokenByPrefix(tokenInHeader)) {
96+
parsedTokenType = getMachineTokenType(tokenInHeader);
97+
} else {
98+
parsedTokenType = TokenType.SessionToken;
99+
}
100+
}
101+
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
102+
return isTokenTypeAccepted(typeToCheck, acceptsToken);
103+
}
104+
91105
export interface AuthenticateRequest {
92106
/**
93107
* @example
@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
96110
<T extends readonly TokenType[]>(
97111
request: Request,
98112
options: AuthenticateRequestOptions & { acceptsToken: T },
99-
): Promise<RequestState<T[number]>>;
113+
): Promise<RequestState<T[number] | null>>;
100114

101115
/**
102116
* @example
@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
123137
export const authenticateRequest: AuthenticateRequest = (async (
124138
request: Request,
125139
options: AuthenticateRequestOptions,
126-
): Promise<RequestState<TokenType>> => {
140+
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
127141
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
128142
assertValidSecretKey(authenticateContext.secretKey);
129143

@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
655669
// Handle case where tokenType is any and the token is not a machine token
656670
if (!isMachineTokenByPrefix(tokenInHeader)) {
657671
return signedOut({
658-
tokenType: acceptsToken as MachineTokenType,
672+
tokenType: acceptsToken as TokenType,
659673
authenticateContext,
660674
reason: AuthErrorReason.TokenTypeMismatch,
661675
message: '',
@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
722736
});
723737
}
724738

739+
// If acceptsToken is an array, early check if the token is in the accepted array
740+
// to avoid unnecessary verification calls
741+
if (Array.isArray(acceptsToken)) {
742+
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
743+
return signedOutInvalidToken();
744+
}
745+
}
746+
725747
if (authenticateContext.tokenInHeader) {
726748
if (acceptsToken === 'any') {
727749
return authenticateAnyRequestWithTokenInHeader();
728750
}
729-
730751
if (acceptsToken === TokenType.SessionToken) {
731752
return authenticateRequestWithTokenInHeader();
732753
}
733-
734754
return authenticateMachineRequestWithTokenInHeader();
735755
}
736756

0 commit comments

Comments
 (0)