From ab88aa590fb5a853ddbd8273a713bf142a9f5049 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 6 Dec 2019 14:21:52 +0100 Subject: [PATCH] feat: added issuer.FAPIClient for FAPI RW integrations --- docs/README.md | 11 +++++++ lib/client.js | 20 ++++++++++++ lib/issuer.js | 6 ++++ test/client/client_instance.test.js | 47 +++++++++++++++++++++++++++++ types/index.d.ts | 5 +++ 5 files changed, 89 insertions(+) diff --git a/docs/README.md b/docs/README.md index dcd66b1a..fcfd050f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,7 @@ If you or your business use openid-client, please consider becoming a [sponsor][ - [Class: <Issuer>](#class-issuer) - [new Issuer(metadata)](#new-issuermetadata) - [issuer.Client](#issuerclient) + - [issuer.FAPIClient](#issuerfapiclient) - [issuer.metadata](#issuermetadata) - [issuer.keystore([forceReload])](#issuerkeystoreforcereload) - [Issuer.discover(issuer)](#issuerdiscoverissuer) @@ -87,6 +88,16 @@ Returns the `` class tied to this issuer. --- +#### `issuer.FAPIClient` + +Returns the `` class tied to this issuer. `` inherits from `` and +adds necessary FAPI related checks. `s_hash` presence in authorization endpoint response ID Tokens +as well as authorization endpoint `iat` not being too far in the past (fixed to be 1 hour). + +- Returns: `` + +--- + #### `issuer.metadata` Returns metadata from the issuer's discovery document. diff --git a/lib/client.js b/lib/client.js index 8fc6f608..6eb94357 100644 --- a/lib/client.js +++ b/lib/client.js @@ -807,6 +807,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base jwt: idToken, }); } + if (!payload.c_hash && tokenSet.code) { throw new RPError({ message: 'missing required property c_hash', @@ -814,6 +815,25 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base }); } + const fapi = this.constructor.name === 'FAPIClient'; + + if (fapi) { + if (payload.iat < timestamp - 3600) { + throw new RPError({ + printf: ['id_token issued too far in the past, now %i, iat %i', timestamp, payload.iat], + jwt: idToken, + }); + } + + if (!payload.s_hash && (tokenSet.state || state)) { + throw new RPError({ + message: 'missing required property s_hash', + jwt: idToken, + }); + } + } + + if (payload.s_hash) { if (!state) { throw new TypeError('cannot verify s_hash, "checks.state" property not provided'); diff --git a/lib/issuer.js b/lib/issuer.js index de74f545..fce06ff7 100644 --- a/lib/issuer.js +++ b/lib/issuer.js @@ -1,3 +1,5 @@ +/* eslint-disable max-classes-per-file */ + const { inspect } = require('util'); const url = require('url'); @@ -63,6 +65,10 @@ class Issuer { Object.defineProperty(this, 'Client', { value: getClient(this, aadIssValidation), }); + + Object.defineProperty(this, 'FAPIClient', { + value: class FAPIClient extends this.Client {}, + }); } /** diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index dd11f69b..555b5f34 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -1589,6 +1589,11 @@ describe('Client', () => { client_secret: 'its gotta be a long secret and i mean at least 32 characters', }); + this.fapiClient = new this.issuer.FAPIClient({ + client_id: 'identifier', + client_secret: 'secure', + }); + this.IdToken = async (key, alg, payload) => { return jose.JWS.sign(payload, key, { alg, @@ -2193,6 +2198,48 @@ describe('Client', () => { }); }); + it('FAPIClient validates s_hash presence', function () { + const code = 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y'; // eslint-disable-line camelcase, max-len + const c_hash = '77QmUPtjPfzWtF2AnpK9RQ'; // eslint-disable-line camelcase + + return this.IdToken(this.keystore.get(), 'RS256', { + c_hash, + iss: this.issuer.issuer, + sub: 'userId', + aud: this.fapiClient.client_id, + exp: now() + 3600, + iat: now(), + }) + .then((token) => { + // const tokenset = new TokenSet(); + return this.fapiClient.callback(null, { code, id_token: token, state: 'foo' }, { state: 'foo' }); + }) + .then(fail, (error) => { + expect(error).to.have.property('message', 'missing required property s_hash'); + }); + }); + + it('FAPIClient checks iat is fresh', function () { + const code = 'jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y'; // eslint-disable-line camelcase, max-len + const c_hash = '77QmUPtjPfzWtF2AnpK9RQ'; // eslint-disable-line camelcase + + return this.IdToken(this.keystore.get(), 'RS256', { + c_hash, + iss: this.issuer.issuer, + sub: 'userId', + aud: this.fapiClient.client_id, + exp: now() + 3600, + iat: now() - 3601, + }) + .then((token) => { + // const tokenset = new TokenSet(); + return this.fapiClient.callback(null, { code, id_token: token, state: 'foo' }, { state: 'foo' }); + }) + .then(fail, (error) => { + expect(error).to.have.property('message').matches(/^id_token issued too far in the past, now \d+, iat \d+/); + }); + }); + it('validates state presence when s_hash is returned', function () { const s_hash = '77QmUPtjPfzWtF2AnpK9RQ'; // eslint-disable-line camelcase diff --git a/types/index.d.ts b/types/index.d.ts index 81acbab9..3d60171f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -504,6 +504,11 @@ export class Issuer { // tslint:disable-line:no-unnecess */ Client: TypeOfGenericClient; + /** + * Returns the class tied to this issuer. + */ + FAPIClient: TypeOfGenericClient; + /** * Returns metadata from the issuer's discovery document. */