From fe46fec1615e56d67fbf47ea6b69f0ad2d3f96f5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Feb 2024 14:43:52 +0000 Subject: [PATCH] Allow specifying more OIDC client metadata for dynamic registration (#4070) --- spec/unit/oidc/register.spec.ts | 38 +++++++++++++++++-------- src/@types/common.ts | 17 +++++++++++ src/matrix.ts | 1 + src/oidc/register.ts | 50 ++++++++++++++++++++++----------- 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 src/@types/common.ts diff --git a/spec/unit/oidc/register.spec.ts b/spec/unit/oidc/register.spec.ts index ae61ecd2d..f0e257841 100644 --- a/spec/unit/oidc/register.spec.ts +++ b/spec/unit/oidc/register.spec.ts @@ -17,13 +17,22 @@ limitations under the License. import fetchMockJest from "fetch-mock-jest"; import { OidcError } from "../../../src/oidc/error"; -import { registerOidcClient } from "../../../src/oidc/register"; +import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register"; describe("registerOidcClient()", () => { const issuer = "https://auth.com/"; const registrationEndpoint = "https://auth.com/register"; const clientName = "Element"; const baseUrl = "https://just.testing"; + const metadata: OidcRegistrationClientMetadata = { + clientUri: baseUrl, + redirectUris: [baseUrl], + clientName, + applicationType: "web", + tosUri: "http://tos-uri", + policyUri: "http://policy-uri", + contacts: ["admin@example.com"], + }; const dynamicClientId = "xyz789"; const delegatedAuthConfig = { @@ -42,14 +51,19 @@ describe("registerOidcClient()", () => { status: 200, body: JSON.stringify({ client_id: dynamicClientId }), }); - expect(await registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId); - expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, { - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - }, - method: "POST", - body: JSON.stringify({ + expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId); + expect(fetchMockJest).toHaveBeenCalledWith( + registrationEndpoint, + expect.objectContaining({ + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + method: "POST", + }), + ); + expect(JSON.parse(fetchMockJest.mock.calls[0][1]!.body as string)).toEqual( + expect.objectContaining({ client_name: clientName, client_uri: baseUrl, response_types: ["code"], @@ -59,14 +73,14 @@ describe("registerOidcClient()", () => { token_endpoint_auth_method: "none", application_type: "web", }), - }); + ); }); it("should throw when registration request fails", async () => { fetchMockJest.post(registrationEndpoint, { status: 500, }); - await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow( + await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow( OidcError.DynamicRegistrationFailed, ); }); @@ -77,7 +91,7 @@ describe("registerOidcClient()", () => { // no clientId in response body: "{}", }); - await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow( + await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow( OidcError.DynamicRegistrationInvalid, ); }); diff --git a/src/@types/common.ts b/src/@types/common.ts new file mode 100644 index 000000000..77b856faf --- /dev/null +++ b/src/@types/common.ts @@ -0,0 +1,17 @@ +/* +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. +*/ + +export type NonEmptyArray = [T, ...T[]]; diff --git a/src/matrix.ts b/src/matrix.ts index 6be935040..9dd83affa 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -56,6 +56,7 @@ export * from "./crypto/store/localStorage-crypto-store"; export * from "./crypto/store/indexeddb-crypto-store"; export type { OutgoingRoomKeyRequest } from "./crypto/store/base"; export * from "./content-repo"; +export * from "./@types/common"; export * from "./@types/uia"; export * from "./@types/event"; export * from "./@types/PushRules"; diff --git a/src/oidc/register.ts b/src/oidc/register.ts index 44f933fd4..65add4935 100644 --- a/src/oidc/register.ts +++ b/src/oidc/register.ts @@ -19,16 +19,37 @@ import { OidcError } from "./error"; import { Method } from "../http-api"; import { logger } from "../logger"; import { ValidatedIssuerConfig } from "./validate"; +import { NonEmptyArray } from "../@types/common"; /** * Client metadata passed to registration endpoint */ export type OidcRegistrationClientMetadata = { - clientName: string; - clientUri: string; - redirectUris: string[]; + clientName: OidcRegistrationRequestBody["client_name"]; + clientUri: OidcRegistrationRequestBody["client_uri"]; + logoUri?: OidcRegistrationRequestBody["logo_uri"]; + applicationType: OidcRegistrationRequestBody["application_type"]; + redirectUris: OidcRegistrationRequestBody["redirect_uris"]; + contacts: OidcRegistrationRequestBody["contacts"]; + tosUri: OidcRegistrationRequestBody["tos_uri"]; + policyUri: OidcRegistrationRequestBody["policy_uri"]; }; +interface OidcRegistrationRequestBody { + client_name: string; + client_uri: string; + logo_uri?: string; + contacts: NonEmptyArray; + tos_uri: string; + policy_uri: string; + redirect_uris?: NonEmptyArray; + response_types?: NonEmptyArray; + grant_types?: NonEmptyArray; + id_token_signed_response_alg: string; + token_endpoint_auth_method: string; + application_type: "web" | "native"; +} + /** * Make the client registration request * @param registrationEndpoint - URL as returned from issuer ./well-known/openid-configuration @@ -42,7 +63,7 @@ const doRegistration = async ( clientMetadata: OidcRegistrationClientMetadata, ): Promise => { // https://openid.net/specs/openid-connect-registration-1_0.html - const metadata = { + const metadata: OidcRegistrationRequestBody = { client_name: clientMetadata.clientName, client_uri: clientMetadata.clientUri, response_types: ["code"], @@ -50,7 +71,11 @@ const doRegistration = async ( redirect_uris: clientMetadata.redirectUris, id_token_signed_response_alg: "RS256", token_endpoint_auth_method: "none", - application_type: "web", + application_type: clientMetadata.applicationType, + logo_uri: clientMetadata.logoUri, + contacts: clientMetadata.contacts, + policy_uri: clientMetadata.policyUri, + tos_uri: clientMetadata.tosUri, }; const headers = { "Accept": "application/json", @@ -88,25 +113,16 @@ const doRegistration = async ( /** * Attempts dynamic registration against the configured registration endpoint * @param delegatedAuthConfig - Auth config from ValidatedServerConfig - * @param clientName - Client name to register with the OP, eg 'Element' - * @param baseUrl - URL of the home page of the Client, eg 'https://app.element.io/' + * @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: IDelegatedAuthConfig & ValidatedIssuerConfig, - clientName: string, - baseUrl: string, + clientMetadata: OidcRegistrationClientMetadata, ): Promise => { - const clientMetadata = { - clientName, - clientUri: baseUrl, - redirectUris: [baseUrl], - }; if (!delegatedAuthConfig.registrationEndpoint) { throw new Error(OidcError.DynamicRegistrationNotSupported); } - const clientId = await doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata); - - return clientId; + return doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata); };