diff --git a/docs/README.md b/docs/README.md index dd375cd435..e8ae7011fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -935,7 +935,8 @@ Verifies the claims and signature of a JSON Web Token. past from now if expiration is not present. **Default:** 'false' - `ignoreNbf`: `` When true will not be validating the "nbf" claim value to be in the past from now. **Default:** 'false' - - `issuer`: `` Expected issuer value. An exact match must be found in the payload. + - `issuer`: `` | `string[]` Expected issuer value(s). When string an exact match must + be found in the payload, when array at least one must be matched. - `jti`: `` Expected jti value. An exact match must be found in the payload. - `maxAuthAge`: `` When provided the payload is checked to have the "auth_time" claim and its value is validated, provided as timespan string e.g. `30m`, `24 hours`. See diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js index 8af4b5fcfe..5941efe155 100644 --- a/lib/jwt/verify.js +++ b/lib/jwt/verify.js @@ -63,12 +63,15 @@ const validateOptions = ({ isOptionString(maxTokenAge, 'options.maxTokenAge') isOptionString(subject, 'options.subject') - isOptionString(issuer, 'options.issuer') isOptionString(maxAuthAge, 'options.maxAuthAge') isOptionString(jti, 'options.jti') isOptionString(clockTolerance, 'options.clockTolerance') isOptionString(typ, 'options.typ') + if (issuer !== undefined && (isNotString(issuer) && isNotArrayOfStrings(issuer))) { + throw new TypeError('options.issuer must be a string or an array of strings') + } + if (audience !== undefined && (isNotString(audience) && isNotArrayOfStrings(audience))) { throw new TypeError('options.audience must be a string or an array of strings') } @@ -161,7 +164,7 @@ const validateTypes = ({ header, payload }, profile, options) => { isPayloadString(payload.jti, '"jti" claim', 'jti', profile === LOGOUTTOKEN || profile === ATJWT || !!options.jti) isPayloadString(payload.acr, '"acr" claim', 'acr') isPayloadString(payload.nonce, '"nonce" claim', 'nonce', !!options.nonce) - isPayloadString(payload.iss, '"iss" claim', 'iss', !!options.issuer) + isStringOrArrayOfStrings(payload.iss, 'iss', !!options.issuer) isPayloadString(payload.sub, '"sub" claim', 'sub', profile === IDTOKEN || profile === ATJWT || !!options.subject) isStringOrArrayOfStrings(payload.aud, 'aud', !!options.audience) isPayloadString(payload.azp, '"azp" claim', 'azp', profile === IDTOKEN && Array.isArray(payload.aud) && payload.aud.length > 1) @@ -235,7 +238,7 @@ module.exports = (token, key, options = {}) => { const unix = epoch(now) validateTypes(decoded, profile, options) - if (issuer && decoded.payload.iss !== issuer) { + if (issuer && (typeof decoded.payload.iss !== 'string' || !(typeof issuer === 'string' ? [issuer] : issuer).includes(decoded.payload.iss))) { throw new JWTClaimInvalid('unexpected "iss" claim value', 'iss', 'check_failed') } diff --git a/test/jwt/verify.test.js b/test/jwt/verify.test.js index a752fd9fd5..9bd13aa602 100644 --- a/test/jwt/verify.test.js +++ b/test/jwt/verify.test.js @@ -35,7 +35,6 @@ test('options must be an object', t => { }) test('options.clockTolerance must be a string', string, 'clockTolerance') -test('options.issuer must be a string', string, 'issuer') test('options.jti must be a string', string, 'jti') test('options.profile must be a string', string, 'profile') test('options.maxAuthAge must be a string', string, 'maxAuthAge') @@ -55,6 +54,17 @@ test('options.ignoreExp must be boolean', boolean, 'ignoreExp') test('options.ignoreNbf must be boolean', boolean, 'ignoreNbf') test('options.ignoreIat must be boolean', boolean, 'ignoreIat') +test('options.issuer must be string or array of strings', t => { + ;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => { + t.throws(() => { + JWT.verify(token, key, { issuer: val }) + }, { instanceOf: TypeError, message: 'options.issuer must be a string or an array of strings' }) + t.throws(() => { + JWT.verify(token, key, { issuer: [val] }) + }, { instanceOf: TypeError, message: 'options.issuer must be a string or an array of strings' }) + }) +}) + test('options.audience must be string or array of strings', t => { ;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => { t.throws(() => { @@ -116,7 +126,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => { }) }) -;['jti', 'acr', 'iss', 'nonce', 'sub', 'azp'].forEach((claim) => { +;['jti', 'acr', 'nonce', 'sub', 'azp'].forEach((claim) => { test(`"${claim} must be a string when provided"`, t => { ;['', 0, 1, true, null, [], {}].forEach((val) => { const err = t.throws(() => { @@ -130,7 +140,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => { }) }) -;['aud', 'amr'].forEach((claim) => { +;['aud', 'amr', 'iss'].forEach((claim) => { test(`"${claim} must be a string when provided"`, t => { ;['', 0, 1, true, null, [], {}].forEach((val) => { let err diff --git a/types/index.d.ts b/types/index.d.ts index ba0948b59f..ca67c45ef1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -444,7 +444,7 @@ export namespace JWT { ignoreIat?: boolean; maxTokenAge?: string; subject?: string; - issuer?: string; + issuer?: string | string[]; maxAuthAge?: string; jti?: string; clockTolerance?: string; @@ -479,7 +479,7 @@ export namespace JWT { function sign(payload: object, key: ProduceKeyInputWithNone, options?: SignOptions): string; interface VerifyProfileOptions { - issuer: string; + issuer: string | string[]; audience: string | string[]; profile?: profile; }