Skip to content

Commit af9df3e

Browse files
committed
fix(backend): Reject OAuth JWTs for session token token type
1 parent aa2d3b5 commit af9df3e

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,6 +1496,73 @@ describe('tokens.authenticateRequest(options)', () => {
14961496
isAuthenticated: false,
14971497
});
14981498
});
1499+
1500+
test('rejects OAuth JWT token when acceptsToken is session_token', async () => {
1501+
const request = mockRequest({ authorization: `Bearer ${mockTokens.oauth_token}` });
1502+
const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'session_token' }));
1503+
1504+
expect(result).toBeSignedOut({
1505+
reason: AuthErrorReason.TokenTypeMismatch,
1506+
message: '',
1507+
tokenType: 'session_token',
1508+
isAuthenticated: false,
1509+
});
1510+
expect(result.toAuth()).toBeSignedOutToAuth();
1511+
});
1512+
1513+
test('rejects M2M token when acceptsToken is session_token', async () => {
1514+
const request = mockRequest({ authorization: `Bearer ${mockTokens.m2m_token}` });
1515+
const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'session_token' }));
1516+
1517+
expect(result).toBeSignedOut({
1518+
reason: AuthErrorReason.TokenTypeMismatch,
1519+
message: '',
1520+
tokenType: 'session_token',
1521+
isAuthenticated: false,
1522+
});
1523+
expect(result.toAuth()).toBeSignedOutToAuth();
1524+
});
1525+
1526+
test('rejects API key when acceptsToken is session_token', async () => {
1527+
const request = mockRequest({ authorization: `Bearer ${mockTokens.api_key}` });
1528+
const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'session_token' }));
1529+
1530+
expect(result).toBeSignedOut({
1531+
reason: AuthErrorReason.TokenTypeMismatch,
1532+
message: '',
1533+
tokenType: 'session_token',
1534+
isAuthenticated: false,
1535+
});
1536+
expect(result.toAuth()).toBeSignedOutToAuth();
1537+
});
1538+
1539+
test('accepts valid session token when acceptsToken is session_token', async () => {
1540+
server.use(
1541+
http.get('https://api.clerk.test/v1/jwks', () => {
1542+
return HttpResponse.json(mockJwks);
1543+
}),
1544+
);
1545+
1546+
const request = mockRequest({ authorization: `Bearer ${mockJwt}` });
1547+
const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'session_token' }));
1548+
1549+
expect(result).toBeSignedIn();
1550+
expect(result.tokenType).toBe('session_token');
1551+
});
1552+
1553+
test('accepts OAuth JWT when acceptsToken is "any"', async () => {
1554+
server.use(
1555+
http.post(mockMachineAuthResponses.oauth_token.endpoint, () => {
1556+
return HttpResponse.json(mockVerificationResults.oauth_token);
1557+
}),
1558+
);
1559+
1560+
const request = mockRequest({ authorization: `Bearer ${mockTokens.oauth_token}` });
1561+
const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'any' }));
1562+
1563+
expect(result).toBeMachineAuthenticated();
1564+
expect(result.tokenType).toBe('oauth_token');
1565+
});
14991566
});
15001567

15011568
describe('Array of Accepted Token Types', () => {

packages/backend/src/tokens/request.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,20 @@ export const authenticateRequest: AuthenticateRequest = (async (
411411
async function authenticateRequestWithTokenInHeader() {
412412
const { tokenInHeader } = authenticateContext;
413413

414+
// SECURITY: Reject machine tokens (M2M, OAuth, API keys) when expecting session tokens.
415+
// OAuth JWTs (RFC 9068) are valid JWTs signed by Clerk and will pass verifyToken() verification,
416+
// but they should not be accepted as session tokens. We must explicitly check the token type
417+
// before verification to prevent machine tokens from being incorrectly authenticated as sessions.
418+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
419+
if (isMachineToken(tokenInHeader!)) {
420+
return signedOut({
421+
tokenType: TokenType.SessionToken,
422+
authenticateContext,
423+
reason: AuthErrorReason.TokenTypeMismatch,
424+
message: '',
425+
});
426+
}
427+
414428
try {
415429
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416430
const { data, errors } = await verifyToken(tokenInHeader!, authenticateContext);
@@ -608,6 +622,19 @@ export const authenticateRequest: AuthenticateRequest = (async (
608622
return handleSessionTokenError(decodedErrors[0], 'cookie');
609623
}
610624

625+
// SECURITY: Defense-in-depth check to reject machine tokens in cookies.
626+
// While machine tokens should only be in headers, this prevents potential security issues
627+
// if a machine token somehow ends up in the session cookie.
628+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
629+
if (isMachineToken(authenticateContext.sessionTokenInCookie!)) {
630+
return signedOut({
631+
tokenType: TokenType.SessionToken,
632+
authenticateContext,
633+
reason: AuthErrorReason.TokenTypeMismatch,
634+
message: '',
635+
});
636+
}
637+
611638
if (decodeResult.payload.iat < authenticateContext.clientUat) {
612639
return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenIATBeforeClientUAT, '');
613640
}

0 commit comments

Comments
 (0)