From 381aeba4f83df86e62e50a52c0e481439dd4cd7d Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 29 May 2023 19:06:33 +1200 Subject: [PATCH 01/11] validate m.authentication, fetch issuer wellknown --- src/autodiscovery.ts | 148 ++++++++++++++++++++++++++++++++++++++++++- src/client.ts | 4 +- 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 5a7294b29ab..5259bd9bbc6 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IClientWellKnown, IWellKnownConfig } from "./client"; +import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, M_AUTHENTICATION, IServerVersions } from "./client"; import { logger } from "./logger"; import { MatrixError, Method, timeoutSignal } from "./http-api"; @@ -40,6 +40,11 @@ enum AutoDiscoveryError { InvalidIs = "Invalid identity server discovery response", MissingWellknown = "No .well-known JSON file found", InvalidJson = "Invalid JSON", + OidcNotSupported = "OIDC authentication not supported", + OidcMisconfigured = "OIDC is misconfigured", + // @TODO(kerrya) surface more detailed errors + OidcGeneral = "Something went wrong with OIDC discovery", + OidcOpSupport = "Configured OIDC OP does not support required functions" } interface WellKnownConfig extends Omit { @@ -47,9 +52,101 @@ interface WellKnownConfig extends Omit { error?: IWellKnownConfig["error"] | null; } +type ValidatedIssuerConfig = { + authorizationEndpoint: string; + tokenEndpoint: string; + registrationEndpoint: string; +} + +interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig { + state: AutoDiscoveryAction; + error?: IWellKnownConfig["error"] | null; +} + export interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; + "m.authentication"?: DelegatedAuthConfig; +} + +/** + * Validates MSC2965 m.authentication config + * Returns valid configuration + * @param {IClientWellKnown} wellKnown + * @returns {IDelegatedAuthConfig} config when present and valid, otherwise undefined + * @throws when config is not found or invalid + */ +const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): IDelegatedAuthConfig => { + const authentication = M_AUTHENTICATION.findIn(wellKnown); + + if (!authentication) { + throw new Error(AutoDiscoveryError.OidcNotSupported); + } + + if ( + typeof authentication.issuer === "string" && + !authentication.account || typeof authentication.account === "string" + ) { + return { + issuer: authentication.issuer, + account: authentication.account, + } + } + + throw new Error(AutoDiscoveryError.OidcMisconfigured) +} + +// force into a record to make accessing properties easier +const isRecord = (value: unknown): value is Record => !!value && typeof value === "object"; +const requiredStringProperty = (wellKnown: Record, key: string): boolean => { + if (!wellKnown[key] || typeof wellKnown[key] !== "string") { + logger.error(`OIDC issuer configuration: ${key} is invalid`) + return false; + } + return true; +} +const requiredArrayValue = (wellKnown: Record, key: string, value: any): boolean => { + const array = wellKnown[key]; + if (!array || !Array.isArray(array) || !array.find(value)) { + logger.error(`OIDC issuer configuration: ${key} is invalid. ${value} is required.`) + return false; + } + return true; +} + +/** + * Validates issue `.well-known/openid-configuration` + * As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html + * validates that OP is compatible with Element's OIDC flow + * @param {unknown} wellKnown + * @returns {ValidatedIssuerConfig} valid issuer config + * @throws {Error} when issuer config is not found or is invalid + */ +const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => { + if (!isRecord(wellKnown)) { + logger.error("Issuer configuration not found"); + throw new Error(AutoDiscoveryError.OidcOpSupport); + } + + const isInvalid = [ + requiredStringProperty(wellKnown, 'authorization_endpoint'), + requiredStringProperty(wellKnown, 'token_endpoint'), + requiredStringProperty(wellKnown, 'registration_endpoint'), + requiredArrayValue(wellKnown, 'response_types_supported', 'code'), + requiredArrayValue(wellKnown, 'grant_types_supported', 'authorization_code'), + requiredArrayValue(wellKnown, 'code_challenge_methods_supported', 'S256'), + ].some(isValid => !isValid); + + if (!isInvalid) { + return { + authorizationEndpoint: wellKnown['authorization_endpoint'], + tokenEndpoint: wellKnown['token_endpoint'], + registrationEndpoint: wellKnown['registration_endpoint'], + } as ValidatedIssuerConfig; + } + + logger.error("Issuer configuration not valid"); + throw new Error(AutoDiscoveryError.OidcOpSupport) } /** @@ -170,7 +267,7 @@ export class AutoDiscovery { } // Step 3: Make sure the homeserver URL points to a homeserver. - const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`); + const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`); if (!hsVersions?.raw?.["versions"]) { logger.error("Invalid /versions response"); clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; @@ -256,10 +353,55 @@ export class AutoDiscovery { } }); + await this.validateDiscoveryAuthenticationConfig(wellknown); + // Step 8: Give the config to the caller (finally) return Promise.resolve(clientConfig); } + public static async validateDiscoveryAuthenticationConfig(wellKnown: IClientWellKnown): Promise { + try { + const homeserverAuthenticationConfig = validateWellKnownAuthentication(wellKnown); + + const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(homeserverAuthenticationConfig.issuer)}/.well-known/openid-configuration`; + const issuerWellKnown = await this.fetchWellKnownObject(issuerOpenIdConfigUrl); + + if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) { + // @TODO(kerrya) consider more error handling + throw new Error(AutoDiscoveryError.OidcGeneral) + } + + const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw); + + const delegatedAuthConfig: DelegatedAuthConfig = { + state: AutoDiscoveryAction.SUCCESS, + error: null, + ...homeserverAuthenticationConfig, + ...validatedIssuerConfig + } + return delegatedAuthConfig; + + } catch (error) { + console.log('hhh', error); + // @TODO(kerrya) what to do here? + // should some be ignore and some fail_error? + // if ( + // (error as Error).message === AutoDiscoveryError.OidcNotSupported || + // (error as Error).message === AutoDiscoveryError.OidcMisconfigured + // ) { + // return { + // state: AutoDiscoveryAction.IGNORE, + // error: (error as Error).message as AutoDiscoveryError + // } + // }; + // } + return { + state: AutoDiscoveryAction.FAIL_ERROR, + error: error.message as AutoDiscoveryError, + } + } + } + /** * Attempts to automatically discover client configuration information * prior to logging in. Such information includes the homeserver URL @@ -412,7 +554,7 @@ export class AutoDiscovery { * @returns Promise which resolves to the returned state. * @internal */ - private static async fetchWellKnownObject(url: string): Promise { + private static async fetchWellKnownObject(url: string): Promise> > { let response: Response; try { diff --git a/src/client.ts b/src/client.ts index caca1a89a82..68091578f54 100644 --- a/src/client.ts +++ b/src/client.ts @@ -597,8 +597,8 @@ export interface IClientWellKnown { [M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965 } -export interface IWellKnownConfig { - raw?: IClientWellKnown; +export interface IWellKnownConfig { + raw?: T; action?: AutoDiscoveryAction; reason?: string; error?: Error | string; From 413398a7f2ae03c96ee9f1957811eec046c96a0a Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 30 May 2023 13:02:41 +1200 Subject: [PATCH 02/11] move validation functions into separate file --- src/autodiscovery.ts | 152 ++++++++++--------------------------------- src/oidc/validate.ts | 95 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 118 deletions(-) create mode 100644 src/oidc/validate.ts diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 5259bd9bbc6..7e1a4d14c33 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -15,9 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, M_AUTHENTICATION, IServerVersions } from "./client"; +import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions } from "./client"; import { logger } from "./logger"; import { MatrixError, Method, timeoutSignal } from "./http-api"; +import { + OidcDiscoveryError, + ValidatedIssuerConfig, + validateOIDCIssuerWellKnown, + validateWellKnownAuthentication, +} from "./oidc/validate"; // Dev note: Auto discovery is part of the spec. // See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -40,24 +46,12 @@ enum AutoDiscoveryError { InvalidIs = "Invalid identity server discovery response", MissingWellknown = "No .well-known JSON file found", InvalidJson = "Invalid JSON", - OidcNotSupported = "OIDC authentication not supported", - OidcMisconfigured = "OIDC is misconfigured", - // @TODO(kerrya) surface more detailed errors - OidcGeneral = "Something went wrong with OIDC discovery", - OidcOpSupport = "Configured OIDC OP does not support required functions" } interface WellKnownConfig extends Omit { state: AutoDiscoveryAction; error?: IWellKnownConfig["error"] | null; } - -type ValidatedIssuerConfig = { - authorizationEndpoint: string; - tokenEndpoint: string; - registrationEndpoint: string; -} - interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig { state: AutoDiscoveryAction; error?: IWellKnownConfig["error"] | null; @@ -69,86 +63,6 @@ export interface ClientConfig extends Omit { - const authentication = M_AUTHENTICATION.findIn(wellKnown); - - if (!authentication) { - throw new Error(AutoDiscoveryError.OidcNotSupported); - } - - if ( - typeof authentication.issuer === "string" && - !authentication.account || typeof authentication.account === "string" - ) { - return { - issuer: authentication.issuer, - account: authentication.account, - } - } - - throw new Error(AutoDiscoveryError.OidcMisconfigured) -} - -// force into a record to make accessing properties easier -const isRecord = (value: unknown): value is Record => !!value && typeof value === "object"; -const requiredStringProperty = (wellKnown: Record, key: string): boolean => { - if (!wellKnown[key] || typeof wellKnown[key] !== "string") { - logger.error(`OIDC issuer configuration: ${key} is invalid`) - return false; - } - return true; -} -const requiredArrayValue = (wellKnown: Record, key: string, value: any): boolean => { - const array = wellKnown[key]; - if (!array || !Array.isArray(array) || !array.find(value)) { - logger.error(`OIDC issuer configuration: ${key} is invalid. ${value} is required.`) - return false; - } - return true; -} - -/** - * Validates issue `.well-known/openid-configuration` - * As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html - * validates that OP is compatible with Element's OIDC flow - * @param {unknown} wellKnown - * @returns {ValidatedIssuerConfig} valid issuer config - * @throws {Error} when issuer config is not found or is invalid - */ -const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => { - if (!isRecord(wellKnown)) { - logger.error("Issuer configuration not found"); - throw new Error(AutoDiscoveryError.OidcOpSupport); - } - - const isInvalid = [ - requiredStringProperty(wellKnown, 'authorization_endpoint'), - requiredStringProperty(wellKnown, 'token_endpoint'), - requiredStringProperty(wellKnown, 'registration_endpoint'), - requiredArrayValue(wellKnown, 'response_types_supported', 'code'), - requiredArrayValue(wellKnown, 'grant_types_supported', 'authorization_code'), - requiredArrayValue(wellKnown, 'code_challenge_methods_supported', 'S256'), - ].some(isValid => !isValid); - - if (!isInvalid) { - return { - authorizationEndpoint: wellKnown['authorization_endpoint'], - tokenEndpoint: wellKnown['token_endpoint'], - registrationEndpoint: wellKnown['registration_endpoint'], - } as ValidatedIssuerConfig; - } - - logger.error("Issuer configuration not valid"); - throw new Error(AutoDiscoveryError.OidcOpSupport) -} - /** * Utilities for automatically discovery resources, such as homeservers * for users to log in to. @@ -353,22 +267,27 @@ export class AutoDiscovery { } }); - await this.validateDiscoveryAuthenticationConfig(wellknown); + const authConfig = await this.validateDiscoveryAuthenticationConfig(wellknown); + clientConfig.m_authentication = authConfig; // Step 8: Give the config to the caller (finally) return Promise.resolve(clientConfig); } - public static async validateDiscoveryAuthenticationConfig(wellKnown: IClientWellKnown): Promise { + public static async validateDiscoveryAuthenticationConfig( + wellKnown: IClientWellKnown, + ): Promise { try { const homeserverAuthenticationConfig = validateWellKnownAuthentication(wellKnown); - const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(homeserverAuthenticationConfig.issuer)}/.well-known/openid-configuration`; + const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl( + homeserverAuthenticationConfig.issuer, + )}/.well-known/openid-configuration`; const issuerWellKnown = await this.fetchWellKnownObject(issuerOpenIdConfigUrl); if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) { - // @TODO(kerrya) consider more error handling - throw new Error(AutoDiscoveryError.OidcGeneral) + logger.error("Failed to fetch issuer openid configuration"); + throw new Error(OidcDiscoveryError.General); } const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw); @@ -377,28 +296,22 @@ export class AutoDiscovery { state: AutoDiscoveryAction.SUCCESS, error: null, ...homeserverAuthenticationConfig, - ...validatedIssuerConfig - } + ...validatedIssuerConfig, + }; return delegatedAuthConfig; - } catch (error) { - console.log('hhh', error); - // @TODO(kerrya) what to do here? - // should some be ignore and some fail_error? - // if ( - // (error as Error).message === AutoDiscoveryError.OidcNotSupported || - // (error as Error).message === AutoDiscoveryError.OidcMisconfigured - // ) { - // return { - // state: AutoDiscoveryAction.IGNORE, - // error: (error as Error).message as AutoDiscoveryError - // } - // }; - // } + console.log("hhh", error); + + const errorMessage = (error as Error).message as unknown as OidcDiscoveryError; + const errorType = Object.values(OidcDiscoveryError).includes(errorMessage) + ? errorMessage + : OidcDiscoveryError.General; + + // @TODO(kerrya) better way to handle this fail type return { state: AutoDiscoveryAction.FAIL_ERROR, - error: error.message as AutoDiscoveryError, - } + error: errorType, + } as unknown as DelegatedAuthConfig; } } @@ -450,7 +363,8 @@ export class AutoDiscovery { // Step 1: Actually request the .well-known JSON file and make sure it // at least has a homeserver definition. - const wellknown = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`); + const domainWithProtocol = domain.includes("://") ? domain : `https://${domain}`; + const wellknown = await this.fetchWellKnownObject(`${domainWithProtocol}/.well-known/matrix/client`); if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) { logger.error("No response or error when parsing .well-known"); if (wellknown.reason) logger.error(wellknown.reason); @@ -554,7 +468,9 @@ export class AutoDiscovery { * @returns Promise which resolves to the returned state. * @internal */ - private static async fetchWellKnownObject(url: string): Promise> > { + private static async fetchWellKnownObject( + url: string, + ): Promise>> { let response: Response; try { diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts new file mode 100644 index 00000000000..98d37b3b085 --- /dev/null +++ b/src/oidc/validate.ts @@ -0,0 +1,95 @@ +import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client"; +import { logger } from "../logger"; + +export enum OidcDiscoveryError { + NotSupported = "OIDC authentication not supported", + Misconfigured = "OIDC is misconfigured", + General = "Something went wrong with OIDC discovery", + OpSupport = "Configured OIDC OP does not support required functions", +} + +export type ValidatedIssuerConfig = { + authorizationEndpoint: string; + tokenEndpoint: string; + registrationEndpoint: string; +}; + +/** + * Validates MSC2965 m.authentication config + * Returns valid configuration + * @param wellKnown - client well known as returned from ./well-known/client/matrix + * @returns config - when present and valid + * @throws when config is not found or invalid + */ +export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): IDelegatedAuthConfig => { + const authentication = M_AUTHENTICATION.findIn(wellKnown); + + if (!authentication) { + throw new Error(OidcDiscoveryError.NotSupported); + } + + if ( + (typeof authentication.issuer === "string" && !authentication.account) || + typeof authentication.account === "string" + ) { + return { + issuer: authentication.issuer, + account: authentication.account, + }; + } + + throw new Error(OidcDiscoveryError.Misconfigured); +}; + +// force into a record to make accessing properties easier +const isRecord = (value: unknown): value is Record => !!value && typeof value === "object"; +const requiredStringProperty = (wellKnown: Record, key: string): boolean => { + if (!wellKnown[key] || typeof wellKnown[key] !== "string") { + logger.error(`OIDC issuer configuration: ${key} is invalid`); + return false; + } + return true; +}; +const requiredArrayValue = (wellKnown: Record, key: string, value: any): boolean => { + const array = wellKnown[key]; + if (!array || !Array.isArray(array) || !array.find(value)) { + logger.error(`OIDC issuer configuration: ${key} is invalid. ${value} is required.`); + return false; + } + return true; +}; + +/** + * Validates issue `.well-known/openid-configuration` + * As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html + * validates that OP is compatible with Element's OIDC flow + * @param wellKnown - json object + * @returns valid issuer config + * @throws Error - when issuer config is not found or is invalid + */ +export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => { + if (!isRecord(wellKnown)) { + logger.error("Issuer configuration not found"); + throw new Error(OidcDiscoveryError.OpSupport); + } + + const isInvalid = [ + requiredStringProperty(wellKnown, "authorization_endpoint"), + requiredStringProperty(wellKnown, "token_endpoint"), + requiredStringProperty(wellKnown, "registration_endpoint"), + requiredArrayValue(wellKnown, "response_types_supported", "code"), + requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"), + requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"), + ].some((isValid) => !isValid); + + if (!isInvalid) { + return { + authorizationEndpoint: wellKnown["authorization_endpoint"], + tokenEndpoint: wellKnown["token_endpoint"], + registrationEndpoint: wellKnown["registration_endpoint"], + } as ValidatedIssuerConfig; + } + + logger.error("Issuer configuration not valid"); + throw new Error(OidcDiscoveryError.OpSupport); +}; From c081bca3795e4ef79a426511c4e566693257f0e7 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 31 May 2023 17:14:21 +1200 Subject: [PATCH 03/11] test validateWellKnownAuthentication --- spec/unit/oidc/validate.spec.ts | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 spec/unit/oidc/validate.spec.ts diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts new file mode 100644 index 00000000000..9bad9bfd428 --- /dev/null +++ b/spec/unit/oidc/validate.spec.ts @@ -0,0 +1,103 @@ +/* +Copyright 2023 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 { M_AUTHENTICATION } from "../../../src"; +import { OidcDiscoveryError, validateWellKnownAuthentication } from "../../../src/oidc/validate"; + +describe('validateWellKnownAuthentication()', () => { + const baseWk = { + "m.homeserver" : { + base_url: "https://hs.org" + } + } + it('should throw not supported error when wellKnown has no m.authentication section', () => { + expect(() => validateWellKnownAuthentication(baseWk)).toThrow(OidcDiscoveryError.NotSupported); + }); + + it('should throw misconfigured error when authentication issuer is not a string', () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.stable!]: { + issuer: { url: 'test.com' } + } + } + expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); + }); + + it('should throw misconfigured error when authentication account is not a string', () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.stable!]: { + issuer: "test.com", + account: { url: "test" } + } + } + expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); + }); + + it('should return valid config when wk uses stable m.authentication', () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.stable!]: { + issuer: "test.com", + account: "account.com", + } + } + expect(validateWellKnownAuthentication(wk)).toEqual({ + issuer: "test.com", + account: "account.com" + }); + }); + + it('should return valid config when m.authentication account is falsy', () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.stable!]: { + issuer: "test.com", + } + } + expect(validateWellKnownAuthentication(wk)).toEqual({ + issuer: "test.com", + }); + }); + + it('should remove unexpected properties', () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.stable!]: { + issuer: "test.com", + somethingElse: "test" + } + } + expect(validateWellKnownAuthentication(wk)).toEqual({ + issuer: "test.com", + }); + }); + + it('should return valid config when wk uses unstable prefix for m.authentication', () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.unstable!]: { + issuer: "test.com", + account: "account.com", + } + } + expect(validateWellKnownAuthentication(wk)).toEqual({ + issuer: "test.com", + account: "account.com" + }); + }); +}); \ No newline at end of file From 661a6320acb45d15c88c0ace17166b995dd88555 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 31 May 2023 17:34:19 +1200 Subject: [PATCH 04/11] test validateOIDCIssuerWellKnown --- spec/unit/oidc/validate.spec.ts | 138 +++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 31 deletions(-) diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index 9bad9bfd428..b0de15dea76 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -15,89 +15,165 @@ limitations under the License. */ import { M_AUTHENTICATION } from "../../../src"; -import { OidcDiscoveryError, validateWellKnownAuthentication } from "../../../src/oidc/validate"; +import { logger } from "../../../src/logger"; +import { + OidcDiscoveryError, + validateOIDCIssuerWellKnown, + validateWellKnownAuthentication, +} from "../../../src/oidc/validate"; -describe('validateWellKnownAuthentication()', () => { +describe("validateWellKnownAuthentication()", () => { const baseWk = { - "m.homeserver" : { - base_url: "https://hs.org" - } - } - it('should throw not supported error when wellKnown has no m.authentication section', () => { + "m.homeserver": { + base_url: "https://hs.org", + }, + }; + it("should throw not supported error when wellKnown has no m.authentication section", () => { expect(() => validateWellKnownAuthentication(baseWk)).toThrow(OidcDiscoveryError.NotSupported); }); - it('should throw misconfigured error when authentication issuer is not a string', () => { + it("should throw misconfigured error when authentication issuer is not a string", () => { const wk = { ...baseWk, [M_AUTHENTICATION.stable!]: { - issuer: { url: 'test.com' } - } - } + issuer: { url: "test.com" }, + }, + }; expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); }); - it('should throw misconfigured error when authentication account is not a string', () => { + it("should throw misconfigured error when authentication account is not a string", () => { const wk = { ...baseWk, [M_AUTHENTICATION.stable!]: { issuer: "test.com", - account: { url: "test" } - } - } + account: { url: "test" }, + }, + }; expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); }); - it('should return valid config when wk uses stable m.authentication', () => { + it("should return valid config when wk uses stable m.authentication", () => { const wk = { ...baseWk, [M_AUTHENTICATION.stable!]: { issuer: "test.com", account: "account.com", - } - } + }, + }; expect(validateWellKnownAuthentication(wk)).toEqual({ issuer: "test.com", - account: "account.com" + account: "account.com", }); }); - it('should return valid config when m.authentication account is falsy', () => { + it("should return valid config when m.authentication account is falsy", () => { const wk = { ...baseWk, [M_AUTHENTICATION.stable!]: { issuer: "test.com", - } - } + }, + }; expect(validateWellKnownAuthentication(wk)).toEqual({ issuer: "test.com", }); }); - it('should remove unexpected properties', () => { + it("should remove unexpected properties", () => { const wk = { ...baseWk, [M_AUTHENTICATION.stable!]: { issuer: "test.com", - somethingElse: "test" - } - } + somethingElse: "test", + }, + }; expect(validateWellKnownAuthentication(wk)).toEqual({ issuer: "test.com", }); }); - it('should return valid config when wk uses unstable prefix for m.authentication', () => { + it("should return valid config when wk uses unstable prefix for m.authentication", () => { const wk = { ...baseWk, [M_AUTHENTICATION.unstable!]: { issuer: "test.com", account: "account.com", - } - } + }, + }; expect(validateWellKnownAuthentication(wk)).toEqual({ issuer: "test.com", - account: "account.com" + account: "account.com", }); }); -}); \ No newline at end of file +}); + +describe("validateOIDCIssuerWellKnown", () => { + const validWk = { + authorization_endpoint: "https://test.org/authorize", + token_endpoint: "https://authorize.org/token", + registration_endpoint: "https://authorize.org/regsiter", + response_types_supported: ["code"], + grant_types_supported: ["authorization_code"], + code_challenge_methods_supported: ["S256"], + }; + beforeEach(() => { + // stub to avoid console litter + jest.spyOn(logger, "error") + .mockClear() + .mockImplementation(() => {}); + }); + + it("should throw OP support error when wellKnown is not an object", () => { + expect(() => { + validateOIDCIssuerWellKnown([]); + }).toThrow(OidcDiscoveryError.OpSupport); + expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed"); + }); + + it("should log all errors before throwing", () => { + expect(() => { + validateOIDCIssuerWellKnown({ + ...validWk, + authorization_endpoint: undefined, + response_types_supported: [], + }); + }).toThrow(OidcDiscoveryError.OpSupport); + expect(logger.error).toHaveBeenCalledWith("OIDC issuer configuration: authorization_endpoint is invalid"); + expect(logger.error).toHaveBeenCalledWith( + "OIDC issuer configuration: response_types_supported is invalid. code is required.", + ); + }); + + it("should return validated issuer config", () => { + expect(validateOIDCIssuerWellKnown(validWk)).toEqual({ + authorizationEndpoint: validWk.authorization_endpoint, + tokenEndpoint: validWk.token_endpoint, + registrationEndpoint: validWk.registration_endpoint, + }); + }); + + type TestCase = [string, any]; + it.each([ + ["authorization_endpoint", undefined], + ["authorization_endpoint", { not: "a string" }], + ["token_endpoint", undefined], + ["token_endpoint", { not: "a string" }], + ["registration_endpoint", undefined], + ["registration_endpoint", { not: "a string" }], + ["response_types_supported", undefined], + ["response_types_supported", "not an array"], + ["response_types_supported", ["doesnt include code"]], + ["grant_types_supported", undefined], + ["grant_types_supported", "not an array"], + ["grant_types_supported", ["doesnt include authorization_code"]], + ["code_challenge_methods_supported", undefined], + ["code_challenge_methods_supported", "not an array"], + ["code_challenge_methods_supported", ["doesnt include S256"]], + ])("should throw OP support error when %s is %s", (key, value) => { + const wk = { + ...validWk, + [key]: value, + }; + expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcDiscoveryError.OpSupport); + }); +}); From abeee9d1cef0f203fd363ef707246b8129a2a0b7 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 1 Jun 2023 17:36:52 +1200 Subject: [PATCH 05/11] add authentication cases to autodiscovery tests --- spec/unit/autodiscovery.spec.ts | 23 ++++++++++++++++++++--- src/autodiscovery.ts | 13 ++++++++----- src/oidc/validate.ts | 27 ++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index f6db5327c7b..fcb5284af3f 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -18,6 +18,7 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; import { AutoDiscovery } from "../../src/autodiscovery"; +import { OidcDiscoveryError } from "../../src/oidc/validate"; describe("AutoDiscovery", function () { const getHttpBackend = (): MockHttpBackend => { @@ -368,7 +369,7 @@ describe("AutoDiscovery", function () { }, ); - it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + "m.homeserver", function () { + it("should return SUCCESS when .well-known has a verifiably accurate base_url for m.homeserver", function () { const httpBackend = getHttpBackend(); httpBackend .when("GET", "/_matrix/client/versions") @@ -397,6 +398,10 @@ describe("AutoDiscovery", function () { error: null, base_url: null, }, + "m.authentication": { + state: "IGNORE", + error: OidcDiscoveryError.NotSupported, + }, }; expect(conf).toEqual(expected); @@ -434,6 +439,10 @@ describe("AutoDiscovery", function () { error: null, base_url: null, }, + "m.authentication": { + state: "IGNORE", + error: OidcDiscoveryError.NotSupported, + }, }; expect(conf).toEqual(expected); @@ -625,7 +634,7 @@ describe("AutoDiscovery", function () { }, ); - it("should return SUCCESS when the identity server configuration is " + "verifiably accurate", function () { + it("should return SUCCESS when the identity server configuration is verifiably accurate", function () { const httpBackend = getHttpBackend(); httpBackend .when("GET", "/_matrix/client/versions") @@ -664,6 +673,10 @@ describe("AutoDiscovery", function () { error: null, base_url: "https://identity.example.org", }, + "m.authentication": { + state: "IGNORE", + error: OidcDiscoveryError.NotSupported, + }, }; expect(conf).toEqual(expected); @@ -671,7 +684,7 @@ describe("AutoDiscovery", function () { ]); }); - it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", function () { + it("should return SUCCESS and preserve non-standard keys from the .well-known response", function () { const httpBackend = getHttpBackend(); httpBackend .when("GET", "/_matrix/client/versions") @@ -716,6 +729,10 @@ describe("AutoDiscovery", function () { "org.example.custom.property": { cupcakes: "yes", }, + "m.authentication": { + state: "IGNORE", + error: OidcDiscoveryError.NotSupported, + }, }; expect(conf).toEqual(expected); diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 7e1a4d14c33..8af5a5737b9 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions } from "./client"; +import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client"; import { logger } from "./logger"; import { MatrixError, Method, timeoutSignal } from "./http-api"; import { @@ -268,7 +268,7 @@ export class AutoDiscovery { }); const authConfig = await this.validateDiscoveryAuthenticationConfig(wellknown); - clientConfig.m_authentication = authConfig; + clientConfig[M_AUTHENTICATION.stable!] = authConfig; // Step 8: Give the config to the caller (finally) return Promise.resolve(clientConfig); @@ -300,16 +300,19 @@ export class AutoDiscovery { }; return delegatedAuthConfig; } catch (error) { - console.log("hhh", error); - const errorMessage = (error as Error).message as unknown as OidcDiscoveryError; const errorType = Object.values(OidcDiscoveryError).includes(errorMessage) ? errorMessage : OidcDiscoveryError.General; + const state = + errorType === OidcDiscoveryError.NotSupported + ? AutoDiscoveryAction.IGNORE + : AutoDiscoveryAction.FAIL_ERROR; + // @TODO(kerrya) better way to handle this fail type return { - state: AutoDiscoveryAction.FAIL_ERROR, + state, error: errorType, } as unknown as DelegatedAuthConfig; } diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 98d37b3b085..f8b65d2da45 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 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 { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client"; import { logger } from "../logger"; @@ -29,8 +45,8 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID } if ( - (typeof authentication.issuer === "string" && !authentication.account) || - typeof authentication.account === "string" + typeof authentication.issuer === "string" && + (!authentication.account || typeof authentication.account === "string") ) { return { issuer: authentication.issuer, @@ -42,7 +58,8 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID }; // force into a record to make accessing properties easier -const isRecord = (value: unknown): value is Record => !!value && typeof value === "object"; +const isRecord = (value: unknown): value is Record => + !!value && typeof value === "object" && !Array.isArray(value); const requiredStringProperty = (wellKnown: Record, key: string): boolean => { if (!wellKnown[key] || typeof wellKnown[key] !== "string") { logger.error(`OIDC issuer configuration: ${key} is invalid`); @@ -52,7 +69,7 @@ const requiredStringProperty = (wellKnown: Record, key: string) }; const requiredArrayValue = (wellKnown: Record, key: string, value: any): boolean => { const array = wellKnown[key]; - if (!array || !Array.isArray(array) || !array.find(value)) { + if (!array || !Array.isArray(array) || !array.includes(value)) { logger.error(`OIDC issuer configuration: ${key} is invalid. ${value} is required.`); return false; } @@ -69,7 +86,7 @@ const requiredArrayValue = (wellKnown: Record, key: string, val */ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => { if (!isRecord(wellKnown)) { - logger.error("Issuer configuration not found"); + logger.error("Issuer configuration not found or malformed"); throw new Error(OidcDiscoveryError.OpSupport); } From 6ab8cc44510bbfbae381d147d07e619de6614232 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 1 Jun 2023 17:55:10 +1200 Subject: [PATCH 06/11] test invalid authentication config on wk --- spec/unit/autodiscovery.spec.ts | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index fcb5284af3f..2a8f080ed20 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -450,6 +450,50 @@ describe("AutoDiscovery", function () { ]); }); + it("should return SUCCESS with authentication error when authentication config is invalid", function () { + const httpBackend = getHttpBackend(); + httpBackend + .when("GET", "/_matrix/client/versions") + .check((req) => { + expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); + }) + .respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.authentication": { + invalid: true, + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "SUCCESS", + error: null, + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + "m.authentication": { + state: "FAIL_ERROR", + error: OidcDiscoveryError.Misconfigured, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + it( "should return SUCCESS / FAIL_PROMPT when the identity server configuration " + "is wrong (missing base_url)", function () { From 5f32b3f6a9f0c33028c45b583dd8e7bb89c477df Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 1 Jun 2023 18:10:07 +1200 Subject: [PATCH 07/11] improve types --- src/autodiscovery.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 8af5a5737b9..0ca16fd67d7 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -48,19 +48,18 @@ enum AutoDiscoveryError { InvalidJson = "Invalid JSON", } -interface WellKnownConfig extends Omit { - state: AutoDiscoveryAction; - error?: IWellKnownConfig["error"] | null; -} -interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig { +interface AutoDiscoveryState { state: AutoDiscoveryAction; error?: IWellKnownConfig["error"] | null; } +interface WellKnownConfig extends Omit, AutoDiscoveryState {} + +interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {} export interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; - "m.authentication"?: DelegatedAuthConfig; + "m.authentication"?: DelegatedAuthConfig | AutoDiscoveryState; } /** @@ -276,7 +275,7 @@ export class AutoDiscovery { public static async validateDiscoveryAuthenticationConfig( wellKnown: IClientWellKnown, - ): Promise { + ): Promise { try { const homeserverAuthenticationConfig = validateWellKnownAuthentication(wellKnown); @@ -310,11 +309,10 @@ export class AutoDiscovery { ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR; - // @TODO(kerrya) better way to handle this fail type return { state, error: errorType, - } as unknown as DelegatedAuthConfig; + }; } } From c53acc0cea03637ef2b224659f5d2108a29b80b5 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 8 Jun 2023 13:55:55 +1200 Subject: [PATCH 08/11] test case for account:false --- spec/unit/oidc/validate.spec.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index b0de15dea76..497d4df308c 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -52,6 +52,17 @@ describe("validateWellKnownAuthentication()", () => { }; expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); }); + + it("should throw misconfigured error when authentication account is false", () => { + const wk = { + ...baseWk, + [M_AUTHENTICATION.stable!]: { + issuer: "test.com", + account: false, + }, + }; + expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); + }); it("should return valid config when wk uses stable m.authentication", () => { const wk = { @@ -67,7 +78,7 @@ describe("validateWellKnownAuthentication()", () => { }); }); - it("should return valid config when m.authentication account is falsy", () => { + it("should return valid config when m.authentication account is missing", () => { const wk = { ...baseWk, [M_AUTHENTICATION.stable!]: { From 6efdb44c9b5d39609230508977dfe71472e95e24 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 8 Jun 2023 13:56:36 +1200 Subject: [PATCH 09/11] use hasOwnProperty in validateWellKnownAuthentication --- src/oidc/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index f8b65d2da45..43accf26ed4 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -46,7 +46,7 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID if ( typeof authentication.issuer === "string" && - (!authentication.account || typeof authentication.account === "string") + (!authentication.hasOwnProperty('account') || typeof authentication.account === "string") ) { return { issuer: authentication.issuer, From cafd6216955ebde2fe09f00e9f5a4dd89b727eb9 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 8 Jun 2023 14:18:50 +1200 Subject: [PATCH 10/11] comments --- spec/unit/oidc/validate.spec.ts | 2 +- src/autodiscovery.ts | 11 +++++++++++ src/oidc/validate.ts | 3 +-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index 497d4df308c..a98f593e029 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -52,7 +52,7 @@ describe("validateWellKnownAuthentication()", () => { }; expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured); }); - + it("should throw misconfigured error when authentication account is false", () => { const wk = { ...baseWk, diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 0ca16fd67d7..a43410bd630 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -273,6 +273,17 @@ export class AutoDiscovery { return Promise.resolve(clientConfig); } + /** + * Validate delegated auth configuration + * - m.authentication config is present and valid + * - delegated auth issuer openid-configuration is reachable + * - delegated auth issuer openid-configuration is configured correctly for us + * When successful, DelegatedAuthConfig will be returned with endpoints used for delegated auth + * Any errors are caught, and AutoDiscoveryState returned with error + * @param wellKnown - configuration object as returned + * by the .well-known auto-discovery endpoint + * @returns Config or failure result + */ public static async validateDiscoveryAuthenticationConfig( wellKnown: IClientWellKnown, ): Promise { diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 43accf26ed4..6e307501977 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -46,7 +46,7 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID if ( typeof authentication.issuer === "string" && - (!authentication.hasOwnProperty('account') || typeof authentication.account === "string") + (!authentication.hasOwnProperty("account") || typeof authentication.account === "string") ) { return { issuer: authentication.issuer, @@ -57,7 +57,6 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID throw new Error(OidcDiscoveryError.Misconfigured); }; -// force into a record to make accessing properties easier const isRecord = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value); const requiredStringProperty = (wellKnown: Record, key: string): boolean => { From dc841e1466e43fc3fccbc8cb4f3d1cbe67ab81fe Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 9 Jun 2023 18:32:30 +1200 Subject: [PATCH 11/11] make registration_endpoint optional --- spec/unit/oidc/validate.spec.ts | 13 +++++++++++-- src/oidc/validate.ts | 13 ++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index a98f593e029..2ad62afc327 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -119,7 +119,7 @@ describe("validateWellKnownAuthentication()", () => { }); describe("validateOIDCIssuerWellKnown", () => { - const validWk = { + const validWk: any = { authorization_endpoint: "https://test.org/authorize", token_endpoint: "https://authorize.org/token", registration_endpoint: "https://authorize.org/regsiter", @@ -163,13 +163,22 @@ describe("validateOIDCIssuerWellKnown", () => { }); }); + it("should return validated issuer config without registrationendpoint", () => { + const wk = { ...validWk }; + delete wk.registration_endpoint; + expect(validateOIDCIssuerWellKnown(wk)).toEqual({ + authorizationEndpoint: validWk.authorization_endpoint, + tokenEndpoint: validWk.token_endpoint, + registrationEndpoint: undefined, + }); + }); + type TestCase = [string, any]; it.each([ ["authorization_endpoint", undefined], ["authorization_endpoint", { not: "a string" }], ["token_endpoint", undefined], ["token_endpoint", { not: "a string" }], - ["registration_endpoint", undefined], ["registration_endpoint", { not: "a string" }], ["response_types_supported", undefined], ["response_types_supported", "not an array"], diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 6e307501977..1a5f672b4a7 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -27,7 +27,7 @@ export enum OidcDiscoveryError { export type ValidatedIssuerConfig = { authorizationEndpoint: string; tokenEndpoint: string; - registrationEndpoint: string; + registrationEndpoint?: string; }; /** @@ -60,7 +60,14 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID const isRecord = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value); const requiredStringProperty = (wellKnown: Record, key: string): boolean => { - if (!wellKnown[key] || typeof wellKnown[key] !== "string") { + if (!wellKnown[key] || !optionalStringProperty(wellKnown, key)) { + logger.error(`OIDC issuer configuration: ${key} is invalid`); + return false; + } + return true; +}; +const optionalStringProperty = (wellKnown: Record, key: string): boolean => { + if (!!wellKnown[key] && typeof wellKnown[key] !== "string") { logger.error(`OIDC issuer configuration: ${key} is invalid`); return false; } @@ -92,7 +99,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer const isInvalid = [ requiredStringProperty(wellKnown, "authorization_endpoint"), requiredStringProperty(wellKnown, "token_endpoint"), - requiredStringProperty(wellKnown, "registration_endpoint"), + optionalStringProperty(wellKnown, "registration_endpoint"), requiredArrayValue(wellKnown, "response_types_supported", "code"), requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"), requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),