├── .prettierrc ├── .gitignore ├── opslevel.yml ├── .travis.yml ├── tsconfig.json ├── .eslintrc ├── .github └── workflows │ └── semgrep.yml ├── test ├── UnauthorizedError.test.ts ├── string_token.test.ts ├── revocation.test.ts ├── multitenancy.test.ts └── jwt.test.ts ├── src ├── errors │ └── UnauthorizedError.ts ├── util │ └── set.ts └── index.ts ├── LICENSE ├── bin └── changelog ├── package.json ├── CHANGELOG.md └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/* 3 | dist/* 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: iam_protocols 5 | tier: 6 | tags: 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | before_install: npm i -g npm@2 3 | node_js: 4 | - 8 5 | - 10 6 | - 12 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "allowJs": true, 5 | "target": "es5", 6 | "declaration": true, 7 | "esModuleInterop": false 8 | }, 9 | "include": [ 10 | "./src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "root": true, 4 | "parser": "@typescript-eslint/parser", 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/no-namespace": "off" 15 | }, 16 | "ignorePatterns": ["*.js"] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request_target: {} 5 | push: 6 | branches: ["master", "main"] 7 | permissions: 8 | contents: read 9 | jobs: 10 | semgrep: 11 | name: Scan 12 | runs-on: ubuntu-latest 13 | container: 14 | image: returntocorp/semgrep 15 | if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') 16 | steps: 17 | - uses: actions/checkout@v3 18 | - run: semgrep ci 19 | env: 20 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 21 | -------------------------------------------------------------------------------- /test/UnauthorizedError.test.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedError } from '../src/errors/UnauthorizedError'; 2 | import * as assert from 'assert'; 3 | 4 | describe('Unauthorized Error', () => { 5 | const e = new UnauthorizedError('credentials_bad_format', new Error('a')); 6 | 7 | it('should be an instance of UnauthorizedError', () => { 8 | assert.ok(e instanceof UnauthorizedError); 9 | }); 10 | 11 | it('should contains the error code', () => { 12 | assert.ok(e.code, 'credentials_bad_format'); 13 | }); 14 | 15 | it('should contains the error message', () => { 16 | assert.ok(e.code, 'a'); 17 | }); 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /src/errors/UnauthorizedError.ts: -------------------------------------------------------------------------------- 1 | export type ErrorLike = Error | { message: string }; 2 | 3 | type ErrorCode = 'credentials_bad_scheme' | 4 | 'credentials_bad_format' | 5 | 'credentials_required' | 6 | 'invalid_token' | 7 | 'revoked_token'; 8 | 9 | export class UnauthorizedError extends Error { 10 | readonly status: number; 11 | readonly inner: ErrorLike; 12 | readonly code: string; 13 | 14 | constructor(code: ErrorCode, error: ErrorLike) { 15 | super(error.message); 16 | Object.setPrototypeOf(this, UnauthorizedError.prototype); 17 | this.code = code; 18 | this.status = 401; 19 | this.name = 'UnauthorizedError'; 20 | this.inner = error; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/string_token.test.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as express from 'express'; 3 | import { expressjwt, ExpressJwtRequest } from '../src'; 4 | import * as assert from 'assert'; 5 | 6 | 7 | describe('string tokens', function () { 8 | const req = {} as ExpressJwtRequest; 9 | const res = {} as express.Response; 10 | 11 | it('should work with a valid string token', function (done) { 12 | const secret = 'shhhhhh'; 13 | const token = jwt.sign('foo', secret); 14 | 15 | req.headers = {}; 16 | req.headers.authorization = 'Bearer ' + token; 17 | expressjwt({ secret: secret, algorithms: ['HS256'] })(req, res, function () { 18 | assert.equal(req.auth, 'foo'); 19 | done(); 20 | }); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /src/util/set.ts: -------------------------------------------------------------------------------- 1 | // from https://stackoverflow.com/a/54733755 2 | export function set(obj: T, path: string | string[], value: unknown): T { 3 | if (typeof obj !== 'object') { 4 | return obj; 5 | } 6 | 7 | if (typeof path === 'string') { 8 | path = path.toString().match(/[^.[\]]+/g) || []; 9 | } 10 | 11 | path.slice(0, -1).reduce((a, c, i) => // Iterate all of them except the last one 12 | { 13 | return Object(a[c]) === a[c] // Does the key exist and is its value an object? 14 | 15 | // Yes: then follow that path 16 | ? a[c] 17 | // No: create the key. Is the next key a potential array-index? 18 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 19 | // @ts-ignore 20 | : a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] 21 | ? [] // Yes: assign a new array object 22 | : {}; 23 | }, // No: assign a new plain object 24 | obj)[path[path.length - 1]] = value; // Finally assign the value to the last key 25 | return obj; // Return the top-level object to allow chaining 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var changelog = require('conventional-changelog'); 4 | var semver_regex = /\bv?(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?\b/ig; 5 | 6 | const commitPartial = ` - {{header}} 7 | 8 | {{~!-- commit hash --}} {{#if @root.linkReferences}}([{{hash}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/{{@root.commit}}/{{hash}})){{else}}{{hash~}}{{/if}} 9 | 10 | {{~!-- commit references --}}{{#if references}}, closes{{~#each references}} {{#if @root.linkReferences}}[{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if this.repository}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}{{else}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}{{/if}}/{{@root.issue}}/{{this.issue}}){{else}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}{{/if}}{{/each}}{{/if}} 11 | `; 12 | 13 | const headerPartial = `## {{version}}{{#if title}} "{{title}}"{{/if}}{{#if date}} - {{date}}{{/if}} 14 | `; 15 | 16 | changelog({ 17 | releaseCount: 19, 18 | // preset: 'jshint' 19 | }, null, null, null, { 20 | transform: function (commit) { 21 | if (commit.header && semver_regex.exec(commit.header)) { 22 | return null; 23 | } 24 | return commit; 25 | }, 26 | commitPartial: commitPartial, 27 | headerPartial: headerPartial 28 | }).pipe(process.stdout); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-jwt", 3 | "version": "8.5.1", 4 | "description": "JWT authentication middleware.", 5 | "keywords": [ 6 | "auth", 7 | "authn", 8 | "authentication", 9 | "authz", 10 | "authorization", 11 | "http", 12 | "jwt", 13 | "token", 14 | "oauth", 15 | "express" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/auth0/express-jwt.git" 20 | }, 21 | "bugs": { 22 | "url": "http://github.com/auth0/express-jwt/issues" 23 | }, 24 | "author": { 25 | "name": "Matias Woloski", 26 | "email": "matias@auth0.com", 27 | "url": "https://www.auth0.com/" 28 | }, 29 | "license": "MIT", 30 | "main": "dist/index.js", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "README.md", 34 | "/dist" 35 | ], 36 | "dependencies": { 37 | "@types/jsonwebtoken": "^9", 38 | "express-unless": "^2.1.3", 39 | "jsonwebtoken": "^9.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/express": "^4.17.16", 43 | "@types/mocha": "^9.1.0", 44 | "@typescript-eslint/eslint-plugin": "^5.15.0", 45 | "@typescript-eslint/parser": "^5.15.0", 46 | "conventional-changelog": "^3.1.25", 47 | "eslint": "^8.11.0", 48 | "express": "^4.17.3", 49 | "mocha": "^10.2.0", 50 | "prettier": "^2.6.0", 51 | "ts-node": "^10.7.0", 52 | "typescript": "^4.6.2" 53 | }, 54 | "engines": { 55 | "node": ">= 8.0.0" 56 | }, 57 | "scripts": { 58 | "build": "rm -rf dist ; tsc", 59 | "prepare": "npm run build", 60 | "test": "mocha --reporter spec --require ts-node/register test/**", 61 | "lint": "eslint --fix --ext .ts ./src" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/revocation.test.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as express from 'express'; 3 | import { expressjwt, ExpressJwtRequest } from '../src'; 4 | import * as assert from 'assert'; 5 | 6 | describe('revoked jwts', function () { 7 | const secret = 'shhhhhh'; 8 | 9 | const revoked_id = '1234' 10 | 11 | const middleware = expressjwt({ 12 | secret: secret, 13 | algorithms: ['HS256'], 14 | isRevoked: async (req, token) => { 15 | const isRevoked = typeof token?.payload !== 'string' && 16 | token?.payload.jti === revoked_id; 17 | return isRevoked; 18 | } 19 | }); 20 | 21 | it('should throw if token is revoked', function () { 22 | const req = {} as ExpressJwtRequest; 23 | const res = {} as express.Response; 24 | 25 | const token = jwt.sign({ jti: revoked_id, foo: 'bar' }, secret); 26 | 27 | req.headers = {}; 28 | req.headers.authorization = 'Bearer ' + token; 29 | 30 | middleware(req, res, function (err) { 31 | assert.ok(err); 32 | assert.equal(err.code, 'revoked_token'); 33 | assert.equal(err.message, 'The token has been revoked.'); 34 | }); 35 | }); 36 | 37 | it('should work if token is not revoked', function () { 38 | const req = {} as ExpressJwtRequest; 39 | const res = {} as express.Response; 40 | 41 | const token = jwt.sign({ jti: '1233', foo: 'bar' }, secret); 42 | 43 | req.headers = {}; 44 | req.headers.authorization = 'Bearer ' + token; 45 | 46 | middleware(req, res, function () { 47 | assert.equal(req.auth.foo, 'bar'); 48 | }); 49 | }); 50 | 51 | it('should throw if error occurs checking if token is revoked', function (done) { 52 | const req = {} as ExpressJwtRequest; 53 | const res = {} as express.Response; 54 | 55 | const token = jwt.sign({ jti: revoked_id, foo: 'bar' }, secret); 56 | 57 | req.headers = {}; 58 | req.headers.authorization = 'Bearer ' + token; 59 | 60 | expressjwt({ 61 | secret: secret, 62 | algorithms: ['HS256'], 63 | isRevoked: async () => { 64 | throw new Error('An error ocurred'); 65 | } 66 | })(req, res, function (err) { 67 | assert.ok(err); 68 | assert.equal(err.message, 'An error ocurred'); 69 | done(); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/multitenancy.test.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as express from 'express'; 3 | import { expressjwt, ExpressJwtRequest, GetVerificationKey } from '../src'; 4 | import * as assert from 'assert'; 5 | 6 | describe('multitenancy', function () { 7 | const req = {} as ExpressJwtRequest; 8 | const res = {} as express.Response; 9 | 10 | const tenants = { 11 | 'a': { 12 | secret: 'secret-a' 13 | } 14 | }; 15 | 16 | const secretCallback: GetVerificationKey = function (req, token) { 17 | const issuer = (token.payload as jwt.JwtPayload).iss; 18 | if (tenants[issuer]) { 19 | return tenants[issuer].secret; 20 | } 21 | throw new Error('Could not find secret for issuer.'); 22 | }; 23 | 24 | const middleware = expressjwt({ 25 | secret: secretCallback, 26 | algorithms: ['HS256'] 27 | }); 28 | 29 | it('should retrieve secret using callback', function (done) { 30 | const token = jwt.sign({ foo: 'bar' }, tenants.a.secret, { issuer: 'a' }); 31 | 32 | req.headers = {}; 33 | req.headers.authorization = 'Bearer ' + token; 34 | 35 | middleware(req, res, function () { 36 | assert.equal(req.auth.foo, 'bar'); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should throw if an error ocurred when retrieving the token', function (done) { 42 | const secret = 'shhhhhh'; 43 | const token = jwt.sign({ iss: 'inexistent', foo: 'bar' }, secret); 44 | 45 | req.headers = {}; 46 | req.headers.authorization = 'Bearer ' + token; 47 | 48 | middleware(req, res, function (err) { 49 | assert.ok(err); 50 | assert.equal(err.message, 'Could not find secret for issuer.'); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should fail if token is revoked', function (done) { 56 | const token = jwt.sign({ iss: 'a', foo: 'bar' }, tenants.a.secret); 57 | 58 | req.headers = {}; 59 | req.headers.authorization = 'Bearer ' + token; 60 | 61 | expressjwt({ 62 | secret: secretCallback, 63 | algorithms: ['HS256'], 64 | isRevoked: async () => true 65 | })(req, res, function (err) { 66 | assert.ok(err); 67 | assert.equal(err.code, 'revoked_token'); 68 | assert.equal(err.message, 'The token has been revoked.'); 69 | done(); 70 | }); 71 | }); 72 | }); 73 | 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as express from 'express'; 3 | import { unless } from 'express-unless'; 4 | import { set } from './util/set'; 5 | 6 | import { UnauthorizedError } from './errors/UnauthorizedError'; 7 | 8 | /** 9 | * A function that defines how to retrieve the verification key given the express request and the JWT. 10 | */ 11 | export type GetVerificationKey = (req: express.Request, token: jwt.Jwt | undefined) => jwt.Secret | undefined | Promise; 12 | 13 | /** 14 | * @deprecated use GetVerificationKey 15 | */ 16 | export type SecretCallback = GetVerificationKey; 17 | 18 | /** 19 | * @deprecated use GetVerificationKey 20 | */ 21 | export type SecretCallbackLong = GetVerificationKey; 22 | 23 | /** 24 | * A function to check if a token is revoked 25 | */ 26 | export type IsRevoked = (req: express.Request, token: jwt.Jwt | undefined) => boolean | Promise; 27 | 28 | /** 29 | * A function to check if a token is revoked 30 | */ 31 | export type ExpirationHandler = (req: express.Request, err: UnauthorizedError) => void | Promise; 32 | 33 | /** 34 | * A function to customize how a token is retrieved from the express request. 35 | */ 36 | export type TokenGetter = (req: express.Request) => string | Promise | undefined; 37 | 38 | export type Params = { 39 | /** 40 | * The Key or a function to retrieve the key used to verify the JWT. 41 | */ 42 | secret: jwt.Secret | GetVerificationKey, 43 | 44 | /** 45 | * Defines how to retrieves the token from the request object. 46 | */ 47 | getToken?: TokenGetter, 48 | 49 | /** 50 | * Defines how to verify if a token is revoked. 51 | */ 52 | isRevoked?: IsRevoked, 53 | 54 | /** 55 | * If sets to true, continue to the next middleware when the 56 | * request doesn't include a token without failing. 57 | * 58 | * @default true 59 | */ 60 | credentialsRequired?: boolean, 61 | 62 | /** 63 | * Allows to customize the name of the property in the request object 64 | * where the decoded payload is set. 65 | * @default 'auth' 66 | */ 67 | requestProperty?: string, 68 | 69 | /** 70 | * List of JWT algorithms allowed. 71 | */ 72 | algorithms: jwt.Algorithm[], 73 | 74 | /** 75 | * Handle expired tokens. 76 | */ 77 | onExpired?: ExpirationHandler, 78 | } & jwt.VerifyOptions; 79 | 80 | export { UnauthorizedError } from './errors/UnauthorizedError'; 81 | 82 | /** 83 | * @deprecated this breaks tsc when using strict: true 84 | */ 85 | export type ExpressJwtRequest = 86 | express.Request & { auth: T } 87 | 88 | /** 89 | * @deprecated use Request 90 | */ 91 | export type ExpressJwtRequestUnrequired = 92 | express.Request & { auth?: T } 93 | 94 | /** 95 | * The Express Request including the "auth" property with the decoded JWT payload. 96 | */ 97 | export type Request = 98 | express.Request & { auth?: T }; 99 | 100 | /** 101 | * Returns an express middleware to verify JWTs. 102 | * 103 | * @param options {Params} 104 | * @returns 105 | */ 106 | export const expressjwt = (options: Params) => { 107 | if (!options?.secret) throw new RangeError('express-jwt: `secret` is a required option'); 108 | if (!options.algorithms) throw new RangeError('express-jwt: `algorithms` is a required option'); 109 | if (!Array.isArray(options.algorithms)) throw new RangeError('express-jwt: `algorithms` must be an array'); 110 | 111 | const getVerificationKey: GetVerificationKey = 112 | typeof options.secret === 'function' ? 113 | options.secret : 114 | async () => options.secret as jwt.Secret; 115 | 116 | const credentialsRequired = typeof options.credentialsRequired === 'undefined' ? true : options.credentialsRequired; 117 | const requestProperty = typeof options.requestProperty === 'string' ? options.requestProperty : 'auth'; 118 | 119 | const middleware = async function (req: express.Request, res: express.Response, next: express.NextFunction): Promise { 120 | let token: string; 121 | try { 122 | if (req.method === 'OPTIONS' && 'access-control-request-headers' in req.headers) { 123 | const hasAuthInAccessControl = req.headers['access-control-request-headers'] 124 | .split(',') 125 | .map(header => header.trim().toLowerCase()) 126 | .includes('authorization'); 127 | if (hasAuthInAccessControl) { 128 | setImmediate(next); 129 | return; 130 | } 131 | } 132 | 133 | const authorizationHeader = req.headers && 'Authorization' in req.headers ? 'Authorization' : 'authorization'; 134 | if (options.getToken && typeof options.getToken === 'function') { 135 | token = await options.getToken(req); 136 | } else if (req.headers && req.headers[authorizationHeader]) { 137 | const parts = (req.headers[authorizationHeader] as string).split(' '); 138 | if (parts.length == 2) { 139 | const scheme = parts[0]; 140 | const credentials = parts[1]; 141 | 142 | if (/^Bearer$/i.test(scheme)) { 143 | token = credentials; 144 | } else { 145 | if (credentialsRequired) { 146 | throw new UnauthorizedError('credentials_bad_scheme', { message: 'Format is Authorization: Bearer [token]' }); 147 | } else { 148 | return next(); 149 | } 150 | } 151 | } else { 152 | throw new UnauthorizedError('credentials_bad_format', { message: 'Format is Authorization: Bearer [token]' }); 153 | } 154 | } 155 | 156 | if (!token) { 157 | if (credentialsRequired) { 158 | throw new UnauthorizedError('credentials_required', { message: 'No authorization token was found' }); 159 | } else { 160 | return next(); 161 | } 162 | } 163 | 164 | let decodedToken: jwt.Jwt; 165 | 166 | try { 167 | decodedToken = jwt.decode(token, { complete: true }); 168 | } catch (err) { 169 | throw new UnauthorizedError('invalid_token', err); 170 | } 171 | 172 | const key = await getVerificationKey(req, decodedToken); 173 | 174 | try { 175 | await jwt.verify(token, key, options); 176 | } catch (err) { 177 | const wrappedErr = new UnauthorizedError('invalid_token', err); 178 | if (err instanceof jwt.TokenExpiredError && typeof options.onExpired === 'function') { 179 | await options.onExpired(req, wrappedErr); 180 | } else { 181 | throw wrappedErr; 182 | } 183 | } 184 | 185 | const isRevoked = options.isRevoked && await options.isRevoked(req, decodedToken) || false; 186 | if (isRevoked) { 187 | throw new UnauthorizedError('revoked_token', { message: 'The token has been revoked.' }); 188 | } 189 | 190 | const request = req as Request; 191 | set(request, requestProperty, decodedToken.payload); 192 | setImmediate(next); 193 | } catch (err) { 194 | setImmediate(next, err); 195 | } 196 | }; 197 | 198 | middleware.unless = unless; 199 | 200 | return middleware; 201 | } 202 | 203 | 204 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file starting from version **v4.0.0**. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## 8.3.0 - 2023-01-04 7 | 8 | - requestProperty support for nested properties ([bbd3606ce68da2602733d6e4ac32564570753ca1](https://github.com/auth0/express-jwt/commit/bbd3606ce68da2602733d6e4ac32564570753ca1)) 9 | - Update Typescript instructions in Readme.MD ([3c1d5cf8a08a6afbcfc78640b8cdb26fac8002ca](https://github.com/auth0/express-jwt/commit/3c1d5cf8a08a6afbcfc78640b8cdb26fac8002ca)) 10 | 11 | ## 8.2.1 - 2022-12-26 12 | 13 | - add secret rotation example in readme. close #310 ([0000a44ed58aac97798007af19b0324f28acc436](https://github.com/auth0/express-jwt/commit/0000a44ed58aac97798007af19b0324f28acc436)), closes [#310](https://github.com/auth0/express-jwt/issues/310) 14 | - update @types/jsonwebtoken and fix deps in package-lock ([2322a9b67a5b5c716f953a53a0bb4bbc696d0a11](https://github.com/auth0/express-jwt/commit/2322a9b67a5b5c716f953a53a0bb4bbc696d0a11)) 15 | 16 | ## 8.2.0 - 2022-12-22 17 | 18 | - add an optional handler for expired tokens. closes #6048 ([ca6c90ccbb4b61b91f417a5dfa56f0b931b81528](https://github.com/auth0/express-jwt/commit/ca6c90ccbb4b61b91f417a5dfa56f0b931b81528)), closes [#6048](https://github.com/auth0/express-jwt/issues/6048) 19 | 20 | ## 8.1.0 - 2022-12-22 21 | 22 | - update type to match jwks-rsa ([bcad8af9cad82b3777cc38d1c05864a35f82bc53](https://github.com/auth0/express-jwt/commit/bcad8af9cad82b3777cc38d1c05864a35f82bc53)) 23 | - feat: export middleware options type. closes #308 ([25a30f0d50c02cc75ab17b09f3592e76e09f9666](https://github.com/auth0/express-jwt/commit/25a30f0d50c02cc75ab17b09f3592e76e09f9666)), closes [#308](https://github.com/auth0/express-jwt/issues/308) 24 | 25 | ## 8.0.0 - 2022-12-22 26 | 27 | - Upgrade jsonwebtoken to v9. https://github.com/advisories/GHSA-27h2-hvpr-p74q . 28 | 29 | ## 7.7.3 - 2022-05-30 30 | 31 | - Fix tsc build error for express-unless ([e1fe1d264bc5363e008d23fea9d8c5d2ac0d8198](https://github.com/auth0/express-jwt/commit/e1fe1d264bc5363e008d23fea9d8c5d2ac0d8198)) 32 | - Remove esModuleInterop and fix assert import in tests ([9ccf0cfd6aaa4cc61fce2f8ccdb961c4b0358201](https://github.com/auth0/express-jwt/commit/9ccf0cfd6aaa4cc61fce2f8ccdb961c4b0358201)) 33 | 34 | ## 7.7.2 - 2022-05-19 35 | 36 | - fix instaceof comparison for UnauthorizedError. closes #292 ([6c87fe401ecba868feda1ffa530082c7c539321a](https://github.com/auth0/express-jwt/commit/6c87fe401ecba868feda1ffa530082c7c539321a)), closes [#292](https://github.com/auth0/express-jwt/issues/292) 37 | - update changelog ([b1344fa7f6f9dd3d27115a9107b3ef4323733895](https://github.com/auth0/express-jwt/commit/b1344fa7f6f9dd3d27115a9107b3ef4323733895)) 38 | 39 | ## 7.7.1 - 2022-05-13 40 | 41 | - fix readme and package-lock ([7a02ca76c5d7842cfa8b256dcc89dcef1ffbcdc1](https://github.com/auth0/express-jwt/commit/7a02ca76c5d7842cfa8b256dcc89dcef1ffbcdc1)) 42 | - build(deps): required runtime types ([f3f5af5c214241b4f92b91c49db8586ec20e4526](https://github.com/auth0/express-jwt/commit/f3f5af5c214241b4f92b91c49db8586ec20e4526)) 43 | - docs: fix tiny typo ([07e771857489b6344a8dc457069d040a76e84230](https://github.com/auth0/express-jwt/commit/07e771857489b6344a8dc457069d040a76e84230)) 44 | 45 | ## 7.7.0 - 2022-05-06 46 | 47 | - deprecate ExpressJwtRequest in favor of Request with optional auth, closes #284 ([de169def56f98f4237741aa6755d0c5e248bd561](https://github.com/auth0/express-jwt/commit/de169def56f98f4237741aa6755d0c5e248bd561)), closes [#284](https://github.com/auth0/express-jwt/issues/284) 48 | 49 | ## 7.6.2 - 2022-05-02 50 | 51 | - remove undefined from algorhitms fix #285 ([587238bd0ad7a59f784daf9f626b9bf9abc7e029](https://github.com/auth0/express-jwt/commit/587238bd0ad7a59f784daf9f626b9bf9abc7e029)), closes [#285](https://github.com/auth0/express-jwt/issues/285) 52 | 53 | ## 7.6.1 - 2022-05-02 54 | 55 | - add note about @types/jsonwebtoken in readme ([03c8419d6fc78c9029a7b474d3aede7f94e80121](https://github.com/auth0/express-jwt/commit/03c8419d6fc78c9029a7b474d3aede7f94e80121)) 56 | - make algorithms a required parameter in types. closes #285 ([097a1df0d7ba511afce9578e4cf45bca2589b253](https://github.com/auth0/express-jwt/commit/097a1df0d7ba511afce9578e4cf45bca2589b253)), closes [#285](https://github.com/auth0/express-jwt/issues/285) 57 | - update changelog ([9d0f02debb7a3db83edbc9f9b4b6d46993e6a4f4](https://github.com/auth0/express-jwt/commit/9d0f02debb7a3db83edbc9f9b4b6d46993e6a4f4)) 58 | 59 | ## 7.6.0 - 2022-05-02 60 | 61 | - add ExpressJwtRequestUnrequired to the readme ([3890f53f87b0a84dccaafd8de5a43d3c1dfeae89](https://github.com/auth0/express-jwt/commit/3890f53f87b0a84dccaafd8de5a43d3c1dfeae89)) 62 | - add SecretCallback[Long] back for backward compatibility ([c24078e285908cad1c2ac0e63482a75ebf7d7328](https://github.com/auth0/express-jwt/commit/c24078e285908cad1c2ac0e63482a75ebf7d7328)) 63 | - update changelog ([d3a8e80dec3a6c261f840ad763487a16a47bbc4b](https://github.com/auth0/express-jwt/commit/d3a8e80dec3a6c261f840ad763487a16a47bbc4b)) 64 | 65 | ## 7.5.2 - 2022-04-27 66 | 67 | - export another type for credentialsRequired: false / ExpressJwtRequestUnrequired ([1bdb6f3d0cc5f61b7a7b097f700d20cb337d4bef](https://github.com/auth0/express-jwt/commit/1bdb6f3d0cc5f61b7a7b097f700d20cb337d4bef)) 68 | 69 | ## 7.5.1 - 2022-04-27 70 | 71 | - make req.auth optional in the ExpressJwtRequest type ([496fda4a0a20292ca70055b6ab8fdf50414ffa2b](https://github.com/auth0/express-jwt/commit/496fda4a0a20292ca70055b6ab8fdf50414ffa2b)) 72 | - update changelog ([727b57ddfec1f1c5ee4e16cb335ad1ae5a3c131f](https://github.com/auth0/express-jwt/commit/727b57ddfec1f1c5ee4e16cb335ad1ae5a3c131f)) 73 | 74 | ## 7.5.0 - 2022-04-25 75 | 76 | - export TokenGetter ([eb7479b834fb0e052ffad4279394ce353bb13770](https://github.com/auth0/express-jwt/commit/eb7479b834fb0e052ffad4279394ce353bb13770)) 77 | - improve readme and some types. Closes #283 ([1a67f69c8781179d3ce7e5f3de8ece40d31c1772](https://github.com/auth0/express-jwt/commit/1a67f69c8781179d3ce7e5f3de8ece40d31c1772)), closes [#283](https://github.com/auth0/express-jwt/issues/283) 78 | - restore requestProperty ([bf143d07497046b3e7921d3dd4bcbc18e2daeb67](https://github.com/auth0/express-jwt/commit/bf143d07497046b3e7921d3dd4bcbc18e2daeb67)) 79 | 80 | ## 7.4.3 - 2022-04-21 81 | 82 | - improve readme ([bd2515bec698604c645decd5be93e4f401263662](https://github.com/auth0/express-jwt/commit/bd2515bec698604c645decd5be93e4f401263662)) 83 | 84 | ## 7.4.2 - 2022-04-20 85 | 86 | - include '/dist' in package, closes #280 ([cf2665d5581e76ed5742e7c2f34b8d05f91cfd18](https://github.com/auth0/express-jwt/commit/cf2665d5581e76ed5742e7c2f34b8d05f91cfd18)), closes [#280](https://github.com/auth0/express-jwt/issues/280) 87 | 88 | ## 7.4.1 - 2022-04-20 89 | 90 | - fix readme definition for revoked and secret callbacks ([9015cf729cfbbf1b28a9646cccbf26d523dce1de](https://github.com/auth0/express-jwt/commit/9015cf729cfbbf1b28a9646cccbf26d523dce1de)) 91 | - update changelog ([05d7a78baaf76a2a881a95666b0ec7349729d957](https://github.com/auth0/express-jwt/commit/05d7a78baaf76a2a881a95666b0ec7349729d957)) 92 | 93 | ## 7.4.0 - 2022-04-20 94 | 95 | - handle authorization header in cors when is upper cased. fixes #180, #173 ([ab0ee806416e3a5a48ef8a1017a298e1a666b17a](https://github.com/auth0/express-jwt/commit/ab0ee806416e3a5a48ef8a1017a298e1a666b17a)), closes [#180](https://github.com/auth0/express-jwt/issues/180) [#173](https://github.com/auth0/express-jwt/issues/173) 96 | 97 | ## 7.3.0 - 2022-04-20 98 | 99 | - add support for capital Authorization header. closes #200 ([6c0698b513e11ff1d4b152e070a627f5092be801](https://github.com/auth0/express-jwt/commit/6c0698b513e11ff1d4b152e070a627f5092be801)), closes [#200](https://github.com/auth0/express-jwt/issues/200) 100 | 101 | ## 7.2.0 - 2022-04-20 102 | 103 | - Add example on how to enable jwt for specific path ([280511342522f11a90da93187a44af1a1b3cf5eb](https://github.com/auth0/express-jwt/commit/280511342522f11a90da93187a44af1a1b3cf5eb)) 104 | - Fix link to auth0.com ([b04cb9dea9a9fb2dc999a8dbf30ba6f204f50d15](https://github.com/auth0/express-jwt/commit/b04cb9dea9a9fb2dc999a8dbf30ba6f204f50d15)) 105 | - remove travis badge ([a854342c28f7186ec70e298124b4d650a26767b2](https://github.com/auth0/express-jwt/commit/a854342c28f7186ec70e298124b4d650a26767b2)) 106 | - Update docs to continue error handling on mismatch ([627b358d07b19d299964a5ef18a772db9b6426e2](https://github.com/auth0/express-jwt/commit/627b358d07b19d299964a5ef18a772db9b6426e2)) 107 | - Update README.md ([8d7af267189a49f42b88807d236647bd7398fde3](https://github.com/auth0/express-jwt/commit/8d7af267189a49f42b88807d236647bd7398fde3)) 108 | 109 | ## 7.1.0 - 2022-04-20 110 | 111 | - add support for async, closes #249 ([72236ec1cfb0e7847351c83908be1d84141e30f1](https://github.com/auth0/express-jwt/commit/72236ec1cfb0e7847351c83908be1d84141e30f1)), closes [#249](https://github.com/auth0/express-jwt/issues/249) 112 | - update changelog ([cb50ed43b2de9ae9be8643e7834640fa912ef367](https://github.com/auth0/express-jwt/commit/cb50ed43b2de9ae9be8643e7834640fa912ef367)) 113 | 114 | ## 7.0.0 - 2022-04-20 115 | 116 | - Convert the project to typescript and improve typescript ([2b43ccb7252f2cc2fb3c2655a252fd7ae58ce0dd](https://github.com/auth0/express-jwt/commit/2b43ccb7252f2cc2fb3c2655a252fd7ae58ce0dd)) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-jwt 2 | 3 | This module provides Express middleware for validating JWTs ([JSON Web Tokens](https://jwt.io)) through the [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken/) module. The decoded JWT payload is available on the request object. 4 | 5 | ## Install 6 | 7 | ``` 8 | $ npm install express-jwt 9 | ``` 10 | 11 | ## API 12 | 13 | `expressjwt(options)` 14 | 15 | Options has the following parameters: 16 | 17 | - `secret: jwt.Secret | GetVerificationKey` (required): The secret as a string or a function to retrieve the secret. 18 | - `getToken?: TokenGetter` (optional): A function that receives the express `Request` and returns the token, by default it looks in the `Authorization` header. 19 | - `isRevoked?: IsRevoked` (optional): A function to verify if a token is revoked. 20 | - `onExpired?: ExpirationHandler` (optional): A function to handle expired tokens. 21 | - `credentialsRequired?: boolean` (optional): If its false, continue to the next middleware if the request does not contain a token instead of failing, defaults to true. 22 | - `requestProperty?: string` (optional): Name of the property in the request object where the payload is set. Default to `req.auth`. 23 | - Plus... all the options available in the [jsonwebtoken verify function](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback). 24 | 25 | The available functions have the following interface: 26 | 27 | - `GetVerificationKey = (req: express.Request, token: jwt.Jwt | undefined) => Promise;` 28 | - `IsRevoked = (req: express.Request, token: jwt.Jwt | undefined) => Promise;` 29 | - `TokenGetter = (req: express.Request) => string | Promise | undefined;` 30 | 31 | ## Usage 32 | 33 | Basic usage using an HS256 secret: 34 | 35 | ```javascript 36 | var { expressjwt: jwt } = require("express-jwt"); 37 | // or ES6 38 | // import { expressjwt, ExpressJwtRequest } from "express-jwt"; 39 | 40 | app.get( 41 | "/protected", 42 | jwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }), 43 | function (req, res) { 44 | if (!req.auth.admin) return res.sendStatus(401); 45 | res.sendStatus(200); 46 | } 47 | ); 48 | ``` 49 | 50 | The decoded JWT payload is available on the request via the `auth` property. 51 | 52 | > The default behavior of the module is to extract the JWT from the `Authorization` header as an [OAuth2 Bearer token](https://oauth.net/2/bearer-tokens/). 53 | 54 | ### Required Parameters 55 | 56 | The `algorithms` parameter is required to prevent potential downgrade attacks when providing third party libraries as **secrets**. 57 | 58 | :warning: **Do not mix symmetric and asymmetric (ie HS256/RS256) algorithms**: Mixing algorithms without further validation can potentially result in downgrade vulnerabilities. 59 | 60 | ```javascript 61 | jwt({ 62 | secret: "shhhhhhared-secret", 63 | algorithms: ["HS256"], 64 | //algorithms: ['RS256'] 65 | }); 66 | ``` 67 | 68 | ### Additional Options 69 | 70 | You can specify audience and/or issuer as well, which is highly recommended for security purposes: 71 | 72 | ```javascript 73 | jwt({ 74 | secret: "shhhhhhared-secret", 75 | audience: "http://myapi/protected", 76 | issuer: "http://issuer", 77 | algorithms: ["HS256"], 78 | }); 79 | ``` 80 | 81 | > If the JWT has an expiration (`exp`), it will be checked. 82 | 83 | If you are using a base64 URL-encoded secret, pass a `Buffer` with `base64` encoding as the secret instead of a string: 84 | 85 | ```javascript 86 | jwt({ 87 | secret: Buffer.from("shhhhhhared-secret", "base64"), 88 | algorithms: ["RS256"], 89 | }); 90 | ``` 91 | 92 | To only protect specific paths (e.g. beginning with `/api`), use [express router](https://expressjs.com/en/4x/api.html#app.use) call `use`, like so: 93 | 94 | ```javascript 95 | app.use("/api", jwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] })); 96 | ``` 97 | 98 | Or, the other way around, if you want to make some paths unprotected, call `unless` like so. 99 | 100 | ```javascript 101 | app.use( 102 | jwt({ 103 | secret: "shhhhhhared-secret", 104 | algorithms: ["HS256"], 105 | }).unless({ path: ["/token"] }) 106 | ); 107 | ``` 108 | 109 | This is especially useful when applying to multiple routes. In the example above, `path` can be a string, a regexp, or an array of any of those. 110 | 111 | > For more details on the `.unless` syntax including additional options, please see [express-unless](https://github.com/jfromaniello/express-unless). 112 | 113 | This module also support tokens signed with public/private key pairs. Instead of a secret, you can specify a Buffer with the public key 114 | 115 | ```javascript 116 | var publicKey = fs.readFileSync("/path/to/public.pub"); 117 | jwt({ secret: publicKey, algorithms: ["RS256"] }); 118 | ``` 119 | 120 | ### Customizing Token Location 121 | 122 | A custom function for extracting the token from a request can be specified with 123 | the `getToken` option. This is useful if you need to pass the token through a 124 | query parameter or a cookie. You can throw an error in this function and it will 125 | be handled by `express-jwt`. 126 | 127 | ```javascript 128 | app.use( 129 | jwt({ 130 | secret: "hello world !", 131 | algorithms: ["HS256"], 132 | credentialsRequired: false, 133 | getToken: function fromHeaderOrQuerystring(req) { 134 | if ( 135 | req.headers.authorization && 136 | req.headers.authorization.split(" ")[0] === "Bearer" 137 | ) { 138 | return req.headers.authorization.split(" ")[1]; 139 | } else if (req.query && req.query.token) { 140 | return req.query.token; 141 | } 142 | return null; 143 | }, 144 | }) 145 | ); 146 | ``` 147 | 148 | ### Retrieve key dynamically 149 | 150 | If you need to obtain the key dynamically from other sources, you can pass a function in the `secret` parameter with the following parameters: 151 | 152 | - `req` (`Object`) - The express `request` object. 153 | - `token` (`Object`) - An object with the JWT payload and headers. 154 | 155 | For example, if the secret varies based on the [issuer](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#issDef): 156 | 157 | ```javascript 158 | var jwt = require("express-jwt"); 159 | var data = require("./data"); 160 | var utilities = require("./utilities"); 161 | 162 | var getSecret = async function (req, token) { 163 | const issuer = token.payload.iss; 164 | const tenant = await data.getTenantByIdentifier(issuer); 165 | if (!tenant) { 166 | throw new Error("missing_secret"); 167 | } 168 | return utilities.decrypt(tenant.secret); 169 | }; 170 | 171 | app.get( 172 | "/protected", 173 | jwt({ secret: getSecret, algorithms: ["HS256"] }), 174 | function (req, res) { 175 | if (!req.auth.admin) return res.sendStatus(401); 176 | res.sendStatus(200); 177 | } 178 | ); 179 | ``` 180 | 181 | ### Secret rotation 182 | 183 | The getSecret callback could also be used in cases where the same issuer might issue tokens with different keys at certain point: 184 | 185 | ```js 186 | var getSecret = async function (req, token) { 187 | const { iss } = token.payload; 188 | const { kid } = token.header; 189 | // get the verification key by a given key-id and issuer. 190 | return verificationKey; 191 | }; 192 | ``` 193 | 194 | ### Revoked tokens 195 | 196 | It is possible that some tokens will need to be revoked so they cannot be used any longer. You can provide a function as the `isRevoked` option. The signature of the function is `function(req, payload, done)`: 197 | 198 | - `req` (`Object`) - The express `request` object. 199 | - `token` (`Object`) - An object with the JWT payload and headers. 200 | 201 | For example, if the `(iss, jti)` claim pair is used to identify a JWT: 202 | 203 | ```javascript 204 | const jwt = require("express-jwt"); 205 | const data = require("./data"); 206 | 207 | const isRevokedCallback = async (req, token) => { 208 | const issuer = token.payload.iss; 209 | const tokenId = token.payload.jti; 210 | const token = await data.getRevokedToken(issuer, tokenId); 211 | return token !== "undefined"; 212 | }; 213 | 214 | app.get( 215 | "/protected", 216 | jwt({ 217 | secret: "shhhhhhared-secret", 218 | algorithms: ["HS256"], 219 | isRevoked: isRevokedCallback, 220 | }), 221 | function (req, res) { 222 | if (!req.auth.admin) return res.sendStatus(401); 223 | res.sendStatus(200); 224 | } 225 | ); 226 | ``` 227 | 228 | ### Handling expired tokens 229 | 230 | You can handle expired tokens as follows: 231 | 232 | ```javascript 233 | jwt({ 234 | secret: "shhhhhhared-secret", 235 | algorithms: ["HS256"], 236 | onExpired: async (req, err) => { 237 | if (new Date() - err.inner.expiredAt < 5000) { return;} 238 | throw err; 239 | },, 240 | }) 241 | ``` 242 | 243 | ### Error handling 244 | 245 | The default behavior is to throw an error when the token is invalid, so you can add your custom logic to manage unauthorized access as follows: 246 | 247 | ```javascript 248 | app.use(function (err, req, res, next) { 249 | if (err.name === "UnauthorizedError") { 250 | res.status(401).send("invalid token..."); 251 | } else { 252 | next(err); 253 | } 254 | }); 255 | ``` 256 | 257 | You might want to use this module to identify registered users while still providing access to unregistered users. You can do this by using the option `credentialsRequired`: 258 | 259 | ```javascript 260 | app.use( 261 | jwt({ 262 | secret: "hello world !", 263 | algorithms: ["HS256"], 264 | credentialsRequired: false, 265 | }) 266 | ); 267 | ``` 268 | 269 | ## Typescript 270 | 271 | A `Request` type is provided from `express-jwt`, which extends `express.Request` with the `auth` property. It could be aliased, like how `JWTRequest` is below. 272 | 273 | ```typescript 274 | import { expressjwt, Request as JWTRequest } from "express-jwt"; 275 | 276 | app.get( 277 | "/protected", 278 | expressjwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }), 279 | function (req: JWTRequest, res: express.Response) { 280 | if (!req.auth?.admin) return res.sendStatus(401); 281 | res.sendStatus(200); 282 | } 283 | ); 284 | ``` 285 | 286 | ## Migration from v6 287 | 288 | 1. The middleware function is now available as a named import rather than a default one: import { expressjwt } from 'express-jwt' 289 | 2. The decoded JWT payload is now available as req.auth rather than req.user 290 | 3. The `secret` function had `(req, header, payload, cb)`, now it can return a promise and receives `(req, token)`. `token` has `header` and `payload`. 291 | 4. The `isRevoked` function had `(req, payload, cb)`, now it can return a promise and receives `(req, token)`. `token` has `header` and `payload`. 292 | 293 | ## Related Modules 294 | 295 | - [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) — JSON Web Token sign and verification 296 | - [express-jwt-permissions](https://github.com/MichielDeMey/express-jwt-permissions) - Permissions middleware for JWT tokens 297 | 298 | ## Tests 299 | 300 | ``` 301 | $ npm install 302 | $ npm test 303 | ``` 304 | 305 | ## Contributors 306 | 307 | Check them out [here](https://github.com/auth0/express-jwt/graphs/contributors) 308 | 309 | ## Issue Reporting 310 | 311 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 312 | 313 | ## Author 314 | 315 | [Auth0](https://auth0.com) 316 | 317 | ## License 318 | 319 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 320 | -------------------------------------------------------------------------------- /test/jwt.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import * as jwt from 'jsonwebtoken'; 3 | import * as express from 'express'; 4 | import { expressjwt, UnauthorizedError, Request, GetVerificationKey } from '../src'; 5 | import * as assert from 'assert'; 6 | 7 | 8 | describe('failure tests', function () { 9 | const req = {} as express.Request; 10 | const res = {} as express.Response; 11 | 12 | it('should throw if options not sent', function () { 13 | try { 14 | // @ts-ignore 15 | expressjwt(); 16 | } catch (e) { 17 | assert.ok(e); 18 | assert.equal(e.message, "express-jwt: `secret` is a required option"); 19 | } 20 | }); 21 | 22 | it('should throw if algorithms is not sent', function () { 23 | try { 24 | // @ts-ignore 25 | expressjwt({ secret: 'shhhh' }); 26 | } catch (e) { 27 | assert.ok(e); 28 | assert.equal(e.message, 'express-jwt: `algorithms` is a required option'); 29 | } 30 | }); 31 | 32 | it('should throw if algorithms is not an array', function () { 33 | try { 34 | // @ts-ignore 35 | expressjwt({ secret: 'shhhh', algorithms: 'foo' }); 36 | } catch (e) { 37 | assert.ok(e); 38 | assert.equal(e.message, 'express-jwt: `algorithms` must be an array'); 39 | } 40 | }); 41 | 42 | it('should throw if no authorization header and credentials are required', function (done) { 43 | expressjwt({ secret: 'shhhh', credentialsRequired: true, algorithms: ['HS256'] })(req, res, function (err) { 44 | assert.ok(err); 45 | assert.equal(err.code, 'credentials_required'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('support unless skip', function (done) { 51 | req.originalUrl = '/index.html'; 52 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] }).unless({ path: '/index.html' })(req, res, function (err) { 53 | assert.ok(!err); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should skip on CORS preflight', function (done) { 59 | const corsReq = {} as express.Request; 60 | corsReq.method = 'OPTIONS'; 61 | corsReq.headers = { 62 | 'access-control-request-headers': 'sasa, sras, authorization' 63 | }; 64 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] })(corsReq, res, function (err) { 65 | assert.ok(!err); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('should throw if authorization header is malformed', function (done) { 71 | req.headers = {}; 72 | req.headers.authorization = 'wrong'; 73 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] })(req, res, function (err) { 74 | assert.ok(err); 75 | assert.equal(err.code, 'credentials_bad_format'); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('should throw if authorization header is not Bearer', function () { 81 | req.headers = {}; 82 | req.headers.authorization = 'Basic foobar'; 83 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] })(req, res, function (err) { 84 | assert.ok(err); 85 | assert.equal(err.code, 'credentials_bad_scheme'); 86 | }); 87 | }); 88 | 89 | it('should next if authorization header is not Bearer and credentialsRequired is false', function (done) { 90 | req.headers = {}; 91 | req.headers.authorization = 'Basic foobar'; 92 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'], credentialsRequired: false })(req, res, function (err) { 93 | assert.ok(typeof err === 'undefined'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should throw if authorization header is not well-formatted jwt', function (done) { 99 | req.headers = {}; 100 | req.headers.authorization = 'Bearer wrongjwt'; 101 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] })(req, res, function (err) { 102 | assert.ok(err); 103 | assert.equal(err.code, 'invalid_token'); 104 | done(); 105 | }); 106 | }); 107 | 108 | it('should throw if jwt is an invalid json', function (done) { 109 | req.headers = {}; 110 | req.headers.authorization = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.yJ1c2VybmFtZSI6InNhZ3VpYXIiLCJpYXQiOjE0NzEwMTg2MzUsImV4cCI6MTQ3MzYxMDYzNX0.foo'; 111 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] })(req, res, function (err) { 112 | assert.ok(err); 113 | assert.equal(err.code, 'invalid_token'); 114 | done(); 115 | }); 116 | }); 117 | 118 | it('should throw if authorization header is not valid jwt', function (done) { 119 | const secret = 'shhhhhh'; 120 | const token = jwt.sign({ foo: 'bar' }, secret); 121 | 122 | req.headers = {}; 123 | req.headers.authorization = 'Bearer ' + token; 124 | expressjwt({ secret: 'different-shhhh', algorithms: ['HS256'] })(req, res, function (err) { 125 | assert.ok(err); 126 | assert.equal(err.code, 'invalid_token'); 127 | assert.equal(err.message, 'invalid signature'); 128 | done() 129 | }); 130 | }); 131 | 132 | it('should throw if audience is not expected', function (done) { 133 | const secret = 'shhhhhh'; 134 | const token = jwt.sign({ foo: 'bar', aud: 'expected-audience' }, secret, { expiresIn: 500 }); 135 | 136 | req.headers = {}; 137 | req.headers.authorization = 'Bearer ' + token; 138 | expressjwt({ secret: 'shhhhhh', algorithms: ['HS256'], audience: 'not-expected-audience' })(req, res, function (err) { 139 | assert.ok(err); 140 | assert.equal(err.code, 'invalid_token'); 141 | assert.equal(err.message, 'jwt audience invalid. expected: not-expected-audience'); 142 | done(); 143 | }); 144 | }); 145 | 146 | it('should throw if token is expired', function (done) { 147 | const secret = 'shhhhhh'; 148 | const token = jwt.sign({ foo: 'bar', exp: 1382412921 }, secret); 149 | 150 | req.headers = {}; 151 | req.headers.authorization = 'Bearer ' + token; 152 | expressjwt({ secret: 'shhhhhh', algorithms: ['HS256'] })(req, res, function (err) { 153 | assert.ok(err); 154 | assert.equal(err.code, 'invalid_token'); 155 | assert.equal(err.inner.name, 'TokenExpiredError'); 156 | assert.equal(err.message, 'jwt expired'); 157 | done(); 158 | }); 159 | }); 160 | 161 | it('should not throw if token is expired but the expired handler let it thru', function (done) { 162 | const secret = 'shhhhhh'; 163 | const token = jwt.sign({ foo: 'bar', exp: 1382412921 }, secret); 164 | 165 | req.headers = {}; 166 | req.headers.authorization = 'Bearer ' + token; 167 | expressjwt({ 168 | secret: 'shhhhhh', 169 | algorithms: ['HS256'], 170 | // eslint-disable-next-line @typescript-eslint/no-empty-function 171 | onExpired: () => { }, 172 | })(req, res, function (err) { 173 | assert.ok(!err); 174 | //@ts-ignore 175 | assert.equal(req.auth.foo, 'bar'); 176 | done(); 177 | }); 178 | }); 179 | 180 | it('should throw if token is expired and the expired handler rethrows it', function (done) { 181 | const secret = 'shhhhhh'; 182 | const token = jwt.sign({ foo: 'bar', exp: 1382412921 }, secret); 183 | 184 | req.headers = {}; 185 | req.headers.authorization = 'Bearer ' + token; 186 | expressjwt({ 187 | secret: 'shhhhhh', 188 | algorithms: ['HS256'], 189 | // eslint-disable-next-line @typescript-eslint/no-empty-function 190 | onExpired: (req, err) => { throw err; }, 191 | })(req, res, function (err) { 192 | assert.ok(err); 193 | assert.equal(err.code, 'invalid_token'); 194 | assert.equal(err.inner.name, 'TokenExpiredError'); 195 | assert.equal(err.message, 'jwt expired'); 196 | done(); 197 | }); 198 | }); 199 | 200 | it('should throw if token issuer is wrong', function (done) { 201 | const secret = 'shhhhhh'; 202 | const token = jwt.sign({ foo: 'bar', iss: 'http://foo' }, secret); 203 | 204 | req.headers = {}; 205 | req.headers.authorization = 'Bearer ' + token; 206 | expressjwt({ secret: 'shhhhhh', algorithms: ['HS256'], issuer: 'http://wrong' })(req, res, function (err) { 207 | assert.ok(err); 208 | assert.equal(err.code, 'invalid_token'); 209 | assert.equal(err.message, 'jwt issuer invalid. expected: http://wrong'); 210 | done(); 211 | }); 212 | }); 213 | 214 | it('should use errors thrown from custom getToken function', function (done) { 215 | expressjwt({ 216 | secret: 'shhhhhh', algorithms: ['HS256'], 217 | getToken: () => { throw new UnauthorizedError('invalid_token', { message: 'Invalid token!' }); } 218 | })(req, res, function (err) { 219 | assert.ok(err); 220 | assert.equal(err.code, 'invalid_token'); 221 | assert.equal(err.message, 'Invalid token!'); 222 | done(); 223 | }); 224 | }); 225 | 226 | it('should throw error when signature is wrong', function (done) { 227 | const secret = "shhh"; 228 | const token = jwt.sign({ foo: 'bar', iss: 'http://www' }, secret); 229 | // manipulate the token 230 | const newContent = Buffer 231 | .from('{"foo": "bar", "edg": "ar"}') 232 | .toString('base64') 233 | .replace(/=/g, ''); 234 | const splitetToken = token.split("."); 235 | splitetToken[1] = newContent; 236 | const newToken = splitetToken.join("."); 237 | // build request 238 | // @ts-ignore 239 | req.headers = []; 240 | req.headers.authorization = 'Bearer ' + newToken; 241 | expressjwt({ secret: secret, algorithms: ['HS256'] })(req, res, function (err) { 242 | assert.ok(err); 243 | assert.equal(err.code, 'invalid_token'); 244 | assert.equal(err.message, 'invalid signature'); 245 | done(); 246 | }); 247 | }); 248 | 249 | it('should throw error if token is expired even with when credentials are not required', function (done) { 250 | const secret = 'shhhhhh'; 251 | const token = jwt.sign({ foo: 'bar', exp: 1382412921 }, secret); 252 | 253 | req.headers = {}; 254 | req.headers.authorization = 'Bearer ' + token; 255 | expressjwt({ secret: secret, credentialsRequired: false, algorithms: ['HS256'] })(req, res, function (err) { 256 | assert.ok(err); 257 | assert.equal(err.code, 'invalid_token'); 258 | assert.equal(err.message, 'jwt expired'); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('should throw error if token is invalid even with when credentials are not required', function (done) { 264 | const secret = 'shhhhhh'; 265 | const token = jwt.sign({ foo: 'bar', exp: 1382412921 }, secret); 266 | 267 | req.headers = {}; 268 | req.headers.authorization = 'Bearer ' + token; 269 | expressjwt({ secret: "not the secret", algorithms: ['HS256'], credentialsRequired: false })(req, res, function (err) { 270 | assert.ok(err); 271 | assert.equal(err.code, 'invalid_token'); 272 | assert.equal(err.message, 'invalid signature'); 273 | done(); 274 | }); 275 | }); 276 | 277 | }); 278 | 279 | describe('work tests', function () { 280 | // var req = {} as express.Request; 281 | // var res = {} as express.Response; 282 | 283 | it('should work if authorization header is valid jwt', function (done) { 284 | const secret = 'shhhhhh'; 285 | const token = jwt.sign({ foo: 'bar' }, secret); 286 | const req = {} as Request; 287 | const res = {} as express.Response; 288 | req.headers = {}; 289 | req.headers.authorization = 'Bearer ' + token; 290 | expressjwt({ secret: secret, algorithms: ['HS256'] })(req, res, function () { 291 | assert.equal(req.auth?.foo, 'bar'); 292 | done(); 293 | }); 294 | }); 295 | 296 | it('should work with custom and nested request property', function (done) { 297 | const secret = 'shhhhhh'; 298 | const token = jwt.sign({ foo: 'bar' }, secret); 299 | const req = {} as Request; 300 | const res = {} as express.Response; 301 | const requestProperty = 'auth.payload'; 302 | 303 | req.headers = {}; 304 | req.headers.authorization = 'Bearer ' + token; 305 | expressjwt({ secret: secret, algorithms: ['HS256'], requestProperty })(req, res, function () { 306 | assert.equal(req.auth?.payload.foo, 'bar'); 307 | done(); 308 | }); 309 | }); 310 | 311 | it('should work if authorization header is valid with a buffer secret', function (done) { 312 | const secret = Buffer.from('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'base64'); 313 | const token = jwt.sign({ foo: 'bar' }, secret); 314 | const req = {} as Request; 315 | const res = {} as express.Response; 316 | 317 | req.headers = {}; 318 | req.headers.authorization = 'Bearer ' + token; 319 | expressjwt({ secret: secret, algorithms: ['HS256'] })(req, res, function () { 320 | assert.equal(req.auth?.foo, 'bar'); 321 | done(); 322 | }); 323 | }); 324 | 325 | it('should work if Authorization header is capitalized (lambda environment)', function (done) { 326 | const secret = Buffer.from('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'base64'); 327 | const token = jwt.sign({ foo: 'bar' }, secret); 328 | const req = {} as Request; 329 | const res = {} as express.Response; 330 | 331 | req.headers = {}; 332 | req.headers.Authorization = 'Bearer ' + token; 333 | expressjwt({ secret: secret, algorithms: ['HS256'] })(req, res, function (err) { 334 | if (err) { return done(err); } 335 | assert.equal(req.auth?.foo, 'bar'); 336 | done(); 337 | }); 338 | }); 339 | 340 | it('should work if no authorization header and credentials are not required', function (done) { 341 | const req = {} as express.Request; 342 | const res = {} as express.Response; 343 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'], credentialsRequired: false })(req, res, done); 344 | }); 345 | 346 | it('should not work if no authorization header', function (done) { 347 | const req = {} as express.Request; 348 | const res = {} as express.Response; 349 | expressjwt({ secret: 'shhhh', algorithms: ['HS256'] })(req, res, function (err) { 350 | assert(typeof err !== 'undefined'); 351 | done(); 352 | }); 353 | }); 354 | 355 | it('should produce a stack trace that includes the failure reason', function (done) { 356 | const req = {} as express.Request; 357 | const res = {} as express.Response; 358 | const token = jwt.sign({ foo: 'bar' }, 'secretA'); 359 | req.headers = {}; 360 | req.headers.authorization = 'Bearer ' + token; 361 | 362 | expressjwt({ secret: 'secretB', algorithms: ['HS256'] })(req, res, function (err) { 363 | const index = err.stack.indexOf('UnauthorizedError: invalid signature') 364 | assert.equal(index, 0, "Stack trace didn't include 'invalid signature' message.") 365 | done(); 366 | }); 367 | 368 | }); 369 | 370 | it('should work with a custom getToken function', function (done) { 371 | const req = {} as Request; 372 | const res = {} as express.Response; 373 | const secret = 'shhhhhh'; 374 | const token = jwt.sign({ foo: 'bar' }, secret); 375 | 376 | req.headers = {}; 377 | req.query = {}; 378 | req.query.token = token; 379 | 380 | function getTokenFromQuery(req) { 381 | return req.query.token; 382 | } 383 | 384 | expressjwt({ 385 | secret: secret, 386 | algorithms: ['HS256'], 387 | getToken: getTokenFromQuery 388 | })(req, res, function () { 389 | assert.equal(req.auth?.foo, 'bar'); 390 | done(); 391 | }); 392 | }); 393 | 394 | it('should work with an async getToken function', function (done) { 395 | const req = {} as Request; 396 | const res = {} as express.Response; 397 | const secret = 'shhhhhh'; 398 | const token = jwt.sign({ foo: 'bar' }, secret); 399 | 400 | req.headers = {}; 401 | req.query = {}; 402 | req.query.token = token; 403 | 404 | function getTokenFromQuery(req) { 405 | return Promise.resolve(req.query.token); 406 | } 407 | 408 | expressjwt({ 409 | secret: secret, 410 | algorithms: ['HS256'], 411 | getToken: getTokenFromQuery 412 | })(req, res, function () { 413 | assert.equal(req.auth?.foo, 'bar'); 414 | done(); 415 | }); 416 | }); 417 | 418 | it('should work with a secretCallback function that accepts header argument', function (done) { 419 | const req = {} as Request; 420 | const res = {} as express.Response; 421 | const secret = 'shhhhhh'; 422 | const getSecret: GetVerificationKey = async (req, token) => { 423 | // @ts-ignore 424 | assert.equal(token.header.alg, 'HS256'); 425 | // @ts-ignore 426 | assert.equal(token.payload.foo, 'bar'); 427 | return secret; 428 | }; 429 | 430 | const token = jwt.sign({ foo: 'bar' }, secret); 431 | 432 | req.headers = {}; 433 | req.headers.authorization = 'Bearer ' + token; 434 | expressjwt({ secret: getSecret, algorithms: ['HS256'] })(req, res, function () { 435 | assert.equal(req.auth?.foo, 'bar'); 436 | done(); 437 | }); 438 | }); 439 | }); 440 | --------------------------------------------------------------------------------