Skip to content

Commit

Permalink
feat: add helpers for generating secure random values & PKCE challenges
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed May 13, 2019
1 parent 4b27b39 commit 44f1865
Show file tree
Hide file tree
Showing 16 changed files with 75 additions and 33 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ or koa middlewares. Those can however be built using the exposed API.
- [Customizing][documentation-customizing]
- [TokenSet][documentation-tokenset]
- [Strategy][documentation-strategy]
- [generators][documentation-generators]
- [errors][documentation-errors]

## Quick start
Expand Down Expand Up @@ -120,20 +121,17 @@ to get the authorization endpoint's URL with parameters already encoded in the q
to.

```js
const code_verifier = crypto.randomBytes(32).toString('hex');
const { generators } = require('openid-client');
const code_verifier = generators.codeVerifier();
// store the code_verifier in your framework's session mechanism, if it is a cookie based solution
// it should be httpOnly (not readable by javascript) and encrypted.

// sha256 digest of the code_verifier in base64url with no padding
const challenge = crypto.createHash('sha256')
.update(code_verifier)
.digest('base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
const code_challenge = generators.codeChallenge(verifier);

client.authorizationUrl({
scope: 'openid email profile',
resource: 'https://my.api.example.com/resource/32178',
code_challenge: challenge,
code_challenge,
code_challenge_method: 'S256',
});
```
Expand Down Expand Up @@ -190,9 +188,11 @@ to get the authorization endpoint's URL with parameters already encoded in the q
to.

```js
const nonce = crypto.randomBytes(32).toString('hex');
const { generators } = require('openid-client');
const nonce = generators.nonce();
// store the nonce in your framework's session mechanism, if it is a cookie based solution
// it should be httpOnly (not readable by javascript) and encrypted.

client.authorizationUrl({
scope: 'openid email profile',
response_mode: 'form_post',
Expand Down Expand Up @@ -271,4 +271,5 @@ See [Client Authentication Methods][documentation-methods].
[documentation-tokenset]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#tokenset
[documentation-strategy]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#strategy
[documentation-errors]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#errors
[documentation-generators]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#generators
[documentation-methods]: https://github.com/panva/node-openid-client/blob/master/docs/README.md#client-authentication-methods
33 changes: 32 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Customizing](#customizing)
- [TokenSet](#tokenset)
- [Strategy](#strategy)
- [generators](#generators)
- [errors](#errors)

## Sponsor
Expand Down Expand Up @@ -652,11 +653,41 @@ Creates a new Strategy
- `[userinfo]`: `<Object>` Optional argument, omit it when you don't want to load userinfo and
are fine using 'tokenset.claims()' alone.
- `done`: `<Function>`

- Returns: `<Strategy>`

---

## generators

<!-- TOC generators START -->
- [generators.random([bytes])](#generatorsrandombytes)
- [generators.state([bytes])](#generatorsrandombytes)
- [generators.nonce([bytes])](#generatorsrandombytes)
- [generators.codeVerifier([bytes])](#generatorsrandombytes)
- [generators.codeChallenge(codeVerifier)](#generatorscodechallengeverifier)
<!-- TOC generators END -->

---

#### `generators.random([bytes])`

Generates random bytes and encodes them in url safe base64. This method is also aliased as
`generators.nonce`, `generators.state` and `generators.codeVerifier`

- `bytes`: `<number>` Number indicating the number of bytes to generate. **Default:** 32
- Returns: `<string>`

---

#### `generators.codeChallenge(verifier)`

Calculates the S256 PKCE code challenge for an arbitrary code verifier.

- `verifier`: `<string>` Code verifier to calculate the S256 code challenge for.
- Returns: `<string>`

---

## Errors

<!-- TOC Errors START -->
Expand Down
8 changes: 4 additions & 4 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ const {
} = require('lodash');
const tokenHash = require('oidc-token-hash');

const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./util/assert');
const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./helpers/assert');
const pick = require('./helpers/pick');
const processResponse = require('./helpers/process_response');
const TokenSet = require('./token_set');
const { OPError, RPError } = require('./errors');
const now = require('./util/unix_timestamp');
const random = require('./util/random');
const request = require('./util/request');
const now = require('./helpers/unix_timestamp');
const { random } = require('./helpers/generators');
const request = require('./helpers/request');
const {
CALLBACK_PROPERTIES, CLIENT_DEFAULTS, JWT_CONTENT, CLOCK_TOLERANCE,
} = require('./helpers/consts');
Expand Down
File renamed without changes.
9 changes: 4 additions & 5 deletions lib/helpers/client.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
const { merge } = require('lodash');
const jose = require('@panva/jose');

const { assertIssuerConfiguration } = require('../util/assert');
const random = require('../util/random');
const now = require('../util/unix_timestamp');
const request = require('../util/request');

const { assertIssuerConfiguration } = require('./assert');
const { random } = require('./generators');
const now = require('./unix_timestamp');
const request = require('./request');
const instance = require('./weak_cache');

const formUrlEncode = value => encodeURIComponent(value).replace(/%20/g, '+');
Expand Down
13 changes: 13 additions & 0 deletions lib/helpers/generators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { createHash, randomBytes } = require('crypto');

const { encode: base64url } = require('base64url');

const random = (bytes = 32) => base64url(randomBytes(bytes));

module.exports = {
random,
state: random,
nonce: random,
codeVerifier: random,
codeChallenge: codeVerifier => base64url(createHash('sha256').update(codeVerifier).digest()),
};
5 changes: 3 additions & 2 deletions lib/util/request.js → lib/helpers/request.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const got = require('got');
const { defaultsDeep } = require('lodash');

const isAbsoluteUrl = require('../helpers/is_absolute_url');
const pkg = require('../../package.json');
const { HTTP_OPTIONS } = require('../helpers/consts');

const isAbsoluteUrl = require('./is_absolute_url');
const { HTTP_OPTIONS } = require('./consts');

const USER_AGENT = `${pkg.name}/${pkg.version} (${pkg.homepage})`;

Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Registry = require('./issuer_registry');
const Strategy = require('./passport_strategy');
const TokenSet = require('./token_set');
const { CLOCK_TOLERANCE, HTTP_OPTIONS } = require('./helpers/consts');
const generators = require('./helpers/generators');

module.exports = {
Issuer,
Expand All @@ -18,4 +19,5 @@ module.exports = {
http_options: HTTP_OPTIONS,
clock_tolerance: CLOCK_TOLERANCE,
},
generators,
};
6 changes: 3 additions & 3 deletions lib/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const { RPError } = require('./errors');
const getClient = require('./client');
const registry = require('./issuer_registry');
const processResponse = require('./helpers/process_response');
const webfingerNormalize = require('./util/webfinger_normalize');
const webfingerNormalize = require('./helpers/webfinger_normalize');
const instance = require('./helpers/weak_cache');
const request = require('./util/request');
const { assertIssuerConfiguration } = require('./util/assert');
const request = require('./helpers/request');
const { assertIssuerConfiguration } = require('./helpers/assert');
const {
ISSUER_DEFAULTS, OIDC_DISCOVERY, OAUTH2_DISCOVERY, WEBFINGER, REL, AAD_MULTITENANT_DISCOVERY,
} = require('./helpers/consts');
Expand Down
4 changes: 2 additions & 2 deletions lib/passport_strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const base64url = require('base64url');

const { RPError, OPError } = require('./errors');
const { BaseClient } = require('./client');
const random = require('./util/random');
const { random, codeChallenge } = require('./helpers/generators');
const pick = require('./helpers/pick');
const { resolveResponseType, resolveRedirectUri } = require('./helpers/client');

Expand Down Expand Up @@ -106,7 +106,7 @@ OpenIDConnectStrategy.prototype.authenticate = function authenticate(req, option

switch (this._usePKCE) { // eslint-disable-line default-case
case 'S256':
params.code_challenge = base64url.encode(crypto.createHash('sha256').update(verifier).digest());
params.code_challenge = codeChallenge(verifier);
params.code_challenge_method = 'S256';
break;
case 'plain':
Expand Down
2 changes: 1 addition & 1 deletion lib/token_set.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const base64url = require('base64url');

const now = require('./util/unix_timestamp');
const now = require('./helpers/unix_timestamp');

class TokenSet {
/**
Expand Down
5 changes: 0 additions & 5 deletions lib/util/random.js

This file was deleted.

2 changes: 1 addition & 1 deletion test/client/client_instance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const timekeeper = require('timekeeper');

const TokenSet = require('../../lib/token_set');
const { OPError } = require('../../lib/errors');
const now = require('../../lib/util/unix_timestamp');
const now = require('../../lib/helpers/unix_timestamp');
const { Registry, Issuer, custom } = require('../../lib');
const clientInternal = require('../../lib/helpers/client');

Expand Down
2 changes: 1 addition & 1 deletion test/tokenset/tokenset.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const base64url = require('base64url');
const { expect } = require('chai');

const TokenSet = require('../../lib/token_set');
const now = require('../../lib/util/unix_timestamp');
const now = require('../../lib/helpers/unix_timestamp');

describe('TokenSet', function () {
after(function () {
Expand Down

0 comments on commit 44f1865

Please sign in to comment.