Skip to content

Commit

Permalink
feat: added issuer.FAPIClient for FAPI RW integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Dec 6, 2019
1 parent 269569f commit ab88aa5
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 0 deletions.
11 changes: 11 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -87,6 +88,16 @@ Returns the `<Client>` class tied to this issuer.

---

#### `issuer.FAPIClient`

Returns the `<FAPIClient>` class tied to this issuer. `<FAPIClient>` inherits from `<Client>` 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: `<FAPIClient>`

---

#### `issuer.metadata`

Returns metadata from the issuer's discovery document.
Expand Down
20 changes: 20 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -807,13 +807,33 @@ 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',
jwt: idToken,
});
}

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');
Expand Down
6 changes: 6 additions & 0 deletions lib/issuer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-classes-per-file */

const { inspect } = require('util');
const url = require('url');

Expand Down Expand Up @@ -63,6 +65,10 @@ class Issuer {
Object.defineProperty(this, 'Client', {
value: getClient(this, aadIssValidation),
});

Object.defineProperty(this, 'FAPIClient', {
value: class FAPIClient extends this.Client {},
});
}

/**
Expand Down
47 changes: 47 additions & 0 deletions test/client/client_instance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,11 @@ export class Issuer<TClient extends Client> { // tslint:disable-line:no-unnecess
*/
Client: TypeOfGenericClient<TClient>;

/**
* Returns the <FAPIClient> class tied to this issuer.
*/
FAPIClient: TypeOfGenericClient<TClient>;

/**
* Returns metadata from the issuer's discovery document.
*/
Expand Down

0 comments on commit ab88aa5

Please sign in to comment.