Skip to content

Commit 5facfad

Browse files
authored
fix(jwt): use header.alg as fallback in verifyFromJwks (#4144)
1 parent e0f8dd8 commit 5facfad

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

src/utils/jwt/jwt.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
22
import { vi } from 'vitest'
3-
import { encodeBase64 } from '../encode'
3+
import { encodeBase64, encodeBase64Url } from '../encode'
44
import { AlgorithmTypes } from './jwa'
5+
import { signing } from './jws'
56
import * as JWT from './jwt'
7+
import { verifyFromJwks } from './jwt'
68
import {
79
JwtAlgorithmNotImplemented,
810
JwtTokenExpired,
@@ -11,6 +13,7 @@ import {
1113
JwtTokenNotBefore,
1214
JwtTokenSignatureMismatched,
1315
} from './types'
16+
import { utf8Encoder } from './utf8'
1417

1518
describe('isTokenHeader', () => {
1619
it('should return true for valid TokenHeader', () => {
@@ -474,6 +477,45 @@ describe('JWT', () => {
474477
})
475478
})
476479

480+
describe('verifyFromJwks header.alg fallback', () => {
481+
it('Should use header.alg as fallback when matchingKey.alg is missing', async () => {
482+
// Setup: Create a JWT signed with HS384 (different from default HS256)
483+
const payload = { message: 'hello world' }
484+
const headerAlg = 'HS384' // Non-default value
485+
const secret = 'secret'
486+
const kid = 'dummy'
487+
488+
// Create JWT (signed with HS384)
489+
const header = { alg: headerAlg, typ: 'JWT', kid }
490+
const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
491+
const encodedHeader = encode(header)
492+
const encodedPayload = encode(payload)
493+
const signingInput = `${encodedHeader}.${encodedPayload}`
494+
495+
// Use signing function from jws.ts instead of createHmac directly
496+
const signatureBuffer = await signing(secret, headerAlg, utf8Encoder.encode(signingInput))
497+
const signature = encodeBase64Url(signatureBuffer)
498+
499+
const token = `${encodedHeader}.${encodedPayload}.${signature}`
500+
501+
// Create a key without alg property
502+
const keys = [
503+
{
504+
kty: 'oct',
505+
kid,
506+
k: encodeBase64Url(utf8Encoder.encode(secret).buffer),
507+
use: 'sig',
508+
// alg is intentionally omitted
509+
},
510+
]
511+
512+
// Execute: Verify the JWT token signed with HS384
513+
const result = await verifyFromJwks(token, { keys })
514+
515+
// If verification succeeds, it means header.alg was used
516+
expect(result).toEqual(payload)
517+
})
518+
})
477519
async function exportPEMPrivateKey(key: CryptoKey): Promise<string> {
478520
const exported = await crypto.subtle.exportKey('pkcs8', key)
479521
const pem = `-----BEGIN PRIVATE KEY-----\n${encodeBase64(exported)}\n-----END PRIVATE KEY-----`

src/utils/jwt/jwt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export const verifyFromJwks = async (
153153
throw new JwtTokenInvalid(token)
154154
}
155155

156-
return await verify(token, matchingKey, matchingKey.alg as SignatureAlgorithm)
156+
return await verify(token, matchingKey, (matchingKey.alg as SignatureAlgorithm) || header.alg)
157157
}
158158

159159
export const decode = (token: string): { header: TokenHeader; payload: JWTPayload } => {

0 commit comments

Comments
 (0)