Skip to content

Commit

Permalink
Add options for jwks-rsa to enable cache and rate limiting (#6)
Browse files Browse the repository at this point in the history
* add options for jwks to enable cache and rate limiting

* better setup for jwksOptionsDefaults

* fix up tests, upgrade boom, nock, nyc, eslint

* add travis support for node 10 ci

* remove travis support for node 7

* mergeDeepRight to get new options

* remove extra export

* update README with jwks-rsa details

* nest verify options under verify prop

* test coverage to 100, upgrade mocha coveralls

* make sure release process bumps version
  • Loading branch information
cdwills committed Feb 25, 2019
1 parent 614ccaa commit 0d45e63
Show file tree
Hide file tree
Showing 6 changed files with 2,153 additions and 1,305 deletions.
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'
before_install:
- npm install -g yarn@1.3.2
Expand Down
16 changes: 14 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,17 @@ const handler = req =>
authentic(req.cookies.token)
.then(/* the JWT has been validated */)
```

## Options

`authentic` accepts a JSON object with the following options:

* `jwks` Object: options to forward to [`node-jwks-rsa`](https://github.com/auth0/node-jwks-rsa) with the following defaults:

| option | default |
| ----------- | ------- |
| `cache` | `true` |
| `rateLimit` | `true` |

* `verify` Object: options to forward to `jwt.verify` from [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback)
* `issWhitelist` Array: list of trusted OIDC issuers
25 changes: 16 additions & 9 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, 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,21 @@ 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 clients = {}
const verifyOpts = dissoc('issWhitelist', opts)
const jwksOptsDefaults = { jwks: { cache: true, rateLimit: true } }

const factory = options => {
const clients = {}
const opts = mergeDeepRight(jwksOptsDefaults, options)
const {
verify: verifyOpts = {},
jwks: jwksOpts
} = opts

const cacheClient = iss => client =>
clients[iss] = client
Expand All @@ -49,7 +56,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
10 changes: 5 additions & 5 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",
"mocha": "^5.0.0",
"nock": "^9.1.6",
"nyc": "^11.4.1",
"eslint": "5.14.x",
"mocha": "6.0.x",
"nock": "10.x.x",
"nyc": "13.x.x",
"prop-factory": "^1.0.0"
}
}
170 changes: 102 additions & 68 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ const lowerBearerToken = 'bearer ' + token

const { issuer } = oidc

const authentic = require('..')({
ignoreExpiration: true,
issWhitelist: [ issuer ],
})

const badIss = jwt.decode(bad, { complete: true }).payload.iss

Expand All @@ -35,86 +31,124 @@ describe('authentic', () => {
nock.cleanAll()
)

describe('with a valid jwt', () => {
beforeEach(() =>
authentic(token).then(res)
)
describe('setup with minimal valid configuration options', () => {
const authentic = require('..')({
issWhitelist: [ issuer ],
})

it('validates the jwt against the jwks', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
describe('with an expired jwt', () => {
beforeEach(() =>
authentic(token).catch(res)
)

it('caches the jwks client', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
})
})
})

describe('with a valid jwt that starts with Bearer', () => {
beforeEach(() =>
authentic(capitalBearerToken).then(res)
)
describe('setup with valid configuration options', () => {
const authentic = require('..')({
verify: { ignoreExpiration: true },
issWhitelist: [ issuer ],
})

it('validates the jwt against the jwks', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
describe('with a valid jwt', () => {
beforeEach(() =>
authentic(token).then(res)
)

it('caches the jwks client', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
})

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

it('validates the jwt against the jwks', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)

it('caches the jwks client', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
})
it('validates the jwt against the jwks', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)

it('caches the jwks client', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
})

describe('with an invalid jwt', () => {
beforeEach(() =>
authentic('invalid').catch(res)
)
describe('with a valid jwt that starts with Bearer', () => {
beforeEach(() =>
authentic(capitalBearerToken).then(res)
)

it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
it('validates the jwt against the jwks', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)

it('caches the jwks client', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
})
})

describe('with an invalid iss', () => {
beforeEach(() =>
authentic(bad).catch(res)
)
describe('with a valid jwt that starts with bearer', () => {
beforeEach(() =>
authentic(lowerBearerToken).then(res)
)

it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
it('validates the jwt against the jwks', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)

it('caches the jwks client', () =>
expect(res().sub).to.equal('00udjyjssbt2S1QVr0h7')
)
})

it('includes the invalid iss in the error message', () =>
expect(res().output.payload.message).to.contain(badIss)
)
})
describe('with an invalid jwt', () => {
beforeEach(() =>
authentic('invalid').catch(res)
)

it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
})
})

describe('with an expired jwt', () => {
beforeEach(() => {
const auth = require('..')({
issWhitelist: [ issuer ],
})
auth(token).catch(res)
})

it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
})
})

describe('with an invalid iss', () => {
beforeEach(() =>
authentic(bad).catch(res)
)

describe('with a null token', () => {
beforeEach(() =>
authentic(null).catch(res)
)
it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
})

it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
it('includes the invalid iss in the error message', () =>
expect(res().output.payload.message).to.contain(badIss)
)
})

it('mentions that the token was null', () =>
expect(res().output.payload.message).to.contain('null token')
)
describe('with a null token', () => {
beforeEach(() =>
authentic(null).catch(res)
)

it('booms with a 401', () => {
expect(res().isBoom).to.be.true
expect(res().output.statusCode).to.equal(401)
})

it('mentions that the token was null', () =>
expect(res().output.payload.message).to.contain('null token')
)
})
})
})

0 comments on commit 0d45e63

Please sign in to comment.