Skip to content

Commit

Permalink
feat: support additional authorized parties
Browse files Browse the repository at this point in the history
resolves #231
  • Loading branch information
svvac authored Feb 28, 2020
1 parent dd2194e commit c9268ce
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 21 deletions.
16 changes: 12 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Performs [OpenID Provider Issuer Discovery][webfinger-discovery] based on End-Us

<!-- TOC Client START -->
- [Class: &lt;Client&gt;](#class-client)
- [new Client(metadata[, jwks])](#new-clientmetadata-jwks)
- [new Client(metadata[, jwks[, options]])](#new-clientmetadata-jwks-options)
- [client.metadata](#clientmetadata)
- [client.authorizationUrl(parameters)](#clientauthorizationurlparameters)
- [client.endSessionUrl(parameters)](#clientendsessionurlparameters)
Expand All @@ -157,7 +157,7 @@ Performs [OpenID Provider Issuer Discovery][webfinger-discovery] based on End-Us
- [client.deviceAuthorization(parameters[, extras])](#clientdeviceauthorizationparameters-extras)
- [Client Authentication Methods](#client-authentication-methods)
- [Client.register(metadata[, other])](#clientregistermetadata-other)
- [Client.fromUri(registrationClientUri, registrationAccessToken[, jwks])](#clientfromuriregistrationclienturi-registrationaccesstoken-jwks)
- [Client.fromUri(registrationClientUri, registrationAccessToken[, jwks[, clientOptions]])](#clientfromuriregistrationclienturi-registrationaccesstoken-jwks-clientoptions)
<!-- TOC Client END -->

---
Expand All @@ -179,7 +179,7 @@ const { Issuer } = require('openid-client');

---

#### `new Client(metadata[, jwks])`
#### `new Client(metadata[, jwks[, options]])`

Creates a new Client with the provided metadata

Expand Down Expand Up @@ -210,6 +210,9 @@ Creates a new Client with the provided metadata
- other metadata may be present but currently doesn't have any special handling
- `jwks`: `<Object>` JWK Set formatted object with private keys used for signing client assertions
or decrypting responses.
- `options`: `<Object>` additional options for the client
- `additionalAuthorizedParties`: `<string>` &vert; `string[]` additional accepted values for the
Authorized Party (`azp`) claim. **Default:** only the client's client_id value is accepted.
- Returns: `<Client>`

---
Expand Down Expand Up @@ -487,10 +490,12 @@ Performs Dynamic Client Registration with the provided metadata at the issuer's
public parts will be registered as `jwks`.
- `initialAccessToken`: `<string>` Initial Access Token to use as a Bearer token during the
registration call.
- `additionalAuthorizedParties`: `<string>` &vert; `string[]` additional accepted values for the
Authorized Party (`azp`) claim. **Default:** only the client's client_id value is accepted.

---

#### `Client.fromUri(registrationClientUri, registrationAccessToken[, jwks])`
#### `Client.fromUri(registrationClientUri, registrationAccessToken[, jwks[, clientOptions]])`

Performs Dynamic Client Read Request to retrieve a Client instance.

Expand All @@ -499,6 +504,9 @@ Performs Dynamic Client Read Request to retrieve a Client instance.
the Client Read Request
- `jwks`: `<Object>` JWK Set formatted object with private keys used for signing client assertions
or decrypting responses.
- `clientOptions`: `<Object>` additional options passed to the `Client` constructor
- `additionalAuthorizedParties`: `<string>` &vert; `string[]` additional accepted values for the
Authorized Party (`azp`) claim. **Default:** only the client's client_id value is accepted.

---

Expand Down
44 changes: 31 additions & 13 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
* @name constructor
* @api public
*/
constructor(metadata = {}, jwks) {
constructor(metadata = {}, jwks, options) {
super();

if (typeof metadata.client_id !== 'string' || !metadata.client_id) {
Expand Down Expand Up @@ -230,6 +230,10 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
instance(this).set('keystore', keystore);
}

if (options !== undefined) {
instance(this).set('options', options);
}

this[CLOCK_TOLERANCE] = 0;
}

Expand Down Expand Up @@ -903,11 +907,23 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
}
}

if (payload.azp !== undefined && payload.azp !== this.client_id) {
throw new RPError({
printf: ['azp must be the client_id, expected %s, got: %s', this.client_id, payload.azp],
jwt,
});
if (payload.azp !== undefined) {
let { additionalAuthorizedParties } = instance(this).get('options') || {};

if (typeof additionalAuthorizedParties === 'string') {
additionalAuthorizedParties = [this.client_id, additionalAuthorizedParties];
} else if (Array.isArray(additionalAuthorizedParties)) {
additionalAuthorizedParties = [this.client_id, ...additionalAuthorizedParties];
} else {
additionalAuthorizedParties = [this.client_id];
}

if (!additionalAuthorizedParties.includes(payload.azp)) {
throw new RPError({
printf: ['azp mismatch, got: %s', payload.azp],
jwt,
});
}
}

let key;
Expand Down Expand Up @@ -1389,26 +1405,28 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
* @name register
* @api public
*/
static async register(properties, { initialAccessToken, jwks } = {}) {
static async register(metadata, options = {}) {
const { initialAccessToken, jwks, ...clientOptions } = options;

assertIssuerConfiguration(this.issuer, 'registration_endpoint');

if (jwks !== undefined && !(properties.jwks || properties.jwks_uri)) {
if (jwks !== undefined && !(metadata.jwks || metadata.jwks_uri)) {
const keystore = getKeystore.call(this, jwks);
properties.jwks = keystore.toJWKS(false);
metadata.jwks = keystore.toJWKS(false);
}

const response = await request.call(this, {
headers: initialAccessToken ? {
Authorization: authorizationHeaderValue(initialAccessToken),
} : undefined,
json: true,
body: properties,
body: metadata,
url: this.issuer.registration_endpoint,
method: 'POST',
});
const responseBody = processResponse(response, { statusCode: 201, bearer: true });

return new this(responseBody, jwks);
return new this(responseBody, jwks, clientOptions);
}

/**
Expand All @@ -1427,7 +1445,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
* @name fromUri
* @api public
*/
static async fromUri(registrationClientUri, registrationAccessToken, jwks) {
static async fromUri(registrationClientUri, registrationAccessToken, jwks, clientOptions) {
const response = await request.call(this, {
method: 'GET',
url: registrationClientUri,
Expand All @@ -1436,7 +1454,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
});
const responseBody = processResponse(response, { bearer: true });

return new this(responseBody, jwks);
return new this(responseBody, jwks, clientOptions);
}

/**
Expand Down
100 changes: 99 additions & 1 deletion test/client/client_instance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,14 @@ describe('Client', () => {
client_id: 'identifier',
client_secret: 'its gotta be a long secret and i mean at least 32 characters',
});
this.clientWith3rdParty = new this.issuer.Client({
client_id: 'identifier',
client_secret: 'its gotta be a long secret and i mean at least 32 characters',
}, undefined, { additionalAuthorizedParties: 'authorized third party' });
this.clientWith3rdParties = new this.issuer.Client({
client_id: 'identifier',
client_secret: 'its gotta be a long secret and i mean at least 32 characters',
}, undefined, { additionalAuthorizedParties: ['authorized third party', 'another third party'] });

this.fapiClient = new this.issuer.FAPIClient({
client_id: 'identifier',
Expand Down Expand Up @@ -1980,7 +1988,7 @@ describe('Client', () => {
return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.client.validateIdToken(token))
.then(fail, (error) => {
expect(error).to.have.property('message', 'azp must be the client_id, expected identifier, got: not the client');
expect(error).to.have.property('message', 'azp mismatch, got: not the client');
});
});

Expand Down Expand Up @@ -2014,6 +2022,96 @@ describe('Client', () => {
.then((token) => this.client.validateIdToken(token));
});

it('rejects unknown additional party azp values (single additional value)', function () {
const payload = {
iss: this.issuer.issuer,
sub: 'userId',
aud: [this.client.client_id, 'someone else'],
azp: 'some unknown third party',
exp: now() + 3600,
iat: now(),
};

return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.clientWith3rdParty.validateIdToken(token))
.then(fail, (error) => {
expect(error).to.have.property('message', 'azp mismatch, got: some unknown third party');
});
});

it('allows configured additional party azp value (single additional value)', function () {
const payload = {
iss: this.issuer.issuer,
sub: 'userId',
aud: [this.client.client_id, 'someone else'],
azp: 'authorized third party',
exp: now() + 3600,
iat: now(),
};

return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.clientWith3rdParty.validateIdToken(token));
});

it('allows the default (client_id) additional party azp value (single additional value)', function () {
const payload = {
iss: this.issuer.issuer,
sub: 'userId',
aud: [this.client.client_id, 'someone else'],
azp: this.client.client_id,
exp: now() + 3600,
iat: now(),
};

return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.clientWith3rdParty.validateIdToken(token));
});

it('rejects unknown additional party azp values (multiple additional values)', function () {
const payload = {
iss: this.issuer.issuer,
sub: 'userId',
aud: [this.client.client_id, 'someone else'],
azp: 'some unknown third party',
exp: now() + 3600,
iat: now(),
};

return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.clientWith3rdParties.validateIdToken(token))
.then(fail, (error) => {
expect(error).to.have.property('message', 'azp mismatch, got: some unknown third party');
});
});

it('allows configured additional party azp value (multiple additional values)', function () {
const payload = {
iss: this.issuer.issuer,
sub: 'userId',
aud: [this.client.client_id, 'someone else'],
azp: 'authorized third party',
exp: now() + 3600,
iat: now(),
};

return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.clientWith3rdParties.validateIdToken(token));
});

it('allows the default (client_id) additional party azp value (multiple additional values)', function () {
const payload = {
iss: this.issuer.issuer,
sub: 'userId',
aud: [this.client.client_id, 'someone else'],
azp: this.client.client_id,
exp: now() + 3600,
iat: now(),
};

return this.IdToken(this.keystore.get(), 'RS256', payload)
.then((token) => this.clientWith3rdParties.validateIdToken(token));
});

it('verifies the audience when string', function () {
const payload = {
iss: this.issuer.issuer,
Expand Down
10 changes: 7 additions & 3 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,17 @@ export interface IntrospectionResponse {
[key: string]: unknown;
}

export interface ClientOptions {
additionalAuthorizedParties?: string | string[];
}

/**
* Encapsulates a dynamically registered, discovered or instantiated OpenID Connect Client (Client),
* Relying Party (RP), and its metadata, its instances hold the methods for getting an authorization URL,
* consuming callbacks, triggering token endpoint grants, revoking and introspecting tokens.
*/
export class Client {
constructor(metadata: ClientMetadata, jwks?: JSONWebKeySet);
constructor(metadata: ClientMetadata, jwks?: JSONWebKeySet, options?: ClientOptions);
[custom.http_options]: CustomHttpOptionsProvider;
[custom.clock_tolerance]: number;
metadata: ClientMetadata;
Expand Down Expand Up @@ -453,8 +457,8 @@ export class Client {
* for subsequent Device Access Token Request polling.
*/
deviceAuthorization(parameters?: DeviceAuthorizationParameters, extras?: DeviceAuthorizationExtras): Promise<DeviceFlowHandle<Client>>;
static register(metadata: object, other?: RegisterOther): Promise<Client>;
static fromUri(registrationClientUri: string, registrationAccessToken: string, jwks?: JSONWebKeySet): Promise<Client>;
static register(metadata: object, other?: RegisterOther & ClientOptions): Promise<Client>;
static fromUri(registrationClientUri: string, registrationAccessToken: string, jwks?: JSONWebKeySet, clientOptions?: ClientOptions): Promise<Client>;
static [custom.http_options]: CustomHttpOptionsProvider;

[key: string]: unknown;
Expand Down

0 comments on commit c9268ce

Please sign in to comment.