Skip to content

Commit

Permalink
feat: add JWT.verify "typ" option for checking JWT Type Header parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 24, 2020
1 parent 419d09b commit fc08426
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 13 deletions.
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,9 @@ Verifies the claims and signature of a JSON Web Token.
`subject` depending on the use-case.
- `audience`: `<string>` &vert; `string[]` Expected audience value(s). When string an exact match must
be found in the payload, when array at least one must be matched.
- `typ`: `<string>` Expected JWT "typ" Header Parameter value. An exact match must be found in the
JWT header. **Default:** 'undefined' unless a `profile` with a specific value is used, in which
case this option will be ignored.
- `clockTolerance`: `<string>` Clock Tolerance for comparing timestamps, provided as timespan
string e.g. `120s`, `2 minutes`, etc. **Default:** no clock tolerance
- `complete`: `<Boolean>` When false only the parsed payload is returned, otherwise an object with
Expand Down
24 changes: 14 additions & 10 deletions lib/jwt/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const isNotArrayOfStrings = val => !Array.isArray(val) || val.length === 0 || va
const validateOptions = ({
algorithms, audience, clockTolerance, complete = false, crit, ignoreExp = false,
ignoreIat = false, ignoreNbf = false, issuer, jti, maxAuthAge, maxTokenAge, nonce, now = new Date(),
profile, subject
profile, subject, typ
}) => {
isOptionString(profile, 'options.profile')

Expand All @@ -66,6 +66,7 @@ const validateOptions = ({
isOptionString(maxAuthAge, 'options.maxAuthAge')
isOptionString(jti, 'options.jti')
isOptionString(clockTolerance, 'options.clockTolerance')
isOptionString(typ, 'options.typ')

if (audience !== undefined && (isNotString(audience) && isNotArrayOfStrings(audience))) {
throw new TypeError('options.audience must be a string or an array of strings')
Expand Down Expand Up @@ -109,6 +110,8 @@ const validateOptions = ({
throw new TypeError('"audience" option is required to validate a JWT Access Token')
}

typ = ATJWT

break
case LOGOUTTOKEN:
if (!issuer) {
Expand Down Expand Up @@ -142,7 +145,8 @@ const validateOptions = ({
nonce,
now,
profile,
subject
subject,
typ
}
}

Expand All @@ -156,15 +160,15 @@ const validateTypes = ({ header, payload }, profile, options) => {
isPayloadString(payload.jti, '"jti" claim', 'jti', profile === LOGOUTTOKEN || !!options.jti)
isPayloadString(payload.acr, '"acr" claim', 'acr')
isPayloadString(payload.nonce, '"nonce" claim', 'nonce', !!options.nonce)
isPayloadString(payload.iss, '"iss" claim', 'iss', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN || !!options.issuer)
isPayloadString(payload.iss, '"iss" claim', 'iss', !!options.issuer)
isPayloadString(payload.sub, '"sub" claim', 'sub', profile === IDTOKEN || profile === ATJWT || !!options.subject)
isStringOrArrayOfStrings(payload.aud, 'aud', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN || !!options.audience)
isStringOrArrayOfStrings(payload.aud, 'aud', !!options.audience)
isPayloadString(payload.azp, '"azp" claim', 'azp', profile === IDTOKEN && Array.isArray(payload.aud) && payload.aud.length > 1)
isStringOrArrayOfStrings(payload.amr, 'amr')
isPayloadString(header.typ, '"typ" header parameter', 'typ', !!options.typ)

if (profile === ATJWT) {
isPayloadString(payload.client_id, '"client_id" claim', 'client_id', true)
isPayloadString(header.typ, '"typ" header parameter', 'typ', true)
}

if (profile === LOGOUTTOKEN) {
Expand Down Expand Up @@ -221,7 +225,7 @@ module.exports = (token, key, options = {}) => {

const {
algorithms, audience, clockTolerance, complete, crit, ignoreExp, ignoreIat, ignoreNbf, issuer,
jti, maxAuthAge, maxTokenAge, nonce, now, profile, subject
jti, maxAuthAge, maxTokenAge, nonce, now, profile, subject, typ
} = options = validateOptions(options)

const decoded = decode(token, { complete: true })
Expand Down Expand Up @@ -257,6 +261,10 @@ module.exports = (token, key, options = {}) => {
throw new JWTClaimInvalid('unexpected "aud" claim value', 'aud', 'check_failed')
}

if (typ && decoded.header.typ !== typ) {
throw new JWTClaimInvalid('unexpected "typ" JWT header value', 'typ', 'check_failed')
}

const tolerance = clockTolerance ? secs(clockTolerance) : 0

if (maxAuthAge) {
Expand Down Expand Up @@ -295,9 +303,5 @@ module.exports = (token, key, options = {}) => {
throw new JWTClaimInvalid('unexpected "azp" claim value', 'azp', 'check_failed')
}

if (profile === ATJWT && decoded.header.typ !== ATJWT) {
throw new JWTClaimInvalid('invalid JWT typ header value for the used validation profile', 'typ', 'check_failed')
}

return complete ? decoded : decoded.payload
}
29 changes: 26 additions & 3 deletions test/jwt/verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => {

Object.entries({
issuer: 'iss',
jti: 'jti',
nonce: 'nonce',
subject: 'sub',
jti: 'jti'
subject: 'sub'
}).forEach(([option, claim]) => {
test(`option.${option} validation fails`, t => {
let err
Expand All @@ -181,6 +181,29 @@ Object.entries({
})
})

test('option.typ validation fails', t => {
let err
err = t.throws(() => {
const invalid = JWT.sign({}, key, { header: { typ: 'foo' } })
JWT.verify(invalid, key, { typ: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "typ" JWT header value' })
t.is(err.claim, 'typ')
t.is(err.reason, 'check_failed')

err = t.throws(() => {
const invalid = JWS.sign({}, key, { header: { typ: undefined } })
JWT.verify(invalid, key, { typ: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"typ" header parameter is missing' })
t.is(err.claim, 'typ')
t.is(err.reason, 'missing')
})

test('option.typ validation success', t => {
const token = JWT.sign({}, key, { header: { typ: 'foo' } })
JWT.verify(token, key, { typ: 'foo' })
t.pass()
})

test('option.audience validation fails', t => {
let err
err = t.throws(() => {
Expand Down Expand Up @@ -822,7 +845,7 @@ test('must be a supported value', t => {
key,
{ profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: 'invalid JWT typ header value for the used validation profile' })
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "typ" JWT header value' })
t.is(err.claim, 'typ')
t.is(err.reason, 'check_failed')
})
Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export namespace JWT {
audience?: string | string[];
algorithms?: string[];
nonce?: string;
typ?: string;
now?: Date;
crit?: string[];
profile?: JWTProfiles;
Expand Down

0 comments on commit fc08426

Please sign in to comment.