From ca34a9171c4ba2ff3f5887cb472d9f90a9714462 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 17 Oct 2024 10:24:42 +0200 Subject: [PATCH] feat: add a server metadata helper for checking PKCE support --- README.md | 5 +--- conformance/runner.ts | 18 +++++++----- docs/README.md | 1 + docs/interfaces/ConfigurationMethods.md | 4 +-- docs/interfaces/ServerMetadataHelpers.md | 26 +++++++++++++++++ examples/jar.ts | 6 +--- examples/jarm.ts | 6 +--- examples/oauth.ts | 6 +--- examples/oidc.ts | 6 +--- examples/par.ts | 6 +--- src/index.ts | 37 ++++++++++++++++++++++-- 11 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 docs/interfaces/ServerMetadataHelpers.md diff --git a/README.md b/README.md index a24562c3..2dbaa2ab 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,7 @@ let parameters: Record = { code_challenge_method: 'S256', } -if ( - config.serverMetadata().code_challenge_methods_supported?.includes('S256') !== - true -) { +if (!config.serverMetadata().supportsPKCE()) { /** * We cannot be sure the server supports PKCE so we're going to use state too. * Use of PKCE is backwards compatible even if the AS doesn't support it which diff --git a/conformance/runner.ts b/conformance/runner.ts index d1a4cf4d..1fee7d85 100644 --- a/conformance/runner.ts +++ b/conformance/runner.ts @@ -295,14 +295,16 @@ export const flow = (options?: MacroOptions) => { ? lib.getDPoPHandle(client, await lib.randomDPoPKeyPair(ALG)) : undefined - let code_challenge: string | undefined - let code_verifier: string | undefined - let code_challenge_method: string | undefined - - if (response_type.includes('code')) { - code_verifier = lib.randomPKCECodeVerifier() - code_challenge = await lib.calculatePKCECodeChallenge(code_verifier) - code_challenge_method = 'S256' + const code_verifier = lib.randomPKCECodeVerifier() + const code_challenge = await lib.calculatePKCECodeChallenge(code_verifier) + const code_challenge_method = 'S256' + + if ( + !client.serverMetadata().supportsPKCE() && + !response_type.includes('id_token') + ) { + options ||= {} + options.useState = true } const scope = getScope(variant) diff --git a/docs/README.md b/docs/README.md index 2393dcec..94c87121 100644 --- a/docs/README.md +++ b/docs/README.md @@ -115,6 +115,7 @@ Support from the community to continue maintaining and improving this module is - [ModifyAssertionOptions](interfaces/ModifyAssertionOptions.md) - [MTLSEndpointAliases](interfaces/MTLSEndpointAliases.md) - [PrivateKey](interfaces/PrivateKey.md) +- [ServerMetadataHelpers](interfaces/ServerMetadataHelpers.md) - [TokenEndpointResponse](interfaces/TokenEndpointResponse.md) - [TokenEndpointResponseHelpers](interfaces/TokenEndpointResponseHelpers.md) - [UserInfoAddress](interfaces/UserInfoAddress.md) diff --git a/docs/interfaces/ConfigurationMethods.md b/docs/interfaces/ConfigurationMethods.md index 01c0e329..1b58fb30 100644 --- a/docs/interfaces/ConfigurationMethods.md +++ b/docs/interfaces/ConfigurationMethods.md @@ -12,10 +12,10 @@ Public methods available on a [Configuration](../classes/Configuration.md) insta ### serverMetadata() -▸ **serverMetadata**(): [`Readonly`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype)\<[`ServerMetadata`](ServerMetadata.md)\> +▸ **serverMetadata**(): [`Readonly`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype)\<[`ServerMetadata`](ServerMetadata.md)\> & [`ServerMetadataHelpers`](ServerMetadataHelpers.md) Used to retrieve the Authorization Server Metadata #### Returns -[`Readonly`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype)\<[`ServerMetadata`](ServerMetadata.md)\> +[`Readonly`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype)\<[`ServerMetadata`](ServerMetadata.md)\> & [`ServerMetadataHelpers`](ServerMetadataHelpers.md) diff --git a/docs/interfaces/ServerMetadataHelpers.md b/docs/interfaces/ServerMetadataHelpers.md new file mode 100644 index 00000000..a337eeaa --- /dev/null +++ b/docs/interfaces/ServerMetadataHelpers.md @@ -0,0 +1,26 @@ +# Interface: ServerMetadataHelpers + +[💗 Help the project](https://github.com/sponsors/panva) + +Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva). + +*** + +## Methods + +### supportsPKCE() + +▸ **supportsPKCE**(`method`?): `boolean` + +Determines whether the Authorization Server supports a given Code Challenge +Method + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `method`? | `string` | Code Challenge Method. Default is `S256` | + +#### Returns + +`boolean` diff --git a/examples/jar.ts b/examples/jar.ts index 6219287e..2a3f17c5 100644 --- a/examples/jar.ts +++ b/examples/jar.ts @@ -43,11 +43,7 @@ let state!: string * of PKCE is backwards compatible even if the AS doesn't support it which is * why we're using it regardless. */ - if ( - config - .serverMetadata() - .code_challenge_methods_supported?.includes('S256') !== true - ) { + if (!config.serverMetadata().supportsPKCE()) { state = client.randomState() parameters.state = state } diff --git a/examples/jarm.ts b/examples/jarm.ts index 3aa8f7fa..ef8729a8 100644 --- a/examples/jarm.ts +++ b/examples/jarm.ts @@ -44,11 +44,7 @@ let state!: string * of PKCE is backwards compatible even if the AS doesn't support it which is * why we're using it regardless. */ - if ( - config - .serverMetadata() - .code_challenge_methods_supported?.includes('S256') !== true - ) { + if (!config.serverMetadata().supportsPKCE()) { state = client.randomState() parameters.state = state } diff --git a/examples/oauth.ts b/examples/oauth.ts index d87c63ef..dde3bbc0 100644 --- a/examples/oauth.ts +++ b/examples/oauth.ts @@ -42,11 +42,7 @@ let state!: string * of PKCE is backwards compatible even if the AS doesn't support it which is * why we're using it regardless. */ - if ( - config - .serverMetadata() - .code_challenge_methods_supported?.includes('S256') !== true - ) { + if (!config.serverMetadata().supportsPKCE()) { state = client.randomState() parameters.state = state } diff --git a/examples/oidc.ts b/examples/oidc.ts index 1bf04f62..58ee3efd 100644 --- a/examples/oidc.ts +++ b/examples/oidc.ts @@ -42,11 +42,7 @@ let nonce!: string * of PKCE is backwards compatible even if the AS doesn't support it which is * why we're using it regardless. */ - if ( - config - .serverMetadata() - .code_challenge_methods_supported?.includes('S256') !== true - ) { + if (!config.serverMetadata().supportsPKCE()) { nonce = client.randomNonce() parameters.nonce = nonce } diff --git a/examples/par.ts b/examples/par.ts index 58bb51d2..fec2f509 100644 --- a/examples/par.ts +++ b/examples/par.ts @@ -42,11 +42,7 @@ let state!: string * of PKCE is backwards compatible even if the AS doesn't support it which is * why we're using it regardless. */ - if ( - config - .serverMetadata() - .code_challenge_methods_supported?.includes('S256') !== true - ) { + if (!config.serverMetadata().supportsPKCE()) { state = client.randomState() parameters.state = state } diff --git a/src/index.ts b/src/index.ts index 20fffa16..80d2709c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1449,6 +1449,35 @@ async function decrypt( ) } +export interface ServerMetadataHelpers { + /** + * Determines whether the Authorization Server supports a given Code Challenge + * Method + * + * @param method Code Challenge Method. Default is `S256` + */ + supportsPKCE(method?: string): boolean +} + +function getServerHelpers(metadata: Readonly) { + return { + supportsPKCE: { + __proto__: null, + value(method = 'S256') { + return ( + metadata.code_challenge_methods_supported?.includes(method) !== true + ) + }, + }, + } +} + +function addServerHelpers( + metadata: Readonly, +): asserts metadata is typeof metadata & ServerMetadataHelpers { + Object.defineProperties(metadata, getServerHelpers(metadata)) +} + // private const kEntraId: unique symbol = Symbol() @@ -1471,7 +1500,7 @@ export interface ConfigurationMethods { /** * Used to retrieve the Authorization Server Metadata */ - serverMetadata(): Readonly + serverMetadata(): Readonly & ServerMetadataHelpers } /** @@ -1651,8 +1680,10 @@ export class Configuration /** * @ignore */ - serverMetadata(): Readonly { - return structuredClone(int(this).as) + serverMetadata(): Readonly & ServerMetadataHelpers { + const metadata = structuredClone(int(this).as) + addServerHelpers(metadata) + return metadata } /**