openid-client
Advanced tools
Comparing version 0.6.1 to 0.7.0
Following semver, 1.0.0 will mark the first API stable release and commence of this file, | ||
until then please use the compare views of github for reference. | ||
- https://github.com/panva/node-openid-client/compare/v0.6.0...v0.7.0 | ||
- added: webfinger discovery | ||
- added: callback parameter helper for node's http.IncomingMessage | ||
- tested for lts/argon (4), lts/boron (6) and current stable (7) | ||
- https://github.com/panva/node-openid-client/compare/v0.5.4...v0.6.0 | ||
@@ -5,0 +9,0 @@ - added: handling of symmetrically encrypted responses (A...GCMKW, A...KW, PBES2-HS...+A...KW) |
@@ -5,3 +5,5 @@ 'use strict'; | ||
const assert = require('assert'); | ||
const http = require('http'); | ||
const crypto = require('crypto'); | ||
const querystring = require('querystring'); | ||
const jose = require('node-jose'); | ||
@@ -54,8 +56,17 @@ const uuid = require('node-uuid').v4; | ||
function getFromJWT(jwt, position, claim) { | ||
const parsed = JSON.parse(base64url.decode(jwt.split('.')[position])); | ||
return typeof claim === 'undefined' ? parsed : parsed[claim]; | ||
} | ||
function getSub(jwt) { | ||
return getFromJWT(jwt, 1, 'sub'); | ||
} | ||
function getIss(jwt) { | ||
return JSON.parse(base64url.decode(jwt.split('.')[1])).iss; | ||
return getFromJWT(jwt, 1, 'iss'); | ||
} | ||
function getHeader(jwt) { | ||
return JSON.parse(base64url.decode(jwt.split('.')[0])); | ||
return getFromJWT(jwt, 0); | ||
} | ||
@@ -71,3 +82,3 @@ | ||
function authorizationParams(params) { | ||
assert.ok(typeof params === 'object', 'you must provide an object'); | ||
assert.equal(typeof params, 'object', 'you must provide an object'); | ||
@@ -80,2 +91,5 @@ const authParams = _.defaults(params, { | ||
assert(authParams.response_type === 'code' || authParams.nonce, | ||
'nonce MUST be provided for implicit and hybrid flows'); | ||
if (typeof authParams.claims === 'object') { | ||
@@ -95,3 +109,3 @@ authParams.claims = JSON.stringify(authParams.claims); | ||
if (keystore !== undefined) { | ||
assert.ok(jose.JWK.isKeyStore(keystore), 'keystore must be an instance of jose.JWK.KeyStore'); | ||
assert(jose.JWK.isKeyStore(keystore), 'keystore must be an instance of jose.JWK.KeyStore'); | ||
instance(this).keystore = keystore; | ||
@@ -101,3 +115,3 @@ } | ||
if (this.token_endpoint_auth_method.endsWith('_jwt')) { | ||
assert.ok(this.issuer.token_endpoint_auth_signing_alg_values_supported, | ||
assert(this.issuer.token_endpoint_auth_signing_alg_values_supported, | ||
'token_endpoint_auth_signing_alg_values_supported must be provided on the issuer'); | ||
@@ -131,2 +145,41 @@ } | ||
callbackParams(input) { // eslint-disable-line | ||
const isIncomingMessage = input instanceof http.IncomingMessage; | ||
const isString = typeof input === 'string'; | ||
assert(isString || isIncomingMessage, '#callbackParams only accepts string urls or http.IncomingMessage'); | ||
let uri; | ||
if (isIncomingMessage) { | ||
const msg = input; | ||
switch (msg.method) { | ||
case 'GET': | ||
uri = msg.url; | ||
break; | ||
case 'POST': | ||
assert(msg.body, 'incoming message body missing, include a body parser prior to this call'); | ||
switch (typeof msg.body) { | ||
case 'object': | ||
case 'string': | ||
if (Buffer.isBuffer(msg.body)) { | ||
return querystring.parse(msg.body.toString('utf-8')); | ||
} else if (typeof msg.body === 'string') { | ||
return querystring.parse(msg.body); | ||
} | ||
return msg.body; | ||
default: | ||
throw new Error('invalid IncomingMessage body object'); | ||
} | ||
default: | ||
throw new Error('invalid IncomingMessage method'); | ||
} | ||
} else { | ||
uri = input; | ||
} | ||
return url.parse(uri, true).query; | ||
} | ||
authorizationCallback(redirectUri, parameters, checks) { | ||
@@ -264,8 +317,8 @@ const params = _.pick(parameters, CALLBACK_PROPERTIES); | ||
assert.ok(typeof payloadObject.iat === 'number', 'iat is not a number'); | ||
assert.ok(payloadObject.iat <= now, 'id_token issued in the future'); | ||
assert.equal(typeof payloadObject.iat, 'number', 'iat is not a number'); | ||
assert(payloadObject.iat <= now, 'id_token issued in the future'); | ||
if (payloadObject.nbf !== undefined) { | ||
assert.ok(typeof payloadObject.nbf === 'number', 'nbf is not a number'); | ||
assert.ok(payloadObject.nbf <= now, 'id_token not active yet'); | ||
assert.equal(typeof payloadObject.nbf, 'number', 'nbf is not a number'); | ||
assert(payloadObject.nbf <= now, 'id_token not active yet'); | ||
} | ||
@@ -277,4 +330,4 @@ | ||
assert.ok(typeof payloadObject.exp === 'number', 'exp is not a number'); | ||
assert.ok(now < payloadObject.exp, 'id_token expired'); | ||
assert.equal(typeof payloadObject.exp, 'number', 'exp is not a number'); | ||
assert(now < payloadObject.exp, 'id_token expired'); | ||
@@ -291,10 +344,10 @@ if (!Array.isArray(payloadObject.aud)) { | ||
assert.ok(payloadObject.aud.indexOf(this.client_id) !== -1, 'aud is missing the client_id'); | ||
assert(payloadObject.aud.indexOf(this.client_id) !== -1, 'aud is missing the client_id'); | ||
if (isTokenSet && payloadObject.at_hash) { | ||
assert.ok(tokenHash(payloadObject.at_hash, token.access_token), 'at_hash mismatch'); | ||
assert(tokenHash(payloadObject.at_hash, token.access_token), 'at_hash mismatch'); | ||
} | ||
if (isTokenSet && payloadObject.c_hash) { | ||
assert.ok(tokenHash(payloadObject.c_hash, token.code), 'c_hash mismatch'); | ||
assert(tokenHash(payloadObject.c_hash, token.code), 'c_hash mismatch'); | ||
} | ||
@@ -307,3 +360,5 @@ | ||
return (headerObject.alg.startsWith('HS') ? this.joseSecret() : this.issuer.key(headerObject)) | ||
.then(key => jose.JWS.createVerify(key).verify(idToken)) | ||
.then(key => jose.JWS.createVerify(key).verify(idToken).catch(() => { | ||
throw new Error('invalid signature'); | ||
})) | ||
.then(() => token); | ||
@@ -378,3 +433,10 @@ } | ||
return JSON.parse(response.body); | ||
}, gotErrorHandler); | ||
}, gotErrorHandler) | ||
.then((parsed) => { | ||
if (accessToken.id_token) { | ||
assert.equal(getSub(accessToken.id_token), parsed.sub, 'userinfo sub mismatch'); | ||
} | ||
return parsed; | ||
}); | ||
} | ||
@@ -420,5 +482,5 @@ | ||
revoke(token, hint) { | ||
assert.ok(this.issuer.revocation_endpoint || this.issuer.token_revocation_endpoint, | ||
assert(this.issuer.revocation_endpoint || this.issuer.token_revocation_endpoint, | ||
'issuer must be configured with revocation endpoint'); | ||
assert.ok(!hint || typeof hint === 'string', 'hint must be a string'); | ||
assert(!hint || typeof hint === 'string', 'hint must be a string'); | ||
const endpoint = this.issuer.revocation_endpoint || this.issuer.token_revocation_endpoint; | ||
@@ -433,5 +495,5 @@ | ||
introspect(token, hint) { | ||
assert.ok(this.issuer.introspection_endpoint || this.issuer.token_introspection_endpoint, | ||
assert(this.issuer.introspection_endpoint || this.issuer.token_introspection_endpoint, | ||
'issuer must be configured with introspection endpoint'); | ||
assert.ok(!hint || typeof hint === 'string', 'hint must be a string'); | ||
assert(!hint || typeof hint === 'string', 'hint must be a string'); | ||
const endpoint = this.issuer.introspection_endpoint || this.issuer.token_introspection_endpoint; | ||
@@ -521,3 +583,3 @@ | ||
const key = instance(this).keystore.get({ alg }); | ||
assert.ok(key, 'no valid key found'); | ||
assert(key, 'no valid key found'); | ||
@@ -575,7 +637,7 @@ return Promise.resolve(jose.JWS.createSign({ | ||
static register(body, keystore) { | ||
assert.ok(this.issuer.registration_endpoint, 'issuer does not support dynamic registration'); | ||
assert(this.issuer.registration_endpoint, 'issuer does not support dynamic registration'); | ||
if (keystore !== undefined && !(body.jwks || body.jwks_uri)) { | ||
assert.ok(jose.JWK.isKeyStore(keystore), 'keystore must be an instance of jose.JWK.KeyStore'); | ||
assert.ok(keystore.all().every((key) => { | ||
assert(jose.JWK.isKeyStore(keystore), 'keystore must be an instance of jose.JWK.KeyStore'); | ||
assert(keystore.all().every((key) => { | ||
if (key.kty === 'RSA' || key.kty === 'EC') { | ||
@@ -582,0 +644,0 @@ try { key.toPEM(true); } catch (err) { return false; } |
@@ -5,3 +5,5 @@ const pkg = require('../package.json'); | ||
const WELL_KNOWN = '/.well-known/openid-configuration'; | ||
const DISCOVERY = '/.well-known/openid-configuration'; | ||
const WEBFINGER = '/.well-known/webfinger'; | ||
const REL = 'http://openid.net/specs/connect/1.0/issuer'; | ||
@@ -14,2 +16,3 @@ const ISSUER_METADATA = [ | ||
'claims_supported', | ||
'claim_types_supported', | ||
'end_session_endpoint', | ||
@@ -132,2 +135,4 @@ 'grant_types_supported', | ||
module.exports.USER_AGENT = USER_AGENT; | ||
module.exports.WELL_KNOWN = WELL_KNOWN; | ||
module.exports.DISCOVERY = DISCOVERY; | ||
module.exports.REL = REL; | ||
module.exports.WEBFINGER = WEBFINGER; |
'use strict'; | ||
const jose = require('node-jose'); | ||
const assert = require('assert'); | ||
const util = require('util'); | ||
const url = require('url'); | ||
const _ = require('lodash'); | ||
@@ -12,3 +14,5 @@ const LRU = require('lru-cache'); | ||
const ISSUER_METADATA = require('./consts').ISSUER_METADATA; | ||
const WELL_KNOWN = require('./consts').WELL_KNOWN; | ||
const DISCOVERY = require('./consts').DISCOVERY; | ||
const WEBFINGER = require('./consts').WEBFINGER; | ||
const REL = require('./consts').REL; | ||
@@ -18,2 +22,3 @@ const gotErrorHandler = require('./got_error_handler'); | ||
const registry = require('./issuer_registry'); | ||
const webfingerNormalize = require('./webfinger_normalize'); | ||
@@ -61,6 +66,2 @@ const privateProps = new WeakMap(); | ||
static get registry() { | ||
return registry; | ||
} | ||
inspect() { | ||
@@ -96,6 +97,8 @@ return util.format('Issuer <%s>', this.issuer); | ||
return this.keystore(!freshJwksUri) | ||
.then(store => store.get(def)) | ||
.then((key) => { | ||
.then(store => store.all(def)) | ||
.then((keys) => { | ||
assert(keys.length, 'no valid key found'); | ||
assert.equal(keys.length, 1, 'multiple matching keys, kid must be provided'); | ||
lookupCache.set(def, true); | ||
return key; | ||
return keys[0]; | ||
}); | ||
@@ -108,6 +111,32 @@ } | ||
static webfinger(input) { | ||
const resource = webfingerNormalize(input); | ||
const host = url.parse(resource).host; | ||
const query = { resource, rel: REL }; | ||
const opts = { query, followRedirect: true }; | ||
return got.get(`https://${host}${WEBFINGER}`, this.httpOptions(opts)) | ||
.then(response => JSON.parse(response.body)) | ||
.then((body) => { | ||
const foo = _.find(body.links, link => typeof link === 'object' && link.rel === REL && link.href); | ||
assert(foo, 'no issuer found in webfinger'); | ||
const expectedIssuer = foo.href; | ||
if (registry.has(expectedIssuer)) return registry.get(expectedIssuer); | ||
return this.discover(expectedIssuer).then((issuer) => { | ||
try { | ||
assert.equal(issuer.issuer, expectedIssuer, 'discovered issuer mismatch'); | ||
} catch (err) { | ||
registry.delete(issuer.issuer); | ||
throw err; | ||
} | ||
return issuer; | ||
}); | ||
}); | ||
} | ||
static discover(uri) { | ||
uri = stripTrailingSlash(uri); // eslint-disable-line no-param-reassign | ||
const isWellKnown = uri.endsWith(WELL_KNOWN); | ||
const wellKnownUri = isWellKnown ? uri : `${uri}${WELL_KNOWN}`; | ||
const isWellKnown = uri.endsWith(DISCOVERY); | ||
const wellKnownUri = isWellKnown ? uri : `${uri}${DISCOVERY}`; | ||
@@ -114,0 +143,0 @@ return got.get(wellKnownUri, this.httpOptions()) |
{ | ||
"name": "openid-client", | ||
"version": "0.6.1", | ||
"version": "0.7.0", | ||
"description": "OpenID Connect Relying Party (RP, Client) implementation for Node.js", | ||
@@ -36,4 +36,4 @@ "main": "lib/index.js", | ||
"eslint": "^3.0.0", | ||
"eslint-config-airbnb-base": "^8.0.0", | ||
"eslint-plugin-import": "^1.0.0", | ||
"eslint-config-airbnb-base": "^9.0.0", | ||
"eslint-plugin-import": "^2.0.1", | ||
"istanbul": "^0.4.4", | ||
@@ -46,5 +46,6 @@ "koa": "^1.2.0", | ||
"mocha": "^3.0.0", | ||
"nock": "^8.0.0", | ||
"nock": "^9.0.0", | ||
"readable-mock-req": "^0.2.2", | ||
"sinon": "^1.17.4", | ||
"timekeeper": "^0.1.1" | ||
"timekeeper": "^1.0.0" | ||
}, | ||
@@ -51,0 +52,0 @@ "dependencies": { |
@@ -37,2 +37,3 @@ # openid-client | ||
- Discovery of OpenID Provider (Issuer) Metadata | ||
- Discovery of OpenID Provider (Issuer) Metadata via user provided inputs (see #WebFinger) | ||
- [OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 1][feature-registration] | ||
@@ -139,2 +140,26 @@ - Dynamic Client Registration request | ||
### Handling multiple response modes | ||
When handling multiple response modes with one single pass you can use `#authorizationParams` | ||
to get the params object from the koa/express/node request object or a url string. | ||
(http.IncomingMessage). If form_post is your response_type you need to include a body parser prior. | ||
```js | ||
client.authorizationParams('https://client.example.com/cb?code=code'); // => { code: 'code' }; | ||
client.authorizationParams('/cb?code=code'); // => { code: 'code' }; | ||
// koa v1.x w/ koa-body | ||
app.use(bodyParser({ patchNode: true })); | ||
app.use(function* (next) { | ||
const params = client.authorizationParams(this.request.req); // => parsed url query, url fragment or body object | ||
// ... | ||
}); | ||
// express w/ bodyParser | ||
app.use(bodyParser.urlencoded({ extended: false })); | ||
app.use(function (req, res, next) { | ||
const params = client.authorizationParams(req); // => parsed url query, url fragment or body object | ||
// ... | ||
}); | ||
``` | ||
### Refreshing a token | ||
@@ -265,6 +290,19 @@ ```js | ||
## WebFinger discovery | ||
```js | ||
Issuer.webfinger(userInput) // => Promise | ||
.then(function (issuer) { | ||
console.log('Discovered issuer %s', issuer); | ||
}); | ||
``` | ||
Accepts, normalizes, discovers and validates the discovery of User Input using E-Mail, URL, acct, | ||
Hostname and Port syntaxes as described in [Discovery 1.0][feature-discovery]. | ||
Uses already discovered (cached) issuers where applicable. | ||
## Configuration | ||
### Changing HTTP request defaults | ||
Setting `defaultHttpOptions` on `Issuer` always merges your passed options with the default. openid-client uses [got][got-library] for http requests with the following default request options | ||
Setting `defaultHttpOptions` on `Issuer` always merges your passed options with the default. | ||
openid-client uses [got][got-library] for http requests with the following default request options | ||
@@ -271,0 +309,0 @@ ```js |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
49702
15
926
345
16
1