Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options for jwks-rsa to enable cache and rate limiting #6

Merged
merged 11 commits into from
Feb 25, 2019
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: node_js
node_js:
- '10'
- '8'
- '7'
- '6'
cdwills marked this conversation as resolved.
Show resolved Hide resolved
before_install:
- npm install -g yarn@1.3.2
Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ Initialize `authentic` with an options object containing an `issWhitelist` array

**Note:** The urls in the list need to be **exact matches** of the `payload.iss` values in your JWT's.

Any other options passed to `authentic` will be forwarded to `jwt.verify()` for validation and parsing. [See the list of available options here.](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback)

You'll receive a unary function which takes a JWT and returns a `Promise` that resolves with the parsed JWT payload if it is valid, or rejects with a `401` [Boom](https://github.com/hapijs/boom) error if it is invalid.

```js
Expand All @@ -39,3 +37,25 @@ const handler = req =>
authentic(req.cookies.token)
.then(/* the JWT has been validated */)
```

## Options

`authentic` accepts a JSON object of options that will be passed to the underlying libraries responsibile for validation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the options section is a bit confusing. Am I correct that any options other that jwks or issWhitelist will be passed to jwt.verify()? If so, should it be:

{
  verify: { ... },
  jwks: { ... },
  issWhitelist: ...,
}

for some futureproofing? It seems odd to separate jwks' options, but not verify's and I could imagine that causing conflicts eventually.


Besides the `issWhitelist` prop, any other options passed will be forwarded to `jwt.verify()` for validation and parsing. [See the list of available options here.](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback)

Options passed in under the prop `jwks` will be passed to `node-jwks-rsa`.
We have set defaults for 2 values from `jwks`.

```
{
jwks: {
cache: true, // default from authentic
rateLimit: true, // default from authentic
},
issWhitelist: JSON.parse(process.env.ISS_WHITELIST)
}
```

Available options to set for `node-jwks-rsa` can be found here. [See the list of available options here.](https://github.com/auth0/node-jwks-rsa)

21 changes: 13 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const jwks = require('jwks-rsa')
const jwt = require('jsonwebtoken')

const {
applyTo: thrush, composeP, curryN, dissoc, partialRight, prop, replace
applyTo: thrush, compose, composeP, curryN, omit, merge,
mergeDeepRight, partialRight, prop, replace
} = require('ramda')

const { promisify, rename, tapP } = require('@articulate/funky')
Expand All @@ -14,11 +15,11 @@ const wellKnown = '/.well-known/openid-configuration'
const bindFunction = client =>
promisify(client.getSigningKey, client)

const buildClient = url =>
const buildClient = (jwksOpts, url) =>
gimme({ url })
.then(prop('body'))
.then(rename('jwks_uri', 'jwksUri'))
.then(jwks)
.then(compose(jwks, merge(jwksOpts)))
.then(bindFunction)

const chooseKey = key =>
Expand All @@ -29,15 +30,19 @@ const decode = partialRight(jwt.decode, [{ complete: true }])
const enforce = token =>
token || Promise.reject(Boom.unauthorized('null token not allowed'))

const stripBearer =
const stripBearer =
replace(/^Bearer /i, '')

const unauthorized = err =>
Promise.reject(Boom.wrap(err, 401))
Promise.reject(Boom.unauthorized(err))

const factory = opts => {
const jwksOptsDefaults = { jwks: { cache: true, rateLimit: true } }

const factory = options => {
const clients = {}
const verifyOpts = dissoc('issWhitelist', opts)
const opts = mergeDeepRight(jwksOptsDefaults, options)
const verifyOpts = omit([ 'issWhitelist', 'jwks' ], opts)
const jwksOpts = prop('jwks', opts)

const cacheClient = iss => client =>
clients[iss] = client
Expand All @@ -49,7 +54,7 @@ const factory = opts => {
const getSigningKey = ({ header: { kid }, payload: { iss } }) =>
clients[iss]
? clients[iss](kid)
: buildClient(iss.replace(/\/$/, '') + wellKnown)
: buildClient(jwksOpts, iss.replace(/\/$/, '') + wellKnown)
.then(cacheClient(iss))
.then(thrush(kid))

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@
"dependencies": {
"@articulate/funky": "^1.2.0",
"@articulate/gimme": "^1.0.0",
"boom": "5.2.0",
"boom": "7.3.x",
"jsonwebtoken": "^8.1.1",
"jwks-rsa": "^1.2.1",
"ramda": "^0.25.0"
},
"devDependencies": {
"chai": "^4.1.2",
"coveralls": "^3.0.0",
"eslint": "^4.16.0",
"eslint": "5.13.x",
"mocha": "^5.0.0",
"nock": "^9.1.6",
"nyc": "^11.4.1",
"nock": "10.x.x",
"nyc": "13.x.x",
"prop-factory": "^1.0.0"
}
}
2 changes: 1 addition & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('authentic', () => {
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
})

describe('with a valid jwt that starts with bearer', () => {
beforeEach(() =>
authentic(lowerBearerToken).then(res)
Expand Down