Skip to content

Commit

Permalink
Merge 6d8504c into 9a737d4
Browse files Browse the repository at this point in the history
  • Loading branch information
buccfer committed Feb 22, 2022
2 parents 9a737d4 + 6d8504c commit 857a038
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ parserOptions:
rules:
'comma-dangle': ['error', 'never']
'strict': ['error', 'global']
'max-len': ['error', { code: 120 }]
'max-len': ['error', { code: 120, ignoreComments: true }]
'semi': ['error', 'never']
'no-trailing-spaces': ['error', { skipBlankLines: true }]
'object-curly-newline': ['error', { consistent: true }]
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.2.0 / 2022-02-21

### :lady_beetle: Bug Fixes

- Fixed [#18](https://github.com/buccfer/aws-cognito-express/issues/18) - The `aud` OR `client_id` claim must match one of the audience entries provided in the config.

## 3.1.0 / 2021-09-04

### :tada: Enhancements
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aws-cognito-express",
"version": "3.1.0",
"version": "3.2.0",
"description": "Verification of Access and ID tokens issued by AWS Cognito for Node.js",
"main": "index.js",
"files": [
Expand Down
26 changes: 24 additions & 2 deletions src/lib/verify-jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@
const jwt = require('jsonwebtoken')
const { InvalidJWTError } = require('./errors')

/**
* @private
* @description Validates the JWT audience.
* @param {string[]} tokenAudience - The JWT audience.
* @param {string[]} validAudience - The valid audience.
* @returns {boolean} Whether the token audience is valid or not.
* */
const validateAudience = (tokenAudience, validAudience) => {
const validAudienceSet = new Set(validAudience)
return tokenAudience.some((audience) => validAudienceSet.has(audience))
}

/**
* @private
* @description Verifies the JWT signature. If valid, it returns the decoded payload.
* @param {string} token - The JSON web token.
* @param {string} pem - The PEM encoded public RSA key.
* @param {Object} options - Additional fields to validate.
* @param {string[]} options.audience - A set of valid values for the audience (aud) field.
* @param {string[]} options.audience - A set of valid values for the audience (aud or client_id) field.
* @param {string} options.issuer - A valid value for the issuer (iss) field.
* @param {string[]} options.tokenUse - A set of valid values for the token use (token_use) field.
* @returns {Promise<Object>} A promise that resolves to the decoded JWT payload if the verification
Expand All @@ -20,7 +32,6 @@ const verifyJwt = (token, pem, { audience, issuer, tokenUse }) => new Promise((r
algorithms: ['RS256'],
complete: false,
ignoreExpiration: false,
audience,
issuer
}

Expand All @@ -31,6 +42,17 @@ const verifyJwt = (token, pem, { audience, issuer, tokenUse }) => new Promise((r
return reject(new InvalidJWTError(`"${payload.token_use}" tokens are not allowed`))
}

// Cognito access tokens don't have an "aud" claim, so we need to check the "client_id" claim instead.
// See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html#http-api-jwt-authorizer.evaluation
let tokenAudience = payload.aud || payload.client_id
tokenAudience = Array.isArray(tokenAudience) ? tokenAudience : [tokenAudience]

const hasValidAudience = validateAudience(tokenAudience, audience)

if (!hasValidAudience) {
return reject(new InvalidJWTError(`jwt audience invalid. expected: ${audience.join(' or ')}`))
}

return resolve(payload)
})
})
Expand Down
31 changes: 29 additions & 2 deletions test/lib/jwt-validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ describe('Validator', () => {
expect(initScope.isDone()).to.be.true
})

it('Should reject with InvalidJWTError if token audience is invalid', async () => {
it('Should reject with InvalidJWTError if token audience is invalid (aud)', async () => {
const initScope = nock(jwksUrl.origin).get(jwksUrl.pathname).reply(httpStatus.OK, { keys: jwks })
const token = signToken('key_1', tokenPayload, {
audience: chance.hash(),
Expand All @@ -455,6 +455,17 @@ describe('Validator', () => {
expect(initScope.isDone()).to.be.true
})

it('Should reject with InvalidJWTError if token audience is invalid (client_id)', async () => {
const initScope = nock(jwksUrl.origin).get(jwksUrl.pathname).reply(httpStatus.OK, { keys: jwks })
tokenPayload.client_id = chance.hash()
const token = signToken('key_1', tokenPayload, {
issuer: validator.iss,
tokenUse: chance.pickone(validator.tokenUse)
})
await expect(validator.validate(token)).to.eventually.be.rejectedWith(InvalidJWTError, /jwt audience invalid/)
expect(initScope.isDone()).to.be.true
})

it('Should reject with InvalidJWTError if token issuer is invalid', async () => {
const initScope = nock(jwksUrl.origin).get(jwksUrl.pathname).reply(httpStatus.OK, { keys: jwks })
const token = signToken('key_1', tokenPayload, {
Expand Down Expand Up @@ -502,7 +513,7 @@ describe('Validator', () => {
it('Should resolve with the token payload', async () => {
const initScope = nock(jwksUrl.origin).get(jwksUrl.pathname).reply(httpStatus.OK, { keys: jwks })
const token = signToken('key_1', tokenPayload, {
audience: chance.pickone(validator.audience),
audience: [chance.pickone(validator.audience), chance.hash()],
issuer: validator.iss,
tokenUse: chance.pickone(validator.tokenUse)
})
Expand Down Expand Up @@ -537,5 +548,21 @@ describe('Validator', () => {
expect(validator.pems).to.deep.equal({ [jwk1.kid]: pems[jwk1.kid] })
expect(refreshScope.isDone()).to.be.true
})

it('Should resolve with the token payload when the token has no "aud" but has a valid "client_id"', async () => {
const initScope = nock(jwksUrl.origin).get(jwksUrl.pathname).reply(httpStatus.OK, { keys: jwks })
tokenPayload.client_id = chance.pickone(validator.audience)
const token = signToken('key_1', tokenPayload, {
issuer: validator.iss,
tokenUse: chance.pickone(validator.tokenUse)
})
const payload = await validator.validate(token)
expect(payload).to.be.an('object').that.has.all.keys(
'client_id', 'email', 'email_verified', 'exp', 'iat', 'iss', 'token_use'
)
const { email, email_verified, client_id } = payload // eslint-disable-line camelcase
expect({ email, email_verified, client_id }).to.deep.equal(tokenPayload)
expect(initScope.isDone()).to.be.true
})
})
})
8 changes: 6 additions & 2 deletions test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const fs = require('fs')
const path = require('path')
const jwt = require('jsonwebtoken')
// eslint-disable-next-line import/no-extraneous-dependencies
const { pem2jwk } = require('pem-jwk')
const { chance } = require('./index')
const { TOKEN_USE } = require('../src/lib/constants')
Expand Down Expand Up @@ -73,11 +74,11 @@ function generateConfig(opts = {}) {
* @param {string} keyId - The ID of the RSA key pair to use to sign the token.
* @param {Object} payload - The JWT payload.
* @param {Object} opts - Additional options to generate the JWT.
* @param {string} opts.audience - A value for the audience (aud) field.
* @param {string} opts.issuer - A value for the issuer (iss) field.
* @param {string} opts.tokenUse - A value for the token use (token_use) field. ('id' | 'access')
* @param {number} [opts.expiresIn = 3600] - The number of seconds until the token expires.
* @param {string} [opts.kid] - A custom value for the kid header.
* @param {string} [opts.audience] - A value for the audience (aud) field.
* @returns {string} The signed JWT.
* */
function signToken(keyId, payload, opts) {
Expand All @@ -89,10 +90,13 @@ function signToken(keyId, payload, opts) {
algorithm: 'RS256',
keyid: opts.kid || keyId,
expiresIn: opts.expiresIn || 3600,
audience: opts.audience,
issuer: opts.issuer
}

if (opts.audience) {
options.audience = opts.audience
}

const jwtPayload = { ...payload, token_use: opts.tokenUse }

return jwt.sign(jwtPayload, targetRSAKeyPair.private, options)
Expand Down

0 comments on commit 857a038

Please sign in to comment.