Skip to content

Commit

Permalink
Integration with koa via koa-jwt + tests + documentation (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdd authored and sandrinodimattia committed Jun 27, 2017
1 parent b8fa525 commit 2ff3913
Show file tree
Hide file tree
Showing 8 changed files with 401 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -28,6 +28,7 @@ Integrations are also provided with:

- [express/express-jwt](examples/express-demo)
- [hapi/hapi-auth-jwt2](examples/hapi-demo)
- [koa/koa-jwt](examples/koa-demo)

### Caching

Expand Down
97 changes: 97 additions & 0 deletions examples/koa-demo/README.md
@@ -0,0 +1,97 @@
# jwks-rsa - Koa Example

The `jwks-rsa` library provides a small helper that makes it easy to configure `koa-jwt` with the `RS256` algorithm. Using `koaJwtSecret` you can generate a key provider that will provide the right signing key to `koa-jwt` based on the `kid` in the JWT header.

```js
const Koa = require('koa');
const Router = require('koa-router');
const jwt = require('koa-jwt');
const jwksRsa = require('jwks-rsa');

...

// Start the server.
const app = new Koa();

app.use(jwt({
secret: jwksRsa.koaJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 2,
jwksUri: `${jwksHost}/.well-known/jwks.json`
}),
audience,
issuer,
algorithms: [ 'RS256' ]
}));

const router = new Router();

router.get('/me', ctx => {
ctx.body = ctx.state.user
});

app.use(router.middleware());

// Start the server.
const port = process.env.PORT || 4001;
app.listen(port);
```

## Running the sample

```bash
DEBUG=koa,koa-jwt JWKS_HOST=https://my-authz-server AUDIENCE=urn:my-resource-server ISSUER=https://my-authz-server/ node server.js
```

> Tip: You can use Auth0 to test this.
## How does this work?

When you have the sample running you'll need to get a token from your Authorization Server, which will look like this:

```
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJrSTVNakk1T1VZNU9EYzFOMFE0UXpNME9VWXpOa1ZHTVRKRE9VRXpRa0ZDT1RVM05qRTJSZyJ9.eyJpc3MiOiJodHRwczovL3NhbmRyaW5vLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1NjMyNTAxZjQ2OGYwZjE3NTZmNGNhYjAiLCJhdWQiOiJQN2JhQnRTc3JmQlhPY3A5bHlsMUZEZVh0ZmFKUzRyViIsImV4cCI6MTQ2ODk2NDkyNiwiaWF0IjoxNDY4OTI4OTI2fQ.NaNeRSDCNu522u4hcVhV65plQOiGPStgSzVW4vR0liZYQBlZ_3OKqCmHXsu28NwVHW7_KfVgOz4m3BK6eMDZk50dAKf9LQzHhiG8acZLzm5bNMU3iobSAJdRhweRht544ZJkzJ-scS1fyI4gaPS5aD3SaLRYWR0Xsb6N1HU86trnbn-XSYSspNqzIUeJjduEpPwC53V8E2r1WZXbqEHwM9_BGEeNTQ8X9NqCUvbQtnylgYR3mfJRL14JsCWNFmmamgNNHAI0uAJo84mu_03I25eVuCK0VYStLPd0XFEyMVFpk48Bg9KNWLMZ7OUGTB_uv_1u19wKYtqeTbt9m1YcPMQ
```

If you then decode this token (using [jwt.io](https://jwt.io)), you'll see the following header:

```json
{
"typ": "JWT",
"alg": "RS256",
"kid": "RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg"
}
```

Using this `kid` we will try to find the right signing key in the singing keys provided by the JWKS endpoint you configured.

You can then call the sample application like this:

```js
var request = require("request");

var options = {
method: 'GET',
url: 'http://localhost:4001/me',
headers: { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI...' }
};

request(options, function (error, response, body) {
if (error) throw new Error(error);

console.log(body);
});
```

A few things will happen now:

1. `koa-jwt` will decode the token and pass the request and the decoded token to `jwksRsa.koaJwtSecret`
2. `jwks-rsa` will then download all signing keys from the JWKS endpoint and see if a one of the signing keys matches the `kid` in the header of the JWT.
a. If none of the signing keys match the incoming `kid`, an error will be thrown
b. If we have a match, we will pass the right signing key to `koa-jwt`, otherwise throw
3. `koa-jwt` will the continue its own logic to validate the signature of the token, the expiration, audience, issuer, ...

If you repeat this call a few times you'll see in the console output that we're not calling the JWKS endpoint anymore, because caching has been enabled.

If you then make multiple calls with a `kid` that is not defined in the JWKS endpoint, you'll see that rate limiting will kick in.
26 changes: 26 additions & 0 deletions examples/koa-demo/package.json
@@ -0,0 +1,26 @@
{
"name": "koa-demo",
"version": "1.0.0",
"description": "Sample that shows how tu use jwks-rsa with Koa",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"jwks-rsa",
"jwt",
"rs256",
"koa"
],
"author": "Scott Donnelly",
"contributors": [
"Maxim Vorobjov"
],
"license": "MIT",
"dependencies": {
"debug": "^2.2.0",
"koa": "^2.2.0",
"koa-jwt": "^3.2.1",
"koa-router": "^7.1.1"
}
}
35 changes: 35 additions & 0 deletions examples/koa-demo/server.js
@@ -0,0 +1,35 @@
const Koa = require('koa');
const Router = require('koa-router');
const jwt = require('koa-jwt');
const jwksRsa = require('../../lib');

const jwksHost = process.env.JWKS_HOST;
const audience = process.env.AUDIENCE;
const issuer = process.env.ISSUER;

// Initialize the app.
const app = new Koa();

app.use(jwt({
secret: jwksRsa.koaJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 2,
jwksUri: `${jwksHost}/.well-known/jwks.json`
}),
audience,
issuer,
algorithms: [ 'RS256' ]
}));

const router = new Router();

router.get('/me', ctx => {
ctx.body = ctx.state.user
});

app.use(router.middleware());

// Start the server.
const port = process.env.PORT || 4001;
app.listen(port);
5 changes: 4 additions & 1 deletion package.json
Expand Up @@ -23,9 +23,12 @@
"eslint-plugin-babel": "^3.2.0",
"express-jwt": "^3.4.0",
"jsonwebtoken": "^7.1.7",
"koa": "^2.2.0",
"koa-jwt": "^3.2.0",
"mocha": "^2.5.3",
"nock": "^8.0.0",
"rimraf": "^2.5.2"
"rimraf": "^2.5.2",
"supertest": "^3.0.0"
},
"scripts": {
"clean": "rimraf lib/",
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Expand Up @@ -3,6 +3,7 @@ import { JwksClient } from './JwksClient';
import * as errors from './errors';
import { hapiJwt2Key } from './integrations/hapi';
import { expressJwtSecret } from './integrations/express';
import { koaJwtSecret } from './integrations/koa';

module.exports = (options) => {
return new JwksClient(options);
Expand All @@ -15,3 +16,4 @@ module.exports.SigningKeyNotFoundError = errors.SigningKeyNotFoundError;

module.exports.expressJwtSecret = expressJwtSecret;
module.exports.hapiJwt2Key = hapiJwt2Key;
module.exports.koaJwtSecret = koaJwtSecret;
37 changes: 37 additions & 0 deletions src/integrations/koa.js
@@ -0,0 +1,37 @@
import { ArgumentError } from '../errors';
import { JwksClient } from '../JwksClient';

module.exports.koaJwtSecret = (options = {}) => {

if (!options.jwksUri) {
throw new ArgumentError('No JWKS URI provided');
}

const client = new JwksClient(options);

return function secretProvider({ alg, kid } = {}) {

return new Promise((resolve, reject) => {

// Only RS256 is supported.
if (alg !== 'RS256') {
return reject(new Error('Missing / invalid token algorithm'));
}

client.getSigningKey(kid, (err, key) => {
if (err) {

if (options.handleSigningKeyError) {
return options.handleSigningKeyError(err)
.then(reject);
}

return reject(err);
}

// Provide the key.
resolve(key.publicKey || key.rsaPublicKey);
});
});
};
};

0 comments on commit 2ff3913

Please sign in to comment.