Skip to content

Commit f3550da

Browse files
committed
feat: experimental support for OIDC RP Metadata Choices
1 parent 420e760 commit f3550da

File tree

7 files changed

+247
-39
lines changed

7 files changed

+247
-39
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The following draft specifications are implemented by oidc-provider:
5151

5252
- [Financial-grade API: Client Initiated Backchannel Authentication Profile (`FAPI-CIBA`) - Implementer's Draft 01][fapi-ciba]
5353
- [FAPI 2.0 Message Signing (`FAPI 2.0`) - Implementer's Draft 01][fapi2ms-id1]
54+
- [OIDC Relying Party Metadata Choices 1.0 - draft 01][rp-metadata-choices]
5455

5556
Updates to draft specification versions are released as MINOR library versions,
5657
if you utilize these specification implementations consider using the tilde `~` operator in your
@@ -165,3 +166,4 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a
165166
[fapi2sp]: https://openid.net/specs/fapi-security-profile-2_0-final.html
166167
[fapi2ms-id1]: https://openid.net/specs/fapi-2_0-message-signing-ID1.html
167168
[Security Policy]: https://github.com/panva/node-oidc-provider/security/policy
169+
[rp-metadata-choices]: https://openid.net/specs/openid-connect-rp-metadata-choices-1_0-02.html

docs/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ location / {
457457
- Experimental features:
458458
- [externalSigningSupport (e.g. KMS)](#featuresexternalsigningsupport)
459459
- [richAuthorizationRequests](#featuresrichauthorizationrequests)
460+
- [rpMetadataChoices](#featuresrpmetadatachoices)
460461
- [webMessageResponseMode](#featureswebmessageresponsemode)
461462
- [acrValues](#acrvalues)
462463
- [allowOmittingSingleRegisteredRedirectUri](#allowomittingsingleregisteredredirecturi)
@@ -2081,6 +2082,43 @@ async function postLogoutSuccessSource(ctx) {
20812082
20822083
</details>
20832084
2085+
### features.rpMetadataChoices
2086+
2087+
[`OIDC Relying Party Metadata Choices 1.0 - draft 02`](https://openid.net/specs/openid-connect-rp-metadata-choices-1_0-02.html)
2088+
2089+
> [!NOTE]
2090+
> This is an experimental feature.
2091+
2092+
Enables the use of the following multi-valued input parameters metadata from the Relying Party Metadata Choices draft assuming their underlying feature is also enabled:
2093+
- subject_types_supported
2094+
- id_token_signing_alg_values_supported
2095+
- id_token_encryption_alg_values_supported
2096+
- id_token_encryption_enc_values_supported
2097+
- userinfo_signing_alg_values_supported
2098+
- userinfo_encryption_alg_values_supported
2099+
- userinfo_encryption_enc_values_supported
2100+
- request_object_signing_alg_values_supported
2101+
- request_object_encryption_alg_values_supported
2102+
- request_object_encryption_enc_values_supported
2103+
- token_endpoint_auth_methods_supported
2104+
- token_endpoint_auth_signing_alg_values_supported
2105+
- introspection_signing_alg_values_supported
2106+
- introspection_encryption_alg_values_supported
2107+
- introspection_encryption_enc_values_supported
2108+
- authorization_signing_alg_values_supported
2109+
- authorization_encryption_alg_values_supported
2110+
- authorization_encryption_enc_values_supported
2111+
- backchannel_authentication_request_signing_alg_values_supported
2112+
2113+
2114+
_**default value**_:
2115+
```js
2116+
{
2117+
ack: undefined,
2118+
enabled: false
2119+
}
2120+
```
2121+
20842122
### features.userinfo
20852123
20862124
[`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#UserInfo) - UserInfo Endpoint

lib/consts/client_attributes.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,28 @@ const SYNTAX = {
172172
client_secret: noVSCHAR,
173173
};
174174

175+
const CHOICES = {
176+
authorization_encrypted_response_alg: 'authorization_encryption_alg_values_supported',
177+
authorization_encrypted_response_enc: 'authorization_encryption_enc_values_supported',
178+
authorization_signed_response_alg: 'authorization_signing_alg_values_supported',
179+
backchannel_authentication_request_signing_alg: 'backchannel_authentication_request_signing_alg_values_supported',
180+
id_token_encrypted_response_alg: 'id_token_encryption_alg_values_supported',
181+
id_token_encrypted_response_enc: 'id_token_encryption_enc_values_supported',
182+
id_token_signed_response_alg: 'id_token_signing_alg_values_supported',
183+
introspection_encrypted_response_alg: 'introspection_encryption_alg_values_supported',
184+
introspection_encrypted_response_enc: 'introspection_encryption_enc_values_supported',
185+
introspection_signed_response_alg: 'introspection_signing_alg_values_supported',
186+
request_object_encryption_alg: 'request_object_encryption_alg_values_supported',
187+
request_object_encryption_enc: 'request_object_encryption_enc_values_supported',
188+
request_object_signing_alg: 'request_object_signing_alg_values_supported',
189+
subject_type: 'subject_types_supported',
190+
token_endpoint_auth_method: 'token_endpoint_auth_methods_supported',
191+
token_endpoint_auth_signing_alg: 'token_endpoint_auth_signing_alg_values_supported',
192+
userinfo_encrypted_response_alg: 'userinfo_encryption_alg_values_supported',
193+
userinfo_encrypted_response_enc: 'userinfo_encryption_enc_values_supported',
194+
userinfo_signed_response_alg: 'userinfo_signing_alg_values_supported',
195+
};
196+
175197
export {
176198
ARYS,
177199
BOOL,
@@ -185,4 +207,5 @@ export {
185207
SYNTAX,
186208
WEB_URI,
187209
WHEN,
210+
CHOICES,
188211
};

lib/helpers/client_schema.js

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ export default function getSchema(provider) {
151151
RECOGNIZED_METADATA.push('authorization_details_types');
152152
}
153153

154+
let CHOICES = {};
155+
156+
if (features.rpMetadataChoices.enabled) {
157+
CHOICES = Object.fromEntries(Object.entries(CLIENT_ATTRIBUTES.CHOICES)
158+
.filter(([key]) => RECOGNIZED_METADATA.includes(key)));
159+
RECOGNIZED_METADATA.push(...Object.values(CHOICES));
160+
}
161+
154162
instance(provider).RECOGNIZED_METADATA = RECOGNIZED_METADATA;
155163

156164
const ENUM = {
@@ -208,18 +216,32 @@ export default function getSchema(provider) {
208216
ctx,
209217
processCustomMetadata = !!configuration.extraClientMetadata.properties.length,
210218
) {
219+
this.#initialize(metadata);
220+
221+
if (processCustomMetadata) {
222+
this.processCustomMetadata(ctx);
223+
this.#initialize(this);
224+
}
225+
226+
this.ensureStripUnrecognized();
227+
this.ensureStripChoices();
228+
}
229+
230+
#initialize(metadata) {
211231
Object.assign(
212232
this,
213233
omitBy(
214234
pick(DEFAULTS, ...RECOGNIZED_METADATA),
215-
isUndefined,
235+
(value, key) => isUndefined(value)
236+
|| (key in CHOICES && metadata[CHOICES[key]] !== undefined),
216237
),
217238
omitBy(
218239
pick(metadata, ...RECOGNIZED_METADATA, ...configuration.extraClientMetadata.properties),
219240
isUndefined,
220241
),
221242
);
222243

244+
this.choices();
223245
this.required();
224246
this.booleans();
225247
this.whens();
@@ -311,11 +333,11 @@ export default function getSchema(provider) {
311333
this.invalidate('only one tls_client_auth certificate subject value must be provided');
312334
}
313335
} else {
314-
delete this.tls_client_auth_san_dns;
315-
delete this.tls_client_auth_san_email;
316-
delete this.tls_client_auth_san_ip;
317-
delete this.tls_client_auth_san_uri;
318-
delete this.tls_client_auth_subject_dn;
336+
this.#unset('tls_client_auth_san_dns');
337+
this.#unset('tls_client_auth_san_email');
338+
this.#unset('tls_client_auth_san_ip');
339+
this.#unset('tls_client_auth_san_uri');
340+
this.#unset('tls_client_auth_subject_dn');
319341
}
320342
}
321343

@@ -325,16 +347,50 @@ export default function getSchema(provider) {
325347
if (this.jwks !== undefined && this.jwks_uri !== undefined) {
326348
this.invalidate('jwks and jwks_uri must not be used at the same time');
327349
}
350+
}
328351

329-
if (processCustomMetadata) {
330-
this.processCustomMetadata(ctx);
331-
}
352+
choices() {
353+
for (const [target, choice] of Object.entries(CHOICES)) {
354+
if (this[choice] !== undefined) {
355+
if (!Array.isArray(this[choice])) {
356+
this.invalidate(`${choice} must be an array`);
357+
}
358+
const choices = new Set(this[choice]);
332359

333-
this.ensureStripUnrecognized();
360+
if (this[target] !== undefined && !choices.has(this[target])) {
361+
this.invalidate(`${choice} must include the value of provided ${target}`);
362+
}
334363

335-
if (processCustomMetadata) {
336-
// eslint-disable-next-line no-constructor-return
337-
return new Schema(this, ctx, false);
364+
const only = ENUM[target](this);
365+
366+
// test the options in the following order:
367+
// - explicit value (if provided)
368+
// - ...rest
369+
const options = new Set();
370+
if (this[target]) {
371+
options.add(this[target]);
372+
}
373+
for (const value of choices) {
374+
if (typeof value !== 'string' || !value.length) {
375+
this.invalidate(`${choice} must only contain strings`);
376+
}
377+
options.add(value);
378+
}
379+
380+
for (const option of options) {
381+
try {
382+
this[target] = option;
383+
this.#enum(target, only);
384+
break;
385+
} catch {
386+
this.#unset(target);
387+
}
388+
}
389+
390+
if (!this[target]) {
391+
this.invalidate(`${choice} includes no supported values`);
392+
}
393+
}
338394
}
339395
}
340396

@@ -478,34 +534,38 @@ export default function getSchema(provider) {
478534
const only = fn(this);
479535

480536
if (this[prop] !== undefined) {
481-
const isAry = ARYS.includes(prop);
482-
let length;
483-
let method;
484-
if (only instanceof Set) {
485-
({ size: length } = only);
486-
method = 'has';
487-
} else {
488-
({ length } = only);
489-
method = 'includes';
490-
}
491-
492-
if (isAry && !this[prop].every((val) => only[method](val))) {
493-
if (length) {
494-
this.invalidate(`${prop} can only contain ${formatters.formatList([...only], { type: 'disjunction' })}`);
495-
} else {
496-
this.invalidate(`${prop} must be empty (no values are allowed)`);
497-
}
498-
} else if (!isAry && !only[method](this[prop])) {
499-
if (length) {
500-
this.invalidate(`${prop} must be ${formatters.formatList([...only], { type: 'disjunction' })}`);
501-
} else {
502-
this.invalidate(`${prop} must not be provided (no values are allowed)`);
503-
}
504-
}
537+
this.#enum(prop, only);
505538
}
506539
});
507540
}
508541

542+
#enum(prop, only) {
543+
const isAry = ARYS.includes(prop);
544+
let length;
545+
let method;
546+
if (only instanceof Set) {
547+
({ size: length } = only);
548+
method = 'has';
549+
} else {
550+
({ length } = only);
551+
method = 'includes';
552+
}
553+
554+
if (isAry && !this[prop].every((val) => only[method](val))) {
555+
if (length) {
556+
this.invalidate(`${prop} can only contain ${formatters.formatList([...only], { type: 'disjunction' })}`);
557+
} else {
558+
this.invalidate(`${prop} must be empty (no values are allowed)`);
559+
}
560+
} else if (!isAry && !only[method](this[prop])) {
561+
if (length) {
562+
this.invalidate(`${prop} must be ${formatters.formatList([...only], { type: 'disjunction' })}`);
563+
} else {
564+
this.invalidate(`${prop} must not be provided (no values are allowed)`);
565+
}
566+
}
567+
}
568+
509569
normalizeResponseTypes() {
510570
this.response_types = this.response_types.map((type) => [...new Set(type.split(' '))].sort().join(' '));
511571
}
@@ -602,11 +662,19 @@ export default function getSchema(provider) {
602662
const allowed = [...RECOGNIZED_METADATA, ...configuration.extraClientMetadata.properties];
603663
Object.keys(this).forEach((prop) => {
604664
if (!allowed.includes(prop)) {
605-
delete this[prop];
665+
this.#unset(prop);
606666
}
607667
});
608668
}
609669

670+
ensureStripChoices() {
671+
Object.values(CHOICES).forEach(this.#unset, this);
672+
}
673+
674+
#unset(prop) {
675+
delete this[prop];
676+
}
677+
610678
scopes() {
611679
if (this.scope) {
612680
const parsed = new Set(this.scope.split(' '));

lib/helpers/defaults.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,35 @@ function makeDefaults() {
18871887
assertJwtClaimsAndHeader,
18881888
},
18891889

1890+
/*
1891+
* features.rpMetadataChoices
1892+
*
1893+
* title: [`OIDC Relying Party Metadata Choices 1.0 - draft 02`](https://openid.net/specs/openid-connect-rp-metadata-choices-1_0-02.html)
1894+
*
1895+
* description: Enables the use of the following multi-valued input parameters metadata from the Relying Party Metadata Choices draft assuming their underlying feature is also enabled:
1896+
*
1897+
* - subject_types_supported
1898+
* - id_token_signing_alg_values_supported
1899+
* - id_token_encryption_alg_values_supported
1900+
* - id_token_encryption_enc_values_supported
1901+
* - userinfo_signing_alg_values_supported
1902+
* - userinfo_encryption_alg_values_supported
1903+
* - userinfo_encryption_enc_values_supported
1904+
* - request_object_signing_alg_values_supported
1905+
* - request_object_encryption_alg_values_supported
1906+
* - request_object_encryption_enc_values_supported
1907+
* - token_endpoint_auth_methods_supported
1908+
* - token_endpoint_auth_signing_alg_values_supported
1909+
* - introspection_signing_alg_values_supported
1910+
* - introspection_encryption_alg_values_supported
1911+
* - introspection_encryption_enc_values_supported
1912+
* - authorization_signing_alg_values_supported
1913+
* - authorization_encryption_alg_values_supported
1914+
* - authorization_encryption_enc_values_supported
1915+
* - backchannel_authentication_request_signing_alg_values_supported
1916+
*/
1917+
rpMetadataChoices: { enabled: false, ack: undefined },
1918+
18901919
/*
18911920
* features.revocation
18921921
*

lib/helpers/features.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ export const EXPERIMENTS = new Map(Object.entries({
3636
name: 'External Signing Key Support',
3737
version: ['experimental-01'],
3838
},
39+
rpMetadataChoices: {
40+
name: 'OpenID Connect Relying Party Metadata Choices',
41+
version: 'draft-02',
42+
},
3943
}));

0 commit comments

Comments
 (0)