Skip to content

Commit

Permalink
Merge 1d4a073 into 4748a18
Browse files Browse the repository at this point in the history
  • Loading branch information
flintinatux committed Jun 27, 2018
2 parents 4748a18 + 1d4a073 commit 9d23d29
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 32 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,33 @@ Proper validation of JWT's against JWK's.

## Motivation

The process of validating JWT's against JWK's is [quite involved](https://auth0.com/blog/navigating-rs256-and-jwks/), but at the end of the day, you've probably got an auth server with a `/.well-known/openid-configuration` endpoint, and you just want to know if an incoming JWT is valid. End of story. You don't want to fumble with parsing the JWT, matching `kid` values, converting certs, and caching JWK's.
The process of validating JWT's against JWK's is [quite involved](https://auth0.com/blog/navigating-rs256-and-jwks/), but at the end of the day, you probably have an auth server with a `/.well-known/openid-configuration` endpoint, and you just want to know if an incoming JWT is valid. End of story. You don't want to fumble with parsing the JWT, matching `kid` values, converting certs, or caching JWK's.

Now you don't need to. Initialize `authentic` with your base `oidcURI`, and you'll receive a function that accepts a JWT and validates it. The rest is handled for you.
Now you don't need to. Initialize `authentic` with an `issWhitelist`, and you'll receive a function that accepts a JWT and validates it. The rest is handled for you.

## Usage

```haskell
authentic :: { k: v } -> String -> Promise Boom { k: v }
```

Initialize `authentic` with an options object containing your base `oidcURI`. For example:
Initialize `authentic` with an options object containing an `issWhitelist` array listing the `token.payload.iss` values you will accept. For example:

| Provider | Suggested `oidcURI` |
| Provider | Sample `issWhitelist` |
| -------- | ------------------- |
| [Auth0](https://auth0.com/) | `https://${tenant}.auth0.com` |
| [Okta](https://www.okta.com/) | `https://${tenant}.oktapreview.com/oauth2/${appName}` |
| [Auth0](https://auth0.com/) | `[ 'https://${tenant}.auth0.com' ]` |
| [Okta](https://www.okta.com/) | `[ 'https://${tenant}.oktapreview.com/oauth2/${appName}' ]` |

**Note:** Don't include the `/.well-known/openid-configuration` in your `oidcURI`, as `authentic` will add that for you.
**Note:** Don't include the `/.well-known/openid-configuration` in your `issWhitelist` values, as `authentic` will add that for you.

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
const authentic = require('@articulate/authentic')({ oidcURI: process.env.OIDC_URI })
const authentic = require('@articulate/authentic')({
issWhitelist: JSON.parse(process.env.ISS_WHITELIST)
})

const handler = req =>
authentic(req.cookies.token)
Expand Down
26 changes: 16 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ const Boom = require('boom')
const gimme = require('@articulate/gimme')
const jwks = require('jwks-rsa')
const jwt = require('jsonwebtoken')
const property = require('prop-factory')

const {
applyTo: thrush, curryN, dissoc, partialRight, path, prop
applyTo: thrush, curryN, dissoc, partialRight, prop
} = require('ramda')

const { promisify, rename } = require('@articulate/funky')
Expand All @@ -31,23 +30,30 @@ const unauthorized = err =>
Promise.reject(Boom.wrap(err, 401))

const factory = opts => {
const verifyOpts = dissoc('oidcURI', opts)
const clients = {}
const verifyOpts = dissoc('issWhitelist', opts)

const getKey = property()
const cacheClient = iss => client =>
clients[iss] = client

const getSigningKey = kid =>
getKey()
? getKey()(kid)
: buildClient(opts.oidcURI + wellKnown)
.then(getKey)
const checkIss = token =>
opts.issWhitelist.indexOf(token.payload.iss) > -1
? Promise.resolve(token)
: Promise.reject(new Error(`iss '${token.payload.iss}' not in issWhitelist`))

const getSigningKey = ({ header: { kid }, payload: { iss } }) =>
clients[iss]
? clients[iss](kid)
: buildClient(iss + wellKnown)
.then(cacheClient(iss))
.then(thrush(kid))

const verify = curryN(2, partialRight(promisify(jwt.verify), [ verifyOpts ]))

const authentic = token =>
Promise.resolve(token)
.then(decode)
.then(path(['header', 'kid']))
.then(checkIss)
.then(getSigningKey)
.then(chooseKey)
.then(verify(token))
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@
"test:coverage": "nyc yarn test"
},
"dependencies": {
"@articulate/funky": "^0.1.0",
"@articulate/funky": "^1.2.0",
"@articulate/gimme": "^1.0.0",
"boom": "5.2.0",
"jsonwebtoken": "^8.1.1",
"jwks-rsa": "^1.2.1",
"prop-factory": "^1.0.0",
"ramda": "^0.25.0"
},
"devDependencies": {
Expand All @@ -37,6 +36,7 @@
"eslint": "^4.16.0",
"mocha": "^5.0.0",
"nock": "^9.1.6",
"nyc": "^11.4.1"
"nyc": "^11.4.1",
"prop-factory": "^1.0.0"
}
}
1 change: 1 addition & 0 deletions test/fixtures/bad-iss.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/fixtures/keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
"alg": "RS256",
"e": "AQAB",
"n": "gRuHPh4qGuJweXLgwDdOoTL9vCYTn4_TgCa88KArGb0IulDnRolPCycM99_2EseGbkCuuss4O1r7W6vqfs2AXmFetFU3GOxobc8fG1z_qxB7JYU5bRmWjN6UCK43O7HIbRuvdGqblX98DRlx4NVTFFLoJlFGGu87-Fn2unbR8WbgZwel9saZVeVm2SYkba5hYOAwu3_M3ulilyTOo-kwgg9mU-YW10FD_XXevNcT6j0CCqN3HsRYlDr_7-JVG1hlow6As8WaRrSjXb6JQJoFcvEnPmSauSsgvdkuxtRKc59_T_vOESaL70DX9b2hmkDvHau5p7kO7IIMyUgoPK13FQ",
"n": "AJc7FAIHQeLMoKyR70lJTfby1D_4uW6gAuhR0SrdrCHO-Hou9lRroAyJv6Bsa8E9kW460c8SbPOoaXj-qHFCLbAv7L46ZRYkW7hXqSdx3f35gEQshg7amiZVBExj07ziW4_aljXMiRsRu4PG22YahSM6jr106SzblHOepeCpKSFPOED_CrhV3Ky17vAKfv_YhtSNfnnjk9TyIMr7XFSn5NJntiMbwyVOmVlW1U_HE39aQXo8kBrkM11puFexgs6bWeLzu_jT2-M81B0hbo-OGWxiakZYxKBB80W-ybUpcDqVsRRjo1QluJkfDALj7vAn5iJiC1lhN0y1hn3ohnXzpmk",
"kid": "DaX11gArWQebNHO7EMPML5VtT4Ewrfkwc5SlGiWvUwA",
"kty": "RSA",
"use": "sig"
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/token.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 32 additions & 4 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
const { expect } = require('chai')
const jwt = require('jsonwebtoken')
const nock = require('nock')
const property = require('prop-factory')

const bad = require('./fixtures/bad-iss')
const keys = require('./fixtures/keys')
const oidc = require('./fixtures/oidc')
const token = require('./fixtures/token')

const { issuer: oidcURI } = oidc
const { issuer } = oidc

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

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

const wellKnown = '/.well-known/openid-configuration'

describe('authentic', () => {
const res = property()

beforeEach(() => {
nock(oidcURI).get('/.well-known/openid-configuration').reply(200, oidc)
nock(oidcURI).get('/v1/keys').reply(200, keys)
nock(issuer).get(wellKnown).once().reply(200, oidc)
nock(issuer).get('/v1/keys').once().reply(200, keys)
})

nock.disableNetConnect()
Expand All @@ -32,6 +41,10 @@ describe('authentic', () => {
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', () => {
Expand All @@ -44,4 +57,19 @@ describe('authentic', () => {
expect(res().output.statusCode).to.equal(401)
})
})

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

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)
)
})
})
9 changes: 4 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@
joi "^10.6.0"
ramda "^0.24.1"

"@articulate/funky@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@articulate/funky/-/funky-0.1.0.tgz#d7dc756f62b1f66ba703d13e101e7ae510b625f8"
"@articulate/funky@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@articulate/funky/-/funky-1.2.0.tgz#681776d0f5b0045f9bb00cd623cf398be9fff985"
dependencies:
joi "^10.6.0"
ramda "^0.24.1"
ramda "^0.25.0"

"@articulate/gimme@^1.0.0":
version "1.0.0"
Expand Down

0 comments on commit 9d23d29

Please sign in to comment.