From 5cdd524da74fa6fca1b76e2edaf7f66d68303f43 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 8 Apr 2024 15:07:00 +0100 Subject: [PATCH] OIDC improvements in prep of OIDC-QR reciprocation (#4149) * Add `device_authorization_endpoint` field to OIDC issuer well-known metadata Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Allow `validateIdToken` to skip handling nonce when none is present Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tweak registerOidcClient to check OIDC grant_types_supported before registration Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/test-utils/oidc.ts | 1 + spec/unit/oidc/register.spec.ts | 27 ++++++++++++++++++++ src/@types/oidc-client-ts.d.ts | 24 ++++++++++++++++++ src/oidc/register.ts | 45 +++++++++++++-------------------- src/oidc/validate.ts | 11 ++++++-- 5 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 src/@types/oidc-client-ts.d.ts diff --git a/spec/test-utils/oidc.ts b/spec/test-utils/oidc.ts index 7b2adc226d7..4f9a01c2ee2 100644 --- a/spec/test-utils/oidc.ts +++ b/spec/test-utils/oidc.ts @@ -44,6 +44,7 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated token_endpoint: issuer + "token", authorization_endpoint: issuer + "auth", registration_endpoint: issuer + "registration", + device_authorization_endpoint: issuer + "device", jwks_uri: issuer + "jwks", response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"], diff --git a/spec/unit/oidc/register.spec.ts b/spec/unit/oidc/register.spec.ts index 372f7d677c4..f0a37e33366 100644 --- a/spec/unit/oidc/register.spec.ts +++ b/spec/unit/oidc/register.spec.ts @@ -90,4 +90,31 @@ describe("registerOidcClient()", () => { OidcError.DynamicRegistrationInvalid, ); }); + + it("should throw when required endpoints are unavailable", async () => { + await expect(() => + registerOidcClient( + { + ...delegatedAuthConfig, + registrationEndpoint: undefined, + }, + metadata, + ), + ).rejects.toThrow(OidcError.DynamicRegistrationNotSupported); + }); + + it("should throw when required scopes are unavailable", async () => { + await expect(() => + registerOidcClient( + { + ...delegatedAuthConfig, + metadata: { + ...delegatedAuthConfig.metadata, + grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]], + }, + }, + metadata, + ), + ).rejects.toThrow(OidcError.DynamicRegistrationNotSupported); + }); }); diff --git a/src/@types/oidc-client-ts.d.ts b/src/@types/oidc-client-ts.d.ts new file mode 100644 index 00000000000..988a680badf --- /dev/null +++ b/src/@types/oidc-client-ts.d.ts @@ -0,0 +1,24 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import "oidc-client-ts"; + +declare module "oidc-client-ts" { + interface OidcMetadata { + // Add the missing device_authorization_endpoint field to the OidcMetadata interface + device_authorization_endpoint?: string; + } +} diff --git a/src/oidc/register.ts b/src/oidc/register.ts index 6e4948f5065..0c5f0556557 100644 --- a/src/oidc/register.ts +++ b/src/oidc/register.ts @@ -50,23 +50,31 @@ interface OidcRegistrationRequestBody { } /** - * Make the client registration request - * @param registrationEndpoint - URL as returned from issuer ./well-known/openid-configuration - * @param clientMetadata - registration metadata - * @returns resolves to the registered client id when registration is successful - * @throws An `Error` with `message` set to an entry in {@link OidcError}, - * when the registration request fails, or the response is invalid. + * Attempts dynamic registration against the configured registration endpoint + * @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown} + * @param clientMetadata - The metadata for the client which to register + * @returns Promise resolved with registered clientId + * @throws when registration is not supported, on failed request or invalid response */ -const doRegistration = async ( - registrationEndpoint: string, +export const registerOidcClient = async ( + delegatedAuthConfig: OidcClientConfig, clientMetadata: OidcRegistrationClientMetadata, ): Promise => { + if (!delegatedAuthConfig.registrationEndpoint) { + throw new Error(OidcError.DynamicRegistrationNotSupported); + } + + const grantTypes: NonEmptyArray = ["authorization_code", "refresh_token"]; + if (grantTypes.some((scope) => !delegatedAuthConfig.metadata.grant_types_supported.includes(scope))) { + throw new Error(OidcError.DynamicRegistrationNotSupported); + } + // https://openid.net/specs/openid-connect-registration-1_0.html const metadata: OidcRegistrationRequestBody = { client_name: clientMetadata.clientName, client_uri: clientMetadata.clientUri, response_types: ["code"], - grant_types: ["authorization_code", "refresh_token"], + grant_types: grantTypes, redirect_uris: clientMetadata.redirectUris, id_token_signed_response_alg: "RS256", token_endpoint_auth_method: "none", @@ -82,7 +90,7 @@ const doRegistration = async ( }; try { - const response = await fetch(registrationEndpoint, { + const response = await fetch(delegatedAuthConfig.registrationEndpoint, { method: Method.Post, headers, body: JSON.stringify(metadata), @@ -108,20 +116,3 @@ const doRegistration = async ( } } }; - -/** - * Attempts dynamic registration against the configured registration endpoint - * @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown} - * @param clientMetadata - The metadata for the client which to register - * @returns Promise resolved with registered clientId - * @throws when registration is not supported, on failed request or invalid response - */ -export const registerOidcClient = async ( - delegatedAuthConfig: OidcClientConfig, - clientMetadata: OidcRegistrationClientMetadata, -): Promise => { - if (!delegatedAuthConfig.registrationEndpoint) { - throw new Error(OidcError.DynamicRegistrationNotSupported); - } - return doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata); -}; diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index eabbc2071cf..a49a955670b 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -83,6 +83,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer requiredStringProperty(wellKnown, "revocation_endpoint"), optionalStringProperty(wellKnown, "registration_endpoint"), optionalStringProperty(wellKnown, "account_management_uri"), + optionalStringProperty(wellKnown, "device_authorization_endpoint"), optionalStringArrayProperty(wellKnown, "account_management_actions_supported"), requiredArrayValue(wellKnown, "response_types_supported", "code"), requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"), @@ -118,6 +119,7 @@ export type ValidatedIssuerMetadata = Partial & | "response_types_supported" | "grant_types_supported" | "code_challenge_methods_supported" + | "device_authorization_endpoint" > & { // MSC2965 extensions to the OIDC spec account_management_uri?: string; @@ -176,7 +178,12 @@ const decodeIdToken = (token: string): IdTokenClaims => { * @param nonce - nonce used in the authentication request * @throws when id token is invalid */ -export const validateIdToken = (idToken: string | undefined, issuer: string, clientId: string, nonce: string): void => { +export const validateIdToken = ( + idToken: string | undefined, + issuer: string, + clientId: string, + nonce: string | undefined, +): void => { try { if (!idToken) { throw new Error("No ID token"); @@ -201,7 +208,7 @@ export const validateIdToken = (idToken: string | undefined, issuer: string, cli * If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked * to verify that it is the same value as the one that was sent in the Authentication Request. */ - if (claims.nonce !== nonce) { + if (nonce !== undefined && claims.nonce !== nonce) { throw new Error("Invalid nonce"); }