Skip to content

Commit a5bd9eb

Browse files
authored
Merge commit from fork
1 parent 58d3d3a commit a5bd9eb

2 files changed

Lines changed: 178 additions & 6 deletions

File tree

src/utils/jwt/jwt.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,53 @@ describe('JWT', () => {
108108
expect(authorized).toBeUndefined()
109109
})
110110

111+
describe('JwtTokenNotBefore with malformed nbf claim', () => {
112+
it('rejects token with nbf as a non-numeric string', async () => {
113+
const secret = 'a-secret'
114+
const tok = await JWT.sign(
115+
// @ts-expect-error - testing malformed payload (nbf must be number)
116+
{ message: 'hello', nbf: 'tomorrow' },
117+
secret,
118+
AlgorithmTypes.HS256
119+
)
120+
121+
let err
122+
let authorized
123+
try {
124+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
125+
} catch (e) {
126+
err = e
127+
}
128+
expect(err).toEqual(new JwtTokenNotBefore(tok))
129+
expect(authorized).toBeUndefined()
130+
})
131+
132+
it('rejects token with nbf = Infinity (parsed from 1e400 in JSON)', async () => {
133+
// JSON.stringify converts Infinity to null, so hand-craft the payload.
134+
const secret = 'a-secret'
135+
const encode = (s: string) => encodeBase64Url(utf8Encoder.encode(s).buffer).replace(/=/g, '')
136+
const encodedHeader = encode('{"alg":"HS256","typ":"JWT"}')
137+
const encodedPayload = encode('{"message":"hello","nbf":1e400}')
138+
const signingInput = `${encodedHeader}.${encodedPayload}`
139+
const signatureBuffer = await signing(
140+
secret,
141+
AlgorithmTypes.HS256,
142+
utf8Encoder.encode(signingInput)
143+
)
144+
const tok = `${signingInput}.${encodeBase64Url(signatureBuffer).replace(/=/g, '')}`
145+
146+
let err
147+
let authorized
148+
try {
149+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
150+
} catch (e) {
151+
err = e
152+
}
153+
expect(err).toEqual(new JwtTokenNotBefore(tok))
154+
expect(authorized).toBeUndefined()
155+
})
156+
})
157+
111158
it('JwtTokenExpired', async () => {
112159
const tok =
113160
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzMwNDYxMDAsImV4cCI6MTYzMzA0NjQwMH0.H-OI1TWAbmK8RonvcpPaQcNvOKS9sxinEOsgKwjoiVo'
@@ -145,6 +192,68 @@ describe('JWT', () => {
145192
vi.useRealTimers()
146193
})
147194

195+
describe('JwtTokenExpired with malformed exp claim', () => {
196+
it('rejects token with exp = 0 (epoch zero)', async () => {
197+
const secret = 'a-secret'
198+
const tok = await JWT.sign({ message: 'hello', exp: 0 }, secret, AlgorithmTypes.HS256)
199+
200+
let err
201+
let authorized
202+
try {
203+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
204+
} catch (e) {
205+
err = e
206+
}
207+
expect(err).toEqual(new JwtTokenExpired(tok))
208+
expect(authorized).toBeUndefined()
209+
})
210+
211+
it('rejects token with exp as a non-numeric string', async () => {
212+
const secret = 'a-secret'
213+
const tok = await JWT.sign(
214+
// @ts-expect-error - testing malformed payload (exp must be number)
215+
{ message: 'hello', exp: 'tomorrow' },
216+
secret,
217+
AlgorithmTypes.HS256
218+
)
219+
220+
let err
221+
let authorized
222+
try {
223+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
224+
} catch (e) {
225+
err = e
226+
}
227+
expect(err).toEqual(new JwtTokenExpired(tok))
228+
expect(authorized).toBeUndefined()
229+
})
230+
231+
it('rejects token with exp = Infinity (parsed from 1e400 in JSON)', async () => {
232+
// JSON.stringify converts Infinity to null, so hand-craft the payload.
233+
const secret = 'a-secret'
234+
const encode = (s: string) => encodeBase64Url(utf8Encoder.encode(s).buffer).replace(/=/g, '')
235+
const encodedHeader = encode('{"alg":"HS256","typ":"JWT"}')
236+
const encodedPayload = encode('{"message":"hello","exp":1e400}')
237+
const signingInput = `${encodedHeader}.${encodedPayload}`
238+
const signatureBuffer = await signing(
239+
secret,
240+
AlgorithmTypes.HS256,
241+
utf8Encoder.encode(signingInput)
242+
)
243+
const tok = `${signingInput}.${encodeBase64Url(signatureBuffer).replace(/=/g, '')}`
244+
245+
let err
246+
let authorized
247+
try {
248+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
249+
} catch (e) {
250+
err = e
251+
}
252+
expect(err).toEqual(new JwtTokenExpired(tok))
253+
expect(authorized).toBeUndefined()
254+
})
255+
})
256+
148257
it('JwtTokenIssuedAt', async () => {
149258
const now = 1633046400
150259
vi.useFakeTimers().setSystemTime(new Date().setTime(now * 1000))
@@ -165,6 +274,63 @@ describe('JWT', () => {
165274
expect(authorized).toBeUndefined()
166275
})
167276

277+
describe('JwtTokenIssuedAt with malformed iat claim', () => {
278+
it('rejects token with iat as a non-numeric string', async () => {
279+
const now = 1633046400
280+
vi.useFakeTimers().setSystemTime(new Date(now * 1000))
281+
282+
const secret = 'a-secret'
283+
const tok = await JWT.sign(
284+
// @ts-expect-error - testing malformed payload (iat must be number)
285+
{ message: 'hello', iat: 'tomorrow' },
286+
secret,
287+
AlgorithmTypes.HS256
288+
)
289+
290+
let err
291+
let authorized
292+
try {
293+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
294+
} catch (e) {
295+
err = e
296+
}
297+
expect(err).toEqual(new JwtTokenIssuedAt(now, 'tomorrow' as unknown as number))
298+
expect(authorized).toBeUndefined()
299+
300+
vi.useRealTimers()
301+
})
302+
303+
it('rejects token with iat = Infinity (parsed from 1e400 in JSON)', async () => {
304+
// JSON.stringify converts Infinity to null, so hand-craft the payload.
305+
const now = 1633046400
306+
vi.useFakeTimers().setSystemTime(new Date(now * 1000))
307+
308+
const secret = 'a-secret'
309+
const encode = (s: string) => encodeBase64Url(utf8Encoder.encode(s).buffer).replace(/=/g, '')
310+
const encodedHeader = encode('{"alg":"HS256","typ":"JWT"}')
311+
const encodedPayload = encode('{"message":"hello","iat":1e400}')
312+
const signingInput = `${encodedHeader}.${encodedPayload}`
313+
const signatureBuffer = await signing(
314+
secret,
315+
AlgorithmTypes.HS256,
316+
utf8Encoder.encode(signingInput)
317+
)
318+
const tok = `${signingInput}.${encodeBase64Url(signatureBuffer).replace(/=/g, '')}`
319+
320+
let err
321+
let authorized
322+
try {
323+
authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
324+
} catch (e) {
325+
err = e
326+
}
327+
expect(err).toEqual(new JwtTokenIssuedAt(now, Infinity))
328+
expect(authorized).toBeUndefined()
329+
330+
vi.useRealTimers()
331+
})
332+
})
333+
168334
it('JwtTokenIssuer (none)', async () => {
169335
const tok =
170336
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDB9.Ha3tPZzmnLGyFfZYd7GSV0iCn2F9kbZffFVZcTe5kJo'

src/utils/jwt/jwt.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,20 @@ export const verify = async (
128128
throw new JwtAlgorithmMismatch(alg, header.alg)
129129
}
130130
const now = Math.floor(Date.now() / 1000)
131-
if (nbf && payload.nbf && payload.nbf > now) {
132-
throw new JwtTokenNotBefore(token)
131+
if (nbf && payload.nbf !== undefined) {
132+
if (typeof payload.nbf !== 'number' || !Number.isFinite(payload.nbf) || payload.nbf > now) {
133+
throw new JwtTokenNotBefore(token)
134+
}
133135
}
134-
if (exp && payload.exp && payload.exp <= now) {
135-
throw new JwtTokenExpired(token)
136+
if (exp && payload.exp !== undefined) {
137+
if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp) || payload.exp <= now) {
138+
throw new JwtTokenExpired(token)
139+
}
136140
}
137-
if (iat && payload.iat && now < payload.iat) {
138-
throw new JwtTokenIssuedAt(now, payload.iat)
141+
if (iat && payload.iat !== undefined) {
142+
if (typeof payload.iat !== 'number' || !Number.isFinite(payload.iat) || now < payload.iat) {
143+
throw new JwtTokenIssuedAt(now, payload.iat)
144+
}
139145
}
140146
if (iss) {
141147
if (!payload.iss) {

0 commit comments

Comments
 (0)