Skip to content

Commit

Permalink
More Features, Example Application, Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Jul 13, 2016
1 parent be2d7c7 commit 49ae565
Show file tree
Hide file tree
Showing 26 changed files with 1,424 additions and 214 deletions.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node example/index.js
112 changes: 83 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,63 @@
# oidc-client
# openid-client

[![build][travis-image]][travis-url] [![codecov][codecov-image]][codecov-url] [![npm][npm-image]][npm-url] [![licence][licence-image]][licence-url]

oidc-client is a server side [OpenID][openid-connect] Relying Party (RP, Client) implementation for
openid-client is a server side [OpenID][openid-connect] Relying Party (RP, Client) implementation for
Node.js

## Example
Head over to the example folder to see the library in use. This example is deployed and configured
to use an example OpenID Connect Provider [here][heroku-example]. The provider is using
[oidc-provider][oidc-provider] library.

## Get started
On the off-chance you want to manage multiple clients for multiple issuers you need to first get
a Provider.
an Issuer instance.

### via Discovery
### via Discovery (recommended)
```js
const Provider = require('oidc-client').Provider;
Provider.discover('https://accounts.google.com') // => Promise
.then(function (googleProvider) {
console.log('Discovered issuer %s', googleProvider.issuer);
const Issuer = require('openid-client').Issuer;
Issuer.discover('https://accounts.google.com') // => Promise
.then(function (googleIssuer) {
console.log('Discovered issuer %s', googleIssuer);
});
```

### manually
```js
const Provider = require('oidc-client').Provider;
const googleProvider = new Provider({
const Issuer = require('openid-client').Issuer;
const googleIssuer = new Issuer({
issuer: 'https://accounts.google.com',
authorization_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
token_endpoint: 'https://www.googleapis.com/oauth2/v4/token',
userinfo_endpoint: 'https://www.googleapis.com/oauth2/v3/userinfo',
jwks_uri: 'https://www.googleapis.com/oauth2/v3/certs',
}); // => Provider
console.log('Set up issuer %s', googleProvider.issuer);
}); // => Issuer
console.log('Set up issuer %s', googleIssuer);
```

Now you can create your Client.
**Now you can create your Client.**

### manually
### manually (recommended)
You should provide the following metadata; `client_id, client_secret`. You can also provide
`id_token_signed_response_alg` (defaults to `RS256`) and `token_endpoint_auth_method` (defaults to
`client_secret_basic`);

```js
const client = new googleProvider.Client({
const client = new googleIssuer.Client({
client_id: 'zELcpfANLqY7Oqas',
client_secret: 'TQV5U29k1gHibH5bx1layBo0OSAvAbRT3UYW3EWrSYBB5swxjVfWUa1BS8lqzxG/0v9wruMcrGadany3'
}) // => Client
}); // => Client
```

### via Dynamic Registration
Should your provider support Dynamic Registration and/or provided you with a registration client uri
and registration access token you can also have the Client discovered.
### via registration client uri
Should your oidc provider have provided you with a registration client uri and registration access
token you can also have the Client discovered.
```js
new googleProvider.Client.fromUri(registration_client_uri, registration_access_token) // => Promise
new googleIssuer.Client.fromUri(registration_client_uri, registration_access_token) // => Promise
.then(function (client) {
console.log('Discovered client %s', client.client_id);
})
console.log('Discovered client %s', client);
});
```

## Usage
Expand Down Expand Up @@ -81,6 +86,45 @@ client.refresh(refreshToken) // => Promise
});
```

### Revoke a token
```js
client.revoke(token) // => Promise
.then(function () {
console.log('revoked token %j', token);
});
```

### Introspect a token
```js
client.introspect(token) // => Promise
.then(function (details) {
console.log('token details %j', details);
});
```

### Fetching userinfo
```js
client.userinfo(accessToken) // => Promise
.then(function (userinfo) {
console.log('userinfo %j', userinfo);
});
```

via POST
```js
client.userinfo(accessToken, { verb: 'post' }); // => Promise
```

auth via query
```js
client.userinfo(accessToken, { via: 'query' }); // => Promise
```

auth via body
```js
client.userinfo(accessToken, { verb: 'post', via: 'body' }); // => Promise
```

### Custom token endpoint grants
Use when the token endpoint also supports client_credentials or password grants;

Expand All @@ -96,12 +140,22 @@ client.grant({
}); // => Promise
```

[travis-image]: https://img.shields.io/travis/panva/node-oidc-client/master.svg?style=flat-square&maxAge=7200
[travis-url]: https://travis-ci.org/panva/node-oidc-client
[codecov-image]: https://img.shields.io/codecov/c/github/panva/node-oidc-client/master.svg?style=flat-square&maxAge=7200
[codecov-url]: https://codecov.io/gh/panva/node-oidc-client
[npm-image]: https://img.shields.io/npm/v/oidc-client.svg?style=flat-square&maxAge=7200
[npm-url]: https://www.npmjs.com/package/oidc-client
[licence-image]: https://img.shields.io/github/license/panva/node-oidc-client.svg?style=flat-square&maxAge=7200
### Registering new client (via Dynamic Registration)
```js
issuer.Client.register(metadata, [keystore]) // => Promise
.then(function (client) {
console.log('Registered client %s, %j', client, client.metadata);
});
```

[travis-image]: https://img.shields.io/travis/panva/node-openid-client/master.svg?style=flat-square&maxAge=7200
[travis-url]: https://travis-ci.org/panva/node-openid-client
[codecov-image]: https://img.shields.io/codecov/c/github/panva/node-openid-client/master.svg?style=flat-square&maxAge=7200
[codecov-url]: https://codecov.io/gh/panva/node-openid-client
[npm-image]: https://img.shields.io/npm/v/openid-client.svg?style=flat-square&maxAge=7200
[npm-url]: https://www.npmjs.com/package/openid-client
[licence-image]: https://img.shields.io/github/license/panva/node-openid-client.svg?style=flat-square&maxAge=7200
[licence-url]: LICENSE.md
[openid-connect]: http://openid.net/connect/
[heroku-example]: https://tranquil-reef-95185.herokuapp.com/client
[oidc-provider]: https://github.com/panva/node-oidc-provider
208 changes: 208 additions & 0 deletions example/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/* eslint-disable import/no-extraneous-dependencies, func-names */
'use strict';

const _ = require('lodash');
const decode = require('base64url').decode;
const koa = require('koa');
const crypto = require('crypto');
const url = require('url');
const uuid = require('node-uuid').v4;
const jose = require('node-jose');
const path = require('path');
const Router = require('koa-router');
const session = require('koa-session');
const render = require('koa-ejs');

module.exports = issuer => {
const app = koa();

if (process.env.HEROKU) {
app.proxy = true;

app.use(function * (next) {
if (this.secure) {
yield next;
} else {
this.redirect(this.href.replace(/^http:\/\//i, 'https://'));
}
});
}

app.keys = ['some secret hurr'];
app.use(session(app));

const CLIENTS = new Map();
const TOKENS = new Map();

render(app, {
cache: false,
layout: '_layout',
root: path.join(__dirname, 'views'),
});

app.use(function * (next) {
this.session.id = this.session.id || uuid();
yield next;
});

app.use(function * (next) {
try {
yield next;
} catch (error) {
yield this.render('error', { error, session: this.session });
}
});

app.use(function * (next) {
if (!CLIENTS.has(this.session.id)) {
const keystore = jose.JWK.createKeyStore();
yield keystore.generate.apply(keystore,
_.sample([['RSA', 2048], ['EC', _.sample(['P-256', 'P-384', 'P-521'])]]));

const client = yield issuer.Client.register({
grant_types: ['authorization_code', 'refresh_token'],
post_logout_redirect_uris: [url.resolve(this.href, '/')],
redirect_uris: [url.resolve(this.href, 'cb')],
response_types: ['code'],
// token_endpoint_auth_method: 'client_secret_jwt',
token_endpoint_auth_method: 'private_key_jwt',
// id_token_signed_response_alg: 'HS256',
// token_endpoint_auth_signing_alg: 'HS256',
// });
}, keystore);
CLIENTS.set(this.session.id, client);
}
yield next;
});

const router = new Router();

router.get('/', function * () {
yield this.render('index', { session: this.session });
});

router.get('/issuer', function * () {
yield this.render('issuer', {
issuer,
keystore: (yield issuer.keystore()),
session: this.session,
});
});

router.get('/client', function * () {
yield this.render('client', { client: CLIENTS.get(this.session.id), session: this.session });
});

router.get('/logout', function * () {
const id = this.session.id;
this.session = null;

if (!TOKENS.has(id)) {
return this.redirect('/');
}

const tokens = TOKENS.get(id);

yield CLIENTS.get(id).revoke(tokens.access_token);

return this.redirect(url.format(Object.assign(url.parse(issuer.end_session_endpoint), {
search: null,
query: {
id_token_hint: tokens.id_token,
post_logout_redirect_uri: url.resolve(this.href, '/'),
},
})));
});

router.get('/login', function * () {
this.session.state = crypto.randomBytes(16).toString('hex');
this.session.nonce = crypto.randomBytes(16).toString('hex');
const authz = CLIENTS.get(this.session.id).authorizationUrl({
claims: {
id_token: { email_verified: null },
userinfo: { sub: null, email: null },
},
redirect_uri: url.resolve(this.href, 'cb'),
scope: 'openid',
// prompt: 'consent',
state: this.session.state,
nonce: this.session.nonce,
});

this.redirect(authz);
});

router.get('/refresh', function * () {
if (!TOKENS.has(this.session.id)) {
this.session = null;
return this.redirect('/');
}

const tokens = TOKENS.get(this.session.id);
const client = CLIENTS.get(this.session.id);

TOKENS.set(
this.session.id,
yield client.refresh(tokens)
);

return this.redirect('/user');
});

router.get('/cb', function * () {
const state = this.session.state;
delete this.session.state;
const nonce = this.session.nonce;
delete this.session.nonce;

TOKENS.set(
this.session.id,
yield CLIENTS.get(this.session.id)
.authorizationCallback(url.resolve(this.href, 'cb'), this.query, { nonce, state }));

this.session.loggedIn = true;

this.redirect('/user');
});

router.get('/user', function * () {
if (!TOKENS.has(this.session.id)) {
this.session = null;
return this.redirect('/');
}
const tokens = TOKENS.get(this.session.id);
const client = CLIENTS.get(this.session.id);

const context = {
tokens,
userinfo: (yield client.userinfo(tokens).catch(() => {})),
id_token: tokens.id_token ? _.map(tokens.id_token.split('.'), part => {
try {
return JSON.parse(decode(part));
} catch (err) {
return part;
}
}) : undefined,
session: this.session,
introspections: {},
};

const introspections = _.map(tokens, (value, key) => {
if (key.endsWith('token') && key !== 'id_token') {
return client.introspect(value).then((response) => {
context.introspections[key] = response;
});
}
return undefined;
});

yield Promise.all(introspections);

return yield this.render('user', context);
});

app.use(router.routes());
app.use(router.allowedMethods());

return app;
};
12 changes: 12 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

const Issuer = require('..').Issuer;
const ISSUER = process.env.ISSUER || 'https://guarded-cliffs-8635.herokuapp.com';
const port = process.env.PORT || 3001;

const appFactory = require('./app');

Issuer.discover(ISSUER).then(issuer => {
const app = appFactory(issuer);
app.listen(port);
}).catch(() => process.exit(1));
Loading

0 comments on commit 49ae565

Please sign in to comment.