-
-
Notifications
You must be signed in to change notification settings - Fork 310
/
jwt_claims_set.ts
148 lines (126 loc) · 4.34 KB
/
jwt_claims_set.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import type {
JWTPayload,
JWTClaimVerificationOptions,
JWEHeaderParameters,
JWSHeaderParameters,
} from '../types.d'
import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js'
import { decoder } from './buffer_utils.js'
import epoch from './epoch.js'
import secs from './secs.js'
import isObject from './is_object.js'
const normalizeTyp = (value: string) => value.toLowerCase().replace(/^application\//, '')
const checkAudiencePresence = (audPayload: unknown, audOption: unknown[]) => {
if (typeof audPayload === 'string') {
return audOption.includes(audPayload)
}
if (Array.isArray(audPayload)) {
// Each principal intended to process the JWT MUST
// identify itself with a value in the audience claim
return audOption.some(Set.prototype.has.bind(new Set(audPayload)))
}
return false
}
export default (
protectedHeader: JWEHeaderParameters | JWSHeaderParameters,
encodedPayload: Uint8Array,
options: JWTClaimVerificationOptions = {},
) => {
const { typ } = options
if (
typ &&
(typeof protectedHeader!.typ !== 'string' ||
normalizeTyp(protectedHeader!.typ) !== normalizeTyp(typ))
) {
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', 'typ', 'check_failed')
}
let payload!: { [propName: string]: unknown }
try {
payload = JSON.parse(decoder.decode(encodedPayload))
} catch {
//
}
if (!isObject(payload)) {
throw new JWTInvalid('JWT Claims Set must be a top-level JSON object')
}
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options
if (maxTokenAge !== undefined) requiredClaims.push('iat')
if (audience !== undefined) requiredClaims.push('aud')
if (subject !== undefined) requiredClaims.push('sub')
if (issuer !== undefined) requiredClaims.push('iss')
for (const claim of new Set(requiredClaims.reverse())) {
if (!(claim in payload)) {
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, claim, 'missing')
}
}
if (issuer && !(<unknown[]>(Array.isArray(issuer) ? issuer : [issuer])).includes(payload.iss!)) {
throw new JWTClaimValidationFailed('unexpected "iss" claim value', 'iss', 'check_failed')
}
if (subject && payload.sub !== subject) {
throw new JWTClaimValidationFailed('unexpected "sub" claim value', 'sub', 'check_failed')
}
if (
audience &&
!checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)
) {
throw new JWTClaimValidationFailed('unexpected "aud" claim value', 'aud', 'check_failed')
}
let tolerance: number
switch (typeof options.clockTolerance) {
case 'string':
tolerance = secs(options.clockTolerance)
break
case 'number':
tolerance = options.clockTolerance
break
case 'undefined':
tolerance = 0
break
default:
throw new TypeError('Invalid clockTolerance option type')
}
const { currentDate } = options
const now = epoch(currentDate || new Date())
if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {
throw new JWTClaimValidationFailed('"iat" claim must be a number', 'iat', 'invalid')
}
if (payload.nbf !== undefined) {
if (typeof payload.nbf !== 'number') {
throw new JWTClaimValidationFailed('"nbf" claim must be a number', 'nbf', 'invalid')
}
if (payload.nbf > now + tolerance) {
throw new JWTClaimValidationFailed(
'"nbf" claim timestamp check failed',
'nbf',
'check_failed',
)
}
}
if (payload.exp !== undefined) {
if (typeof payload.exp !== 'number') {
throw new JWTClaimValidationFailed('"exp" claim must be a number', 'exp', 'invalid')
}
if (payload.exp <= now - tolerance) {
throw new JWTExpired('"exp" claim timestamp check failed', 'exp', 'check_failed')
}
}
if (maxTokenAge) {
const age = now - payload.iat!
const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge)
if (age - tolerance > max) {
throw new JWTExpired(
'"iat" claim timestamp check failed (too far in the past)',
'iat',
'check_failed',
)
}
if (age < 0 - tolerance) {
throw new JWTClaimValidationFailed(
'"iat" claim timestamp check failed (it should be in the past)',
'iat',
'check_failed',
)
}
}
return <JWTPayload>payload
}