Skip to content

Commit eef0ceb

Browse files
fix(backend): Reject JWT-template tokens on the acceptsToken any/array header path (#9511)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9831487 commit eef0ceb

3 files changed

Lines changed: 26 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': patch
3+
---
4+
5+
Reject JWT-template tokens presented as session tokens in the `Authorization` header when `acceptsToken` is `'any'` or an array that includes `'session_token'`. `authenticateRequest()` now returns a signed-out state with reason `token-type-mismatch` for such a token, matching the existing `acceptsToken: 'session_token'` behavior.

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,25 +1310,33 @@ describe('tokens.authenticateRequest(options)', () => {
13101310
// Regression tests for SEC-340. A JWT-template token is signed by the same instance key and
13111311
// passes verifyToken(), but carries no `sid`, so it outlives revocation of the session that
13121312
// minted it and must not authenticate one.
1313+
const templateInHeader = (jwt: string) => mockRequestWithHeaderAuth({ authorization: jwt });
13131314
describe.each([
1314-
['headerToken', (jwt: string) => mockRequestWithHeaderAuth({ authorization: jwt })],
1315+
['headerToken', templateInHeader, {}],
13151316
[
13161317
'cookieToken',
13171318
(jwt: string) =>
13181319
mockRequestWithCookies(
13191320
{},
13201321
{ __clerk_db_jwt: 'deadbeef', __client_uat: `${mockJwtPayload.iat - 10}`, __session: jwt },
13211322
),
1323+
{},
13221324
],
1323-
])('%s: JWT-template token presented as a session token (SEC-340)', (_label, buildRequest) => {
1325+
['headerToken, acceptsToken: any', templateInHeader, { acceptsToken: 'any' as const }],
1326+
[
1327+
'headerToken, acceptsToken: [session_token, m2m_token]',
1328+
templateInHeader,
1329+
{ acceptsToken: ['session_token', 'm2m_token'] as const },
1330+
],
1331+
])('%s: JWT-template token presented as a session token (SEC-340)', (_label, buildRequest, options) => {
13241332
test('returns signed out', async () => {
13251333
const { sid: _sid, ...payloadWithoutSid } = mockJwtPayload;
13261334
const { data: templateJwt } = await signJwt(payloadWithoutSid, signingJwks, {
13271335
algorithm: 'RS256',
13281336
header: { typ: 'JWT', kid: 'ins_2GIoQhbUpy0hX7B2cVkuTMinXoD', cat: JWT_CATEGORY_JWT_TEMPLATE },
13291337
});
13301338

1331-
const requestState = await authenticateRequest(buildRequest(templateJwt!), mockOptions());
1339+
const requestState = await authenticateRequest(buildRequest(templateJwt!), mockOptions(options));
13321340

13331341
expect(requestState).toBeSignedOut({
13341342
reason: AuthErrorReason.TokenTypeMismatch,

‎packages/backend/src/tokens/request.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,16 @@ export const authenticateRequest: AuthenticateRequest = (async (
864864
});
865865
}
866866

867-
// Handle as a regular session token
867+
// Handle as a regular session token. Mirrors the session-only header path (SEC-340).
868+
if (hasNonSessionJwtCategory(tokenInHeader)) {
869+
return signedOut({
870+
tokenType: TokenType.SessionToken,
871+
authenticateContext,
872+
reason: AuthErrorReason.TokenTypeMismatch,
873+
message: '',
874+
});
875+
}
876+
868877
const { data, errors } = await verifyToken(tokenInHeader, authenticateContext);
869878
if (errors) {
870879
return handleSessionTokenError(errors[0], 'header');

0 commit comments

Comments
 (0)