├── .eslintrc ├── .gitignore ├── README.md ├── package.json └── src ├── api ├── achievements.js ├── index.js └── users.js ├── config.json ├── data ├── achievements.json └── users.json ├── index.js ├── lib ├── JwksClient.js ├── achievements.js ├── errors │ ├── ArgumentError.js │ ├── ForbiddenError.js │ ├── JwksError.js │ ├── SigningKeyNotFoundError.js │ ├── UnauthorizedError.js │ └── index.js ├── expressJwtSecret.js ├── users.js └── utils.js └── middleware ├── expressJwt.js ├── jwtCheck.js └── scopeCheck.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "allowImportExportEverywhere": false, 6 | "codeFrame": false 7 | } 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auth0 RS256 Validation Using JWKS 2 | 3 | ## Purpose 4 | 5 | This sample is intended as an introduction to the Auth0 JWKS endpoint. The purpose is to demonstrate how one would verify an RS256 signed JWT while using the JWKS endpoint to lookup the public certificate necessary verify the token signature. This sample should be used as a basic guide for building your own token verifier, however the sample is not considered production ready. This sample lacks necessary error handling, caching, and other production qualities. 6 | 7 | If you are building a NodeJS application using Express or HapiJS it is recommended that you use the production grade moduels provided by Auth0 ([node-jwks-rsa](https://github.com/auth0/node-jwks-rsa) and [express-jwt](https://github.com/auth0/express-jwt)). The sample code provided here used these two libraries as the basis for the coding examples. This [sample repository](https://github.com/sgmeyer/auth0-rs256-verification) demonstrates that. 8 | 9 | ## Setting Up 10 | 11 | Simply clone the repository locally: 12 | 13 | ``` 14 | git clone git@github.com:auth0-samples/auth0-node-jwks-rs256.git 15 | ``` 16 | 17 | Then install all the modules: 18 | 19 | ``` 20 | npm install 21 | ``` 22 | 23 | Create a .env file in the root directory: 24 | 25 | ``` 26 | PORT=3000 27 | AUTH0_TENANT=your-tenant-subdomain 28 | ``` 29 | 30 | Then run the code on port 3000: 31 | 32 | ``` 33 | npm run dev 34 | ``` 35 | 36 | ## See a bug or something missing? PRs welcome! 37 | 38 | If you see a bug or see that something is missing feel free to post an issue or submit a PR! 39 | 40 | ## What is Auth0? 41 | 42 | Auth0 helps you to: 43 | 44 | * Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. 45 | * Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. 46 | * Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. 47 | * Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. 48 | * Analytics of how, when and where users are logging in. 49 | * Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). 50 | 51 | ## Create a free Auth0 account 52 | 53 | 1. Go to [Auth0](https://auth0.com/signup) and click Sign Up. 54 | 2. Use Google, GitHub or Microsoft Account to login. 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-rs256-verification", 3 | "version": "1.0.0", 4 | "description": "Demonstrates how use Auth0's JWKS endpoint to lookup the certificate necessary to verify an RS256 signed JWT.", 5 | "main": "dist", 6 | "scripts": { 7 | "dev": "nodemon -w src --exec \"babel-node src --presets es2015,stage-0\"", 8 | "test": "eslint src" 9 | }, 10 | "eslintConfig": { 11 | "extends": "eslint:recommended", 12 | "parserOptions": { 13 | "ecmaVersion": 7, 14 | "sourceType": "module" 15 | }, 16 | "env": { 17 | "node": true 18 | }, 19 | "rules": { 20 | "no-console": 0, 21 | "no-unused-vars": 1 22 | } 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/auth0-samples/auth0-node-jwks-rs256.git" 27 | }, 28 | "keywords": [ 29 | "Auth0", 30 | "RS256", 31 | "Resource", 32 | "Server", 33 | "Authorization", 34 | "JWKS", 35 | "JWK" 36 | ], 37 | "author": "Shawn G. Meyer", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/auth0-samples/auth0-node-jwks-rs256/issues" 41 | }, 42 | "homepage": "https://github.com/auth0-samples/auth0-node-jwks-rs256#readme", 43 | "devDependencies": { 44 | "babel-cli": "^6.24.0", 45 | "babel-core": "^6.24.0", 46 | "babel-eslint": "^6.1.2", 47 | "babel-preset-es2015": "^6.24.0", 48 | "babel-preset-stage-0": "^6.22.0", 49 | "eslint": "^3.18.0", 50 | "nodemon": "^1.11.0" 51 | }, 52 | "dependencies": { 53 | "async": "^2.2.0", 54 | "body-parser": "^1.17.1", 55 | "compression": "^1.6.2", 56 | "cors": "^2.8.1", 57 | "dotenv": "^4.0.0", 58 | "express": "^4.15.2", 59 | "jsonwebtoken": "^7.3.0", 60 | "lodash.set": "^4.3.2", 61 | "request": "^2.81.0", 62 | "resource-router-middleware": "^0.6.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/api/achievements.js: -------------------------------------------------------------------------------- 1 | import { Router as router } from 'express'; 2 | import achievements from '../lib/achievements'; 3 | 4 | export default () => { 5 | let achievementsApi = router(); 6 | 7 | achievementsApi.get('/', (req, res) => { 8 | const allAchievements = achievements.list(); 9 | res.json(allAchievements); 10 | }); 11 | 12 | achievementsApi.get('/:id', (req, res) => { 13 | const achievementId = req.params.id; 14 | const achievement = achievements.get(achievementId); 15 | 16 | if (!achievement) { 17 | res.status(404).send( { 18 | "error": "Achievement not found" 19 | }); 20 | } 21 | 22 | res.json(achievement); 23 | }); 24 | 25 | return achievementsApi; 26 | } -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import { Router as router } from 'express'; 2 | import { version } from '../../package.json'; 3 | 4 | import achievements from './achievements'; 5 | import scopeCheck from '../middleware/scopeCheck'; 6 | import users from './users'; 7 | 8 | 9 | export default () => { 10 | let api = router(); 11 | 12 | api.use('/achievements', scopeCheck('read:achievements'), achievements()); 13 | api.use('/users', scopeCheck('read:users read:achievements'), users()); 14 | 15 | api.get('/', (req, res) => { 16 | res.json({ version }); 17 | }); 18 | 19 | return api; 20 | } -------------------------------------------------------------------------------- /src/api/users.js: -------------------------------------------------------------------------------- 1 | import { Router as router } from 'express'; 2 | import users from '../lib/users'; 3 | 4 | export default () => { 5 | var userApi = router(); 6 | userApi.get('/', (req, res) => { 7 | const allUsers = users.list(); 8 | res.json(allUsers); 9 | }); 10 | 11 | userApi.get('/:id', (req, res) => { 12 | const userId = req.params.id; 13 | const user = users.get(userId); 14 | 15 | if (!user) { 16 | res.status(404).send( { 17 | "error": "User not found" 18 | }); 19 | } 20 | 21 | res.json(user); 22 | }); 23 | 24 | return userApi; 25 | } -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bodyLimit": "100kb", 3 | "corsHeaders": ["Link"] 4 | } -------------------------------------------------------------------------------- /src/data/achievements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Code n00b", 4 | "description": "You have completed your first commit and have just started your journey as a sofware engineer." 5 | }, 6 | { 7 | "name": "Code Apprentice", 8 | "description": "You are following at least one Code Craftsman and have commited 10 PRs." 9 | }, 10 | { 11 | "name": "Code Journeyman", 12 | "description": "You have submitted your first PR to a project with 25 or more stars." 13 | }, 14 | { 15 | "name": "Code Craftsman", 16 | "description": "You regularly committing to a major repository." 17 | }, 18 | { 19 | "name": "Code Hero", 20 | "description": "You have a stackoverflow score of 50,000 points and are a contributor to at least 1 repo with 100 or more stars." 21 | }, 22 | { 23 | "name": "Code Legend", 24 | "description": "You are Donald Knuth." 25 | } 26 | ] -------------------------------------------------------------------------------- /src/data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Donald Knuth", 4 | "achievementId": 5 5 | }, 6 | { 7 | "name": "Shawn Meyer", 8 | "achievementId": 3 9 | }, 10 | { 11 | "name": "John Smith", 12 | "achievementId": 1 13 | }, 14 | { 15 | "name": "Jane Smith", 16 | "achievementId": 4 17 | } 18 | ] -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import express from 'express'; 3 | import cors from 'cors'; 4 | import bodyParser from 'body-parser'; 5 | import dotenv from 'dotenv'; 6 | 7 | import api from './api'; 8 | import config from './config.json'; 9 | import { jwtCheck } from './middleware/jwtCheck'; 10 | 11 | dotenv.load(); 12 | 13 | let app = express(); 14 | app.server = http.createServer(app); 15 | 16 | app.use(cors({ 17 | exposedHeaders: config.corsHeaders 18 | })); 19 | 20 | app.use(bodyParser.json({ 21 | limit : config.bodyLimit 22 | })); 23 | 24 | app.use('/api', jwtCheck, api()); 25 | 26 | app.use((err, req, res, next) => { 27 | console.log(err); 28 | 29 | res.status(err.status || 500).json(err); 30 | }); 31 | 32 | app.server.listen(process.env.PORT || 3000); 33 | console.log(`Started on port ${app.server.address().port}`); 34 | 35 | export default app; -------------------------------------------------------------------------------- /src/lib/JwksClient.js: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import { JwksError, SigningKeyNotFoundError } from './errors'; 3 | 4 | import { certToPEM } from './utils'; 5 | 6 | export class JwksClient { 7 | constructor(options) { 8 | this.options = { strictSsl: true, ...options }; 9 | } 10 | 11 | getJwks(cb) { 12 | request({ 13 | uri: this.options.jwksUri, 14 | strictSsl: this.options.strictSsl, 15 | json: true 16 | }, (err, res) => { 17 | if (err || res.statusCode < 200 || res.statusCode >= 300) { 18 | if (res) { 19 | return cb(new JwksError(res.body && (res.body.message || res.body) || res.statusMessage || `Http Error ${res.statusCode}`)); 20 | } 21 | return cb(err); 22 | } 23 | 24 | var jwks = res.body.keys; 25 | return cb(null, jwks); 26 | }); 27 | } 28 | 29 | getSigningKeys(cb) { 30 | const callback = (err, keys) => { 31 | if (err) { 32 | return cb(err); 33 | } 34 | 35 | if (!keys || !keys.length) { 36 | return cb(new JwksError('The JWKS endpoint did not contain any keys')); 37 | } 38 | 39 | const signingKeys = keys 40 | .filter(key => key.use === 'sig' // JWK property `use` determines the JWK is for signing 41 | && key.kty === 'RSA' // We are only supporting RSA 42 | && key.kid // The `kid` must be present to be useful for later 43 | && key.x5c && key.x5c.length // Has useful public keys (we aren't using n or e) 44 | ).map(key => { 45 | return { kid: key.kid, nbf: key.nbf, publicKey: certToPEM(key.x5c[0]) }; 46 | }); 47 | 48 | // If at least a single signing key doesn't exist we have a problem... Kaboom. 49 | if (!signingKeys.length) { 50 | return cb(new JwksError('The JWKS endpoint did not contain any signing keys')); 51 | } 52 | 53 | // Returns all of the available signing keys. 54 | return cb(null, signingKeys); 55 | }; 56 | 57 | this.getJwks(callback); 58 | } 59 | 60 | getSigningKey = (kid, cb) => { 61 | const callback = (err, keys) => { 62 | if (err) { 63 | return cb(err); 64 | } 65 | 66 | const signingKey = keys.find(key => key.kid === kid); 67 | 68 | if (!signingKey) { 69 | var error = new SigningKeyNotFoundError(`Unable to find a signing key that matches '${kid}'`); 70 | return cb(error); 71 | } 72 | 73 | return cb(null, signingKey) 74 | }; 75 | 76 | this.getSigningKeys(callback); 77 | } 78 | } -------------------------------------------------------------------------------- /src/lib/achievements.js: -------------------------------------------------------------------------------- 1 | import achievementData from '../data/achievements'; 2 | 3 | export default { 4 | list: () => { 5 | return achievementData; 6 | }, 7 | get: (id) => { 8 | var achievement = achievementData[id]; 9 | 10 | return achievement; 11 | } 12 | }; -------------------------------------------------------------------------------- /src/lib/errors/ArgumentError.js: -------------------------------------------------------------------------------- 1 | function ArgumentError(message) { 2 | Error.call(this, message); 3 | Error.captureStackTrace(this, this.constructor); 4 | this.name = 'ArgumentError'; 5 | this.message = message; 6 | } 7 | 8 | ArgumentError.prototype = Object.create(Error.prototype); 9 | ArgumentError.prototype.constructor = ArgumentError; 10 | 11 | module.exports = ArgumentError; -------------------------------------------------------------------------------- /src/lib/errors/ForbiddenError.js: -------------------------------------------------------------------------------- 1 | function ForbiddenError (code, error) { 2 | Error.call(this, error.message); 3 | Error.captureStackTrace(this, this.constructor); 4 | this.name = "ForbiddenError"; 5 | this.message = error.message; 6 | this.code = code; 7 | this.status = 403; 8 | this.inner = error; 9 | } 10 | 11 | ForbiddenError.prototype = Object.create(Error.prototype); 12 | ForbiddenError.prototype.constructor = ForbiddenError; 13 | 14 | module.exports = ForbiddenError; -------------------------------------------------------------------------------- /src/lib/errors/JwksError.js: -------------------------------------------------------------------------------- 1 | function JwksError(message) { 2 | Error.call(this, message); 3 | Error.captureStackTrace(this, this.constructor); 4 | this.name = 'JwksError'; 5 | this.message = message; 6 | } 7 | 8 | JwksError.prototype = Object.create(Error.prototype); 9 | JwksError.prototype.constructor = JwksError; 10 | 11 | module.exports = JwksError; -------------------------------------------------------------------------------- /src/lib/errors/SigningKeyNotFoundError.js: -------------------------------------------------------------------------------- 1 | function SigningKeyNotFoundError(message) { 2 | Error.call(this, message); 3 | Error.captureStackTrace(this, this.constructor); 4 | this.name = 'SigningKeyNotFoundError'; 5 | this.message = message; 6 | } 7 | 8 | SigningKeyNotFoundError.prototype = Object.create(Error.prototype); 9 | SigningKeyNotFoundError.prototype.constructor = SigningKeyNotFoundError; 10 | module.exports = SigningKeyNotFoundError; -------------------------------------------------------------------------------- /src/lib/errors/UnauthorizedError.js: -------------------------------------------------------------------------------- 1 | function UnauthorizedError (code, error) { 2 | Error.call(this, error.message); 3 | Error.captureStackTrace(this, this.constructor); 4 | this.name = "UnauthorizedError"; 5 | this.message = error.message; 6 | this.code = code; 7 | this.status = 401; 8 | this.inner = error; 9 | } 10 | 11 | UnauthorizedError.prototype = Object.create(Error.prototype); 12 | UnauthorizedError.prototype.constructor = UnauthorizedError; 13 | 14 | module.exports = UnauthorizedError; -------------------------------------------------------------------------------- /src/lib/errors/index.js: -------------------------------------------------------------------------------- 1 | export ArgumentError from './ArgumentError'; 2 | export ForbiddenError from './ForbiddenError'; 3 | export JwksError from './JwksError'; 4 | export SigningKeyNotFoundError from './SigningKeyNotFoundError'; 5 | export UnauthorizedError from './UnauthorizedError'; -------------------------------------------------------------------------------- /src/lib/expressJwtSecret.js: -------------------------------------------------------------------------------- 1 | import { ArgumentError } from './errors'; 2 | import { JwksClient } from './JwksClient'; 3 | 4 | const handleSigningKeyError = (err, cb) => { 5 | // If we didn't find a match, can't provide a key. 6 | if (err && err.name === 'SigningKeyNotFoundError') { 7 | return cb(null); 8 | } 9 | 10 | // Any other error we will bubble up. 11 | if (err) { 12 | return cb(err); 13 | } 14 | }; 15 | 16 | export default (options) => { 17 | if (options === null || options === undefined) { 18 | throw new ArgumentError('An options object must be provided when initializing expressJwtSecret'); 19 | } 20 | 21 | const client = new JwksClient(options); 22 | const onError = handleSigningKeyError; 23 | 24 | return function secretProvider(req, header, payload, cb) { 25 | // Only RS256 is supported. 26 | if (!header || header.alg !== 'RS256') { 27 | return cb(null, null); 28 | } 29 | 30 | client.getSigningKey(header.kid, (err, key) => { 31 | if (err) { 32 | return onError(err, (newError) => cb(newError, null)); 33 | } 34 | 35 | // Provide the key. 36 | return cb(null, key.publicKey || key.rsaPublicKey); 37 | }); 38 | }; 39 | }; -------------------------------------------------------------------------------- /src/lib/users.js: -------------------------------------------------------------------------------- 1 | import achievements from './achievements'; 2 | import userData from '../data/users'; 3 | 4 | export default { 5 | list: () => { 6 | const users = userData; 7 | 8 | users.forEach((user) => { 9 | const achievement = achievements.get(user.achievementId); 10 | user.achievement = achievement; 11 | }) 12 | 13 | return users; 14 | }, 15 | get: (id) => { 16 | const user = userData[id]; 17 | 18 | if (user && user.achievementId) { 19 | user.achievement = achievements.get(user.achievementId); 20 | } 21 | 22 | return user; 23 | } 24 | }; -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * If you wanted to use `n` and `e` from JWKS check out node-jwks-rsa's implementation: 3 | * https://github.com/auth0/node-jwks-rsa/blob/master/src/utils.js#L35-L57 4 | */ 5 | 6 | export function certToPEM(cert) { 7 | cert = cert.match(/.{1,64}/g).join('\n'); 8 | cert = `-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----\n`; 9 | return cert; 10 | } -------------------------------------------------------------------------------- /src/middleware/expressJwt.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import async from 'async'; 3 | import set from 'lodash.set'; 4 | 5 | import { UnauthorizedError } from '../lib/errors'; 6 | 7 | export default (options) => { 8 | 9 | var secretCallback = options.secret; 10 | 11 | var middleware = (req, res, next) => { 12 | var authHeader = req.headers.authorization; 13 | var parts = authHeader.split(' '); 14 | 15 | if (parts.length != 2) { 16 | throw new UnauthorizedError('credentials_required', { message: 'No authorization token was found' }); 17 | } 18 | 19 | var scheme = parts[0]; 20 | if(!/^Bearer$/i.test(scheme)) { 21 | 22 | throw new UnauthorizedError('credentials_bad_scheme', { message: 'Format is Authorization: Bearer [token]' }); 23 | } 24 | 25 | var token = parts[1]; 26 | 27 | // This could fail. If it does handle as 401 as the token is invalid. 28 | var decodedToken = jwt.decode(token, {complete: true}); 29 | 30 | if (decodedToken.header.alg !== 'RS256') { 31 | // we are only supporting RS256 so fail if this happens. 32 | return cb(null, null); 33 | } 34 | 35 | var tasks = [ 36 | function getSecret(callback) { 37 | secretCallback(req, decodedToken.header, decodedToken.payload, callback); 38 | }, 39 | function verifyToken(secret, callback) { 40 | jwt.verify(token, secret, options, function(err, decoded) { 41 | if (err) { 42 | callback(new UnauthorizedError('invalid_token', err)); 43 | } else { 44 | callback(null, decoded); 45 | } 46 | }); 47 | } 48 | ]; 49 | 50 | async.waterfall(tasks, (err, result) => { 51 | if (err) { 52 | return next(err); 53 | } 54 | 55 | set(req, 'user', result); 56 | next(); 57 | }); 58 | } 59 | 60 | return middleware; 61 | } -------------------------------------------------------------------------------- /src/middleware/jwtCheck.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | import expressJwt from './expressJwt'; 4 | import expressJwtSecret from '../lib/expressJwtSecret'; 5 | 6 | dotenv.load(); 7 | 8 | export const jwtCheck = expressJwt({ 9 | secret: expressJwtSecret({ 10 | jwksUri: `https://${process.env.AUTH0_TENANT}.auth0.com/.well-known/jwks.json` 11 | }), 12 | 13 | // Validate the audience and the issuer. 14 | audience: 'https://api.codehero.com/v1/', 15 | issuer: `https://${process.env.AUTH0_TENANT}.auth0.com/`, 16 | algorithms: ['RS256'] 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /src/middleware/scopeCheck.js: -------------------------------------------------------------------------------- 1 | import { ForbiddenError } from '../lib/errors'; 2 | 3 | export default (requiredScope) => { 4 | var middleware = (req, res, next) => { 5 | const user = req.user; 6 | const requiredScopes = requiredScope.split(' '); 7 | const scopes = user.scope.split(' '); 8 | 9 | if (!requiredScopes || requiredScopes.length < 1) { 10 | next(); 11 | } 12 | 13 | requiredScopes.forEach((scope) => { 14 | if (scopes.indexOf(scope) < 0) { 15 | next(new ForbiddenError('insufficient_Scope', { message: 'The token does not contain sufficient scopes.' })); 16 | } 17 | }); 18 | 19 | next(); 20 | } 21 | 22 | middleware.ForbiddenError = ForbiddenError; 23 | 24 | return middleware; 25 | } --------------------------------------------------------------------------------