From 4f2627a3afa8baf7b375dfa3e43a55e3785544b3 Mon Sep 17 00:00:00 2001 From: MeStrak Date: Fri, 11 Feb 2022 10:48:41 +0000 Subject: [PATCH] feat: add passportJwtSecret support --- docs/security.md | 38 +++++++--- package-lock.json | 137 ++++++++++++++++++++++++++++++++++- package.json | 1 + src/config/config.service.ts | 6 +- 4 files changed, 169 insertions(+), 13 deletions(-) diff --git a/docs/security.md b/docs/security.md index f72859510..69de7bca4 100644 --- a/docs/security.md +++ b/docs/security.md @@ -14,7 +14,12 @@ _ Note that even though the `AUTH_CONFIG_SECRET` is not necessarily a secret (in The provided configuration is very similar to the NestJS passport-jwt example with a few modifications: - it must be provided as a JSON string containing a `config` property which is an array of valid passport-jwt configurations -- the jwtFromRequest which tells passport-jwt where to find the JWT in the request (usually the `Authorization` header) function must be deconstructed into an object containing funcName and args properties +- the `jwtFromRequest` function which tells passport-jwt where to find the JWT in the request (usually the `Authorization` header) function must be deconstructed into an object containing funcName and args properties +- `passportJwtSecret` function should not be written in the configuration file, only the arguments are required in the `passportJwtSecret` object + +### Examples + +This is an example for multiple passport JWT configurations as they would be configured in Javascript. ```js [{ @@ -22,12 +27,12 @@ The provided configuration is very similar to the NestJS passport-jwt example wi cache: true, rateLimit: true, jwksRequestsPerMinute: 5, - jwksUri: `${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`, + jwksUri: "https://auth.issuer.com", }), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - audience: process.env.AUTH0_AUDIENCE, - issuer: `${process.env.AUTH0_DOMAIN}/`, + audience: 'MY_APP_AUDIENCE', + issuer: `https://auth.issuer.com/`, algorithms: ['RS256'], }, { @@ -36,25 +41,38 @@ The provided configuration is very similar to the NestJS passport-jwt example wi secretOrKey: 'APPX_SECRET_KEY', }, { - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromBodyField('USER_JWT'), ignoreExpiration: false, secretOrKey: 'APPY_SECRET_KEY', }] ``` +And an example env file configuration for `AUTH_CONFIG_SECRET` based on the configuration above. + ```bash AUTH_CONFIG_SECRET = # note that in a .env file this should be formatted onto one line - it is shown multiline here for easier readability # see project example.env file for a single line config {"config":[{ - "jwtFromRequest":{ - "funcName": "fromAuthHeaderAsBearerToken"}, + "secretOrKeyProvider": { + "cache": true, + "rateLimit": true, + "jwksRequestsPerMinute": 5, + "jwksUri": "https://uri.com" + }, + "jwtFromRequest":{"funcName": "fromAuthHeaderAsBearerToken"}, + "audience": "MY_APP_AUDIENCE", + "issuer":"https://auth.issuer.com/", + "algorithms": ['RS256'] + } + { + "jwtFromRequest":{"funcName": "fromAuthHeaderAsBearerToken"}, "ignoreExpiration":false, "secretOrKey":"APPX_SECRET_KEY"}, - {"jwtFromRequest": { - "funcName": "fromAuthHeaderAsBearerToken"}, + { + "jwtFromRequest": {"funcName": "fromBodyField", args: 'USER_JWT'}, "ignoreExpiration":false, - "secretOrKey":"another_secret_key"} + "secretOrKey":"APPY_SECRET_KEY"} ] } ``` diff --git a/package-lock.json b/package-lock.json index 8d7063c32..fb9eff990 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3167,6 +3167,11 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.3.tgz", "integrity": "sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==" }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3736,6 +3741,15 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/bson": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", @@ -3749,6 +3763,14 @@ "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, "@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -3797,6 +3819,44 @@ "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/express-unless": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", + "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", + "requires": { + "@types/express": "*" + } + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -3900,6 +3960,11 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -3958,12 +4023,31 @@ "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", "dev": true }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, "@types/retry": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@types/socket.io": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", @@ -15532,6 +15616,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -15736,6 +15828,18 @@ "safe-buffer": "^5.0.1" } }, + "jwks-rsa": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", + "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", + "requires": { + "@types/express-jwt": "0.0.42", + "debug": "^4.3.2", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + } + }, "jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -15846,6 +15950,11 @@ "set-cookie-parser": "^2.4.1" } }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -15958,8 +16067,7 @@ "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.debounce": { "version": "4.0.8", @@ -16241,6 +16349,31 @@ "yallist": "^4.0.0" } }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", diff --git a/package.json b/package.json index b282b5d5a..90d79b7bb 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "haiku-random": "1.0.0", "ioredis": "4.28.2", "joi": "17.5.0", + "jwks-rsa": "^2.0.5", "lodash": "4.17.21", "mongoose": "5.13.13", "mongoose-cast-aggregation": "0.2.1", diff --git a/src/config/config.service.ts b/src/config/config.service.ts index 31e5a6756..ff4a6aad7 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import { Agent } from 'http'; import * as tunnel from 'tunnel'; import { ExtractJwt } from '@mestrak/passport-multi-jwt'; +import { passportJwtSecret } from 'jwks-rsa'; import validationSchema from './environmentValidationSchema'; @Injectable() @@ -108,9 +109,12 @@ export class ConfigService { getAuthConfig(): any { const authConfig = this.get('AUTH_CONFIG_SECRET'); - // as jwtExtractor is a function, translate function name string to function call + //patch in ExtractJwt and passportJwtSecret function calls as they cannot be enocded in JSON return authConfig.config.map((configuration) => ({ ...configuration, + ...(configuration.secretOrKeyProvider + ? { secretOrKeyProvider: passportJwtSecret(configuration.secretOrKeyProvider) } + : {}), jwtFromRequest: ExtractJwt[configuration.jwtFromRequest.funcName](configuration.jwtFromRequest.args), })); }