From 96d003fa38f5c26cac336c9fb5c38acc5b0bf97b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Feb 2024 12:58:34 +0000 Subject: [PATCH 01/10] Remove m.authentication well-known handling as the MSC moved away from it Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/autodiscovery.spec.ts | 99 +------------------------ src/autodiscovery.ts | 126 +------------------------------- src/client.ts | 3 - 3 files changed, 2 insertions(+), 226 deletions(-) diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index b4025218e66..ceed8be1f57 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -15,13 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import fetchMock from "fetch-mock-jest"; import MockHttpBackend from "matrix-mock-request"; -import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src"; +import { AutoDiscoveryAction } from "../../src"; import { AutoDiscovery } from "../../src/autodiscovery"; -import { OidcError } from "../../src/oidc/error"; -import { makeDelegatedAuthConfig } from "../test-utils/oidc"; // keep to reset the fetch function after using MockHttpBackend // @ts-ignore private property @@ -409,10 +406,6 @@ describe("AutoDiscovery", function () { error: null, base_url: null, }, - "m.authentication": { - state: "IGNORE", - error: OidcError.NotSupported, - }, }; expect(conf).toEqual(expected); @@ -450,10 +443,6 @@ describe("AutoDiscovery", function () { error: null, base_url: null, }, - "m.authentication": { - state: "IGNORE", - error: OidcError.NotSupported, - }, }; expect(conf).toEqual(expected); @@ -476,9 +465,6 @@ describe("AutoDiscovery", function () { // 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(), @@ -494,10 +480,6 @@ describe("AutoDiscovery", function () { error: null, base_url: null, }, - "m.authentication": { - state: "FAIL_ERROR", - error: OidcError.Misconfigured, - }, }; expect(conf).toEqual(expected); @@ -728,10 +710,6 @@ describe("AutoDiscovery", function () { error: null, base_url: "https://identity.example.org", }, - "m.authentication": { - state: "IGNORE", - error: OidcError.NotSupported, - }, }; expect(conf).toEqual(expected); @@ -784,10 +762,6 @@ describe("AutoDiscovery", function () { "org.example.custom.property": { cupcakes: "yes", }, - "m.authentication": { - state: "IGNORE", - error: OidcError.NotSupported, - }, }; expect(conf).toEqual(expected); @@ -897,75 +871,4 @@ describe("AutoDiscovery", function () { }), ]); }); - - describe("m.authentication", () => { - const homeserverName = "example.org"; - const homeserverUrl = "https://chat.example.org/"; - const issuer = "https://auth.org/"; - - beforeAll(() => { - // make these tests independent from fetch mocking above - AutoDiscovery.setFetchFn(realAutoDiscoveryFetch); - }); - - beforeEach(() => { - fetchMock.resetBehavior(); - fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.5"] }); - - fetchMock.get("https://example.org/.well-known/matrix/client", { - "m.homeserver": { - // Note: we also expect this test to trim the trailing slash - base_url: "https://chat.example.org/", - }, - "m.authentication": { - issuer, - }, - }); - }); - - it("should return valid authentication configuration", async () => { - const config = makeDelegatedAuthConfig(issuer); - - fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata); - fetchMock.get(`${config.metadata.issuer}jwks`, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - keys: [], - }); - - const result = await AutoDiscovery.findClientConfig(homeserverName); - - expect(result[M_AUTHENTICATION.stable!]).toEqual({ - state: AutoDiscovery.SUCCESS, - ...config, - signingKeys: [], - account: undefined, - error: null, - }); - }); - - it("should set state to error for invalid authentication configuration", async () => { - const config = makeDelegatedAuthConfig(issuer); - // authorization_code is required - config.metadata.grant_types_supported = ["openid"]; - - fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata); - fetchMock.get(`${config.metadata.issuer}jwks`, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - keys: [], - }); - - const result = await AutoDiscovery.findClientConfig(homeserverName); - - expect(result[M_AUTHENTICATION.stable!]).toEqual({ - state: AutoDiscovery.FAIL_ERROR, - error: OidcError.OpSupport, - }); - }); - }); }); diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 9bf6cdafa81..29dac54bdb7 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -15,19 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SigningKey } from "oidc-client-ts"; - -import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client"; +import { IClientWellKnown, IWellKnownConfig, IServerVersions } from "./client"; import { logger } from "./logger"; import { MatrixError, Method, timeoutSignal } from "./http-api"; -import { discoverAndValidateAuthenticationConfig } from "./oidc/discovery"; -import { - ValidatedIssuerConfig, - ValidatedIssuerMetadata, - validateOIDCIssuerWellKnown, - validateWellKnownAuthentication, -} from "./oidc/validate"; -import { OidcError } from "./oidc/error"; import { SUPPORTED_MATRIX_VERSIONS } from "./version-support"; // Dev note: Auto discovery is part of the spec. @@ -65,26 +55,9 @@ interface AutoDiscoveryState { } interface WellKnownConfig extends Omit, AutoDiscoveryState {} -/** - * @deprecated in favour of OidcClientConfig - */ -interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {} - -/** - * @experimental - */ -export interface OidcClientConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig { - metadata: ValidatedIssuerMetadata; - signingKeys?: SigningKey[]; -} - export interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; - /** - * @experimental - */ - "m.authentication"?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState; } /** @@ -318,107 +291,10 @@ export class AutoDiscovery { } }); - const authConfig = await this.discoverAndValidateAuthenticationConfig(wellknown); - clientConfig[M_AUTHENTICATION.stable!] = authConfig; - // Step 8: Give the config to the caller (finally) return Promise.resolve(clientConfig); } - /** - * Validate delegated auth configuration - * @deprecated use discoverAndValidateAuthenticationConfig - * - 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 { - try { - const authentication = M_AUTHENTICATION.findIn(wellKnown) || undefined; - const homeserverAuthenticationConfig = validateWellKnownAuthentication(authentication); - - const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl( - homeserverAuthenticationConfig.issuer, - )}/.well-known/openid-configuration`; - const issuerWellKnown = await this.fetchWellKnownObject(issuerOpenIdConfigUrl); - - if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) { - logger.error("Failed to fetch issuer openid configuration"); - throw new Error(OidcError.General); - } - - const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw); - - const delegatedAuthConfig: DelegatedAuthConfig = { - state: AutoDiscoveryAction.SUCCESS, - error: null, - ...homeserverAuthenticationConfig, - ...validatedIssuerConfig, - }; - return delegatedAuthConfig; - } catch (error) { - const errorMessage = (error as Error).message as unknown as OidcError; - const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General; - - const state = - errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR; - - return { - state, - error: errorType, - }; - } - } - - /** - * 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, validated authentication metadata and optionally signing keys will be returned - * 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 discoverAndValidateAuthenticationConfig( - wellKnown: IClientWellKnown, - ): Promise<(OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState> { - try { - const authentication = M_AUTHENTICATION.findIn(wellKnown) || undefined; - const result = await discoverAndValidateAuthenticationConfig(authentication); - - // include this for backwards compatibility - const validatedIssuerConfig = validateOIDCIssuerWellKnown(result.metadata); - - const response = { - state: AutoDiscoveryAction.SUCCESS, - error: null, - ...validatedIssuerConfig, - ...result, - }; - return response; - } catch (error) { - const errorMessage = (error as Error).message as unknown as OidcError; - const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General; - - const state = - errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR; - - return { - state, - error: errorType, - }; - } - } - /** * Attempts to automatically discover client configuration information * prior to logging in. Such information includes the homeserver URL diff --git a/src/client.ts b/src/client.ts index 15fc1a99429..654573ed064 100644 --- a/src/client.ts +++ b/src/client.ts @@ -625,13 +625,10 @@ export interface IServerVersions { unstable_features: Record; } -export const M_AUTHENTICATION = new UnstableValue("m.authentication", "org.matrix.msc2965.authentication"); - export interface IClientWellKnown { [key: string]: any; "m.homeserver"?: IWellKnownConfig; "m.identity_server"?: IWellKnownConfig; - [M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965 } export interface IWellKnownConfig { From bae525f81f8be2ce1d4aaff61f5cd98fbbeee05e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Feb 2024 13:02:55 +0000 Subject: [PATCH 02/10] Read account management endpoint from the OIDC issuer well-known Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/test-utils/oidc.ts | 6 ++---- src/oidc/validate.ts | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/test-utils/oidc.ts b/spec/test-utils/oidc.ts index 0bd97442daf..7b2adc226d7 100644 --- a/spec/test-utils/oidc.ts +++ b/spec/test-utils/oidc.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { OidcClientConfig } from "../../src"; -import { ValidatedIssuerMetadata } from "../../src/oidc/validate"; +import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src"; /** * Makes a valid OidcClientConfig with minimum valid values @@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien const metadata = mockOpenIdConfiguration(issuer); return { - issuer, - account: issuer + "account", + accountManagementEndpoint: issuer + "account", registrationEndpoint: metadata.registration_endpoint, authorizationEndpoint: metadata.authorization_endpoint, tokenEndpoint: metadata.token_endpoint, diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index ef3e7c26e28..0fe8c0330cb 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -31,6 +31,7 @@ export type ValidatedIssuerConfig = { authorizationEndpoint: string; tokenEndpoint: string; registrationEndpoint?: string; + accountManagementEndpoint?: string; }; /** @@ -102,6 +103,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer requiredStringProperty(wellKnown, "token_endpoint"), requiredStringProperty(wellKnown, "revocation_endpoint"), optionalStringProperty(wellKnown, "registration_endpoint"), + optionalStringProperty(wellKnown, "account_management_uri"), requiredArrayValue(wellKnown, "response_types_supported", "code"), requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"), requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"), @@ -112,6 +114,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer authorizationEndpoint: wellKnown["authorization_endpoint"], tokenEndpoint: wellKnown["token_endpoint"], registrationEndpoint: wellKnown["registration_endpoint"], + accountManagementEndpoint: wellKnown["account_management_uri"], } as ValidatedIssuerConfig; } From 0b07c2cd2b64d78b8f10bab38aaed0959870344b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Feb 2024 14:18:38 +0000 Subject: [PATCH 03/10] Change OIDC API methods shape to no longer rely on defunct well-known response Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/oidc/authorize.spec.ts | 5 +- spec/unit/oidc/tokenRefresher.spec.ts | 16 ++--- spec/unit/oidc/validate.spec.ts | 95 +-------------------------- src/client.ts | 8 --- src/oidc/authorize.ts | 3 +- src/oidc/discovery.ts | 33 ++++++---- src/oidc/index.ts | 8 +++ src/oidc/register.ts | 5 +- src/oidc/tokenRefresher.ts | 13 ++-- src/oidc/validate.ts | 26 -------- 10 files changed, 51 insertions(+), 161 deletions(-) diff --git a/spec/unit/oidc/authorize.spec.ts b/spec/unit/oidc/authorize.spec.ts index 732d407ebc3..f35d4b3cbce 100644 --- a/spec/unit/oidc/authorize.spec.ts +++ b/spec/unit/oidc/authorize.spec.ts @@ -53,7 +53,10 @@ describe("oidc authorization", () => { jest.spyOn(logger, "warn"); jest.setSystemTime(now); - fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration()); + fetchMock.get( + delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration", + mockOpenIdConfiguration(), + ); }); afterEach(() => { diff --git a/spec/unit/oidc/tokenRefresher.spec.ts b/spec/unit/oidc/tokenRefresher.spec.ts index 803a63d9aac..06d745865d2 100644 --- a/spec/unit/oidc/tokenRefresher.spec.ts +++ b/spec/unit/oidc/tokenRefresher.spec.ts @@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => { }, { overwriteRoutes: true }, ); - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await expect(refresher.oidcClientReady).rejects.toThrow(); expect(logger.error).toHaveBeenCalledWith( "Failed to initialise OIDC client.", @@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => { }); it("initialises oidc client", async () => { - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await refresher.oidcClientReady; // @ts-ignore peek at private property to see we initialised the client correctly @@ -114,14 +114,14 @@ describe("OidcTokenRefresher", () => { describe("doRefreshAccessToken()", () => { it("should throw when oidcClient has not been initialised", async () => { - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow( "Cannot get new token before OIDC client is initialised.", ); }); it("should refresh the tokens", async () => { - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await refresher.oidcClientReady; const result = await refresher.doRefreshAccessToken("refresh-token"); @@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => { }); it("should persist the new tokens", async () => { - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await refresher.oidcClientReady; // spy on our stub jest.spyOn(refresher, "persistTokens"); @@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => { { overwriteRoutes: false }, ); - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await refresher.oidcClientReady; // reset call counts fetchMock.resetHistory(); @@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => { { overwriteRoutes: true }, ); - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await refresher.oidcClientReady; await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow(); @@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => { { overwriteRoutes: false }, ); - const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); await refresher.oidcClientReady; // reset call counts fetchMock.resetHistory(); diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index 91b6985f02c..44495c77940 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -17,105 +17,12 @@ limitations under the License. import { mocked } from "jest-mock"; import jwtDecode from "jwt-decode"; -import { M_AUTHENTICATION } from "../../../src"; import { logger } from "../../../src/logger"; -import { - validateIdToken, - validateOIDCIssuerWellKnown, - validateWellKnownAuthentication, -} from "../../../src/oidc/validate"; +import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate"; import { OidcError } from "../../../src/oidc/error"; jest.mock("jwt-decode"); -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(undefined)).toThrow(OidcError.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[M_AUTHENTICATION.stable!] as any)).toThrow( - OidcError.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[M_AUTHENTICATION.stable!] as any)).toThrow( - OidcError.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[M_AUTHENTICATION.stable!] as any)).toThrow( - OidcError.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[M_AUTHENTICATION.stable!])).toEqual({ - issuer: "test.com", - account: "account.com", - }); - }); - - it("should return valid config when m.authentication account is missing", () => { - const wk = { - ...baseWk, - [M_AUTHENTICATION.stable!]: { - issuer: "test.com", - }, - }; - expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({ - issuer: "test.com", - }); - }); - - it("should remove unexpected properties", () => { - const wk = { - ...baseWk, - [M_AUTHENTICATION.stable!]: { - issuer: "test.com", - somethingElse: "test", - }, - }; - expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({ - issuer: "test.com", - }); - }); -}); - describe("validateOIDCIssuerWellKnown", () => { const validWk: any = { authorization_endpoint: "https://test.org/authorize", diff --git a/src/client.ts b/src/client.ts index 654573ed064..3b489ad52db 100644 --- a/src/client.ts +++ b/src/client.ts @@ -642,14 +642,6 @@ export interface IWellKnownConfig { server_name?: string; } -export interface IDelegatedAuthConfig { - // MSC2965 - /** The OIDC Provider/issuer the client should use */ - issuer: string; - /** The optional URL of the web UI where the user can manage their account */ - account?: string; -} - interface IKeyBackupPath { path: string; queryData?: { diff --git a/src/oidc/authorize.ts b/src/oidc/authorize.ts index 557a5c7e42e..1c9985a5b83 100644 --- a/src/oidc/authorize.ts +++ b/src/oidc/authorize.ts @@ -16,7 +16,6 @@ limitations under the License. import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts"; -import { IDelegatedAuthConfig } from "../client"; import { subtleCrypto, TextEncoder } from "../crypto/crypto"; import { logger } from "../logger"; import { randomString } from "../randomstring"; @@ -205,7 +204,7 @@ export const completeAuthorizationCodeGrant = async ( code: string, state: string, ): Promise<{ - oidcClientSettings: IDelegatedAuthConfig & { clientId: string }; + oidcClientSettings: { clientId: string; issuer: string }; tokenResponse: BearerTokenResponse; homeserverUrl: string; idTokenClaims: IdTokenClaims; diff --git a/src/oidc/discovery.ts b/src/oidc/discovery.ts index 76aaeea8054..1a08ed7fc1f 100644 --- a/src/oidc/discovery.ts +++ b/src/oidc/discovery.ts @@ -16,34 +16,43 @@ limitations under the License. import { MetadataService, OidcClientSettingsStore, SigningKey } from "oidc-client-ts"; -import { IDelegatedAuthConfig } from "../client"; -import { isValidatedIssuerMetadata, ValidatedIssuerMetadata, validateWellKnownAuthentication } from "./validate"; +import { + isValidatedIssuerMetadata, + ValidatedIssuerConfig, + ValidatedIssuerMetadata, + validateOIDCIssuerWellKnown, +} from "./validate"; +import { Method, timeoutSignal } from "../http-api"; /** * @experimental * Discover and 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, validated metadata is returned - * @param wellKnown - configuration object as returned - * by the .well-known auto-discovery endpoint + * @param issuer - the OIDC issuer as returned by the /auth_issuer API * @returns validated authentication metadata and optionally signing keys * @throws when delegated auth config is invalid or unreachable */ -export const discoverAndValidateAuthenticationConfig = async ( - authenticationConfig?: IDelegatedAuthConfig, +export const discoverAndValidateOIDCIssuerWellKnown = async ( + issuer: string, ): Promise< - IDelegatedAuthConfig & { + ValidatedIssuerConfig & { metadata: ValidatedIssuerMetadata; signingKeys?: SigningKey[]; } > => { - const homeserverAuthenticationConfig = validateWellKnownAuthentication(authenticationConfig); + const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer); + const issuerWellKnownResponse = await fetch(issuerOpenIdConfigUrl, { + method: Method.Get, + signal: timeoutSignal(5000), + }); + const issuerWellKnown = await issuerWellKnownResponse.json(); + const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown); - // create a temporary settings store so we can use metadata service for discovery + // create a temporary settings store, so we can use metadata service for discovery const settings = new OidcClientSettingsStore({ - authority: homeserverAuthenticationConfig.issuer, + authority: issuer, redirect_uri: "", // Not known yet, this is here to make the type checker happy client_id: "", // Not known yet, this is here to make the type checker happy }); @@ -54,7 +63,7 @@ export const discoverAndValidateAuthenticationConfig = async ( isValidatedIssuerMetadata(metadata); return { - ...homeserverAuthenticationConfig, + ...validatedIssuerConfig, metadata, signingKeys, }; diff --git a/src/oidc/index.ts b/src/oidc/index.ts index 7c15d2ce954..cc79a5e88e6 100644 --- a/src/oidc/index.ts +++ b/src/oidc/index.ts @@ -14,9 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type { SigningKey } from "oidc-client-ts"; +import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate"; + export * from "./authorize"; export * from "./discovery"; export * from "./error"; export * from "./register"; export * from "./tokenRefresher"; export * from "./validate"; + +export interface OidcClientConfig extends ValidatedIssuerConfig { + metadata: ValidatedIssuerMetadata; + signingKeys?: SigningKey[]; +} diff --git a/src/oidc/register.ts b/src/oidc/register.ts index 65add493510..01ae4942615 100644 --- a/src/oidc/register.ts +++ b/src/oidc/register.ts @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IDelegatedAuthConfig } from "../client"; +import { OidcClientConfig } from "."; import { OidcError } from "./error"; import { Method } from "../http-api"; import { logger } from "../logger"; -import { ValidatedIssuerConfig } from "./validate"; import { NonEmptyArray } from "../@types/common"; /** @@ -118,7 +117,7 @@ const doRegistration = async ( * @throws when registration is not supported, on failed request or invalid response */ export const registerOidcClient = async ( - delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig, + delegatedAuthConfig: Omit & { issuer: string }, clientMetadata: OidcRegistrationClientMetadata, ): Promise => { if (!delegatedAuthConfig.registrationEndpoint) { diff --git a/src/oidc/tokenRefresher.ts b/src/oidc/tokenRefresher.ts index 10c9bc48ea3..79ef9442276 100644 --- a/src/oidc/tokenRefresher.ts +++ b/src/oidc/tokenRefresher.ts @@ -17,9 +17,8 @@ limitations under the License. import { IdTokenClaims, OidcClient, WebStorageStateStore } from "oidc-client-ts"; import { AccessTokens } from "../http-api"; -import { IDelegatedAuthConfig } from "../client"; import { generateScope } from "./authorize"; -import { discoverAndValidateAuthenticationConfig } from "./discovery"; +import { discoverAndValidateOIDCIssuerWellKnown } from "./discovery"; import { logger } from "../logger"; /** @@ -42,9 +41,9 @@ export class OidcTokenRefresher { public constructor( /** - * Delegated auth config as found in matrix client .well-known + * The OIDC issuer as returned by the /auth_issuer API */ - authConfig: IDelegatedAuthConfig, + issuer: string, /** * id of this client as registered with the OP */ @@ -63,17 +62,17 @@ export class OidcTokenRefresher { */ private readonly idTokenClaims: IdTokenClaims, ) { - this.oidcClientReady = this.initialiseOidcClient(authConfig, clientId, deviceId, redirectUri); + this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri); } private async initialiseOidcClient( - authConfig: IDelegatedAuthConfig, + issuer: string, clientId: string, deviceId: string, redirectUri: string, ): Promise { try { - const config = await discoverAndValidateAuthenticationConfig(authConfig); + const config = await discoverAndValidateOIDCIssuerWellKnown(issuer); const scope = generateScope(deviceId); diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 0fe8c0330cb..5dd40546ad1 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -17,7 +17,6 @@ limitations under the License. import jwtDecode from "jwt-decode"; import { OidcMetadata, SigninResponse } from "oidc-client-ts"; -import { IDelegatedAuthConfig } from "../client"; import { logger } from "../logger"; import { OidcError } from "./error"; @@ -34,31 +33,6 @@ export type ValidatedIssuerConfig = { accountManagementEndpoint?: 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 = (authentication?: IDelegatedAuthConfig): IDelegatedAuthConfig => { - if (!authentication) { - throw new Error(OidcError.NotSupported); - } - - if ( - typeof authentication.issuer === "string" && - (!authentication.hasOwnProperty("account") || typeof authentication.account === "string") - ) { - return { - issuer: authentication.issuer, - account: authentication.account, - }; - } - - throw new Error(OidcError.Misconfigured); -}; - const isRecord = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value); const requiredStringProperty = (wellKnown: Record, key: string): boolean => { From 2f812c4816c84470aa1ed0599bb55c1935221d4e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Feb 2024 11:35:40 +0000 Subject: [PATCH 04/10] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/oidc/authorize.spec.ts | 4 ++-- spec/unit/oidc/tokenRefresher.spec.ts | 16 ++++++++-------- src/oidc/discovery.ts | 19 ++++--------------- src/oidc/index.ts | 4 ++++ 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/spec/unit/oidc/authorize.spec.ts b/spec/unit/oidc/authorize.spec.ts index a2d5dc1276e..e9cf7589a37 100644 --- a/spec/unit/oidc/authorize.spec.ts +++ b/spec/unit/oidc/authorize.spec.ts @@ -46,8 +46,8 @@ const realSubtleCrypto = crypto.subtleCrypto; describe("oidc authorization", () => { const delegatedAuthConfig = makeDelegatedAuthConfig(); - const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint; - const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint; + const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint; + const tokenEndpoint = delegatedAuthConfig.tokenEndpoint; const clientId = "xyz789"; const baseUrl = "https://test.com"; diff --git a/spec/unit/oidc/tokenRefresher.spec.ts b/spec/unit/oidc/tokenRefresher.spec.ts index 06d745865d2..e291142b868 100644 --- a/spec/unit/oidc/tokenRefresher.spec.ts +++ b/spec/unit/oidc/tokenRefresher.spec.ts @@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => { keys: [], }); - fetchMock.post(config.metadata.token_endpoint, { + fetchMock.post(config.tokenEndpoint, { status: 200, headers: { "Content-Type": "application/json", @@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => { const result = await refresher.doRefreshAccessToken("refresh-token"); - expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, { + expect(fetchMock).toHaveFetched(config.tokenEndpoint, { method: "POST", }); @@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => { it("should only have one inflight refresh request at once", async () => { fetchMock .postOnce( - config.metadata.token_endpoint, + config.tokenEndpoint, { status: 200, headers: { @@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => { { overwriteRoutes: true }, ) .postOnce( - config.metadata.token_endpoint, + config.tokenEndpoint, { status: 200, headers: { @@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => { const result2 = await first; // only one call to token endpoint - expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint); + expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint); expect(result1).toEqual({ accessToken: "first-new-access-token", refreshToken: "first-new-refresh-token", @@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => { it("should log and rethrow when token refresh fails", async () => { fetchMock.post( - config.metadata.token_endpoint, + config.tokenEndpoint, { status: 503, headers: { @@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => { // make sure inflight request is cleared after a failure fetchMock .postOnce( - config.metadata.token_endpoint, + config.tokenEndpoint, { status: 503, headers: { @@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => { { overwriteRoutes: true }, ) .postOnce( - config.metadata.token_endpoint, + config.tokenEndpoint, { status: 200, headers: { diff --git a/src/oidc/discovery.ts b/src/oidc/discovery.ts index 1a08ed7fc1f..d8a81041a36 100644 --- a/src/oidc/discovery.ts +++ b/src/oidc/discovery.ts @@ -14,15 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MetadataService, OidcClientSettingsStore, SigningKey } from "oidc-client-ts"; +import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts"; -import { - isValidatedIssuerMetadata, - ValidatedIssuerConfig, - ValidatedIssuerMetadata, - validateOIDCIssuerWellKnown, -} from "./validate"; +import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate"; import { Method, timeoutSignal } from "../http-api"; +import { OidcClientConfig } from "./index"; /** * @experimental @@ -34,14 +30,7 @@ import { Method, timeoutSignal } from "../http-api"; * @returns validated authentication metadata and optionally signing keys * @throws when delegated auth config is invalid or unreachable */ -export const discoverAndValidateOIDCIssuerWellKnown = async ( - issuer: string, -): Promise< - ValidatedIssuerConfig & { - metadata: ValidatedIssuerMetadata; - signingKeys?: SigningKey[]; - } -> => { +export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise => { const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer); const issuerWellKnownResponse = await fetch(issuerOpenIdConfigUrl, { method: Method.Get, diff --git a/src/oidc/index.ts b/src/oidc/index.ts index cc79a5e88e6..d3cd0b9f386 100644 --- a/src/oidc/index.ts +++ b/src/oidc/index.ts @@ -24,6 +24,10 @@ export * from "./register"; export * from "./tokenRefresher"; export * from "./validate"; +/** + * Validated config for native OIDC authentication + * Contains metadata and signing keys from the issuer's well-known. + */ export interface OidcClientConfig extends ValidatedIssuerConfig { metadata: ValidatedIssuerMetadata; signingKeys?: SigningKey[]; From 0518006f8fcf5b256db6d6cf0339509a517e4bd5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Feb 2024 11:59:33 +0000 Subject: [PATCH 05/10] Add account_management_actions_supported Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/oidc/validate.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 962cf98336d..c5ed1f09830 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -31,6 +31,7 @@ export type ValidatedIssuerConfig = { tokenEndpoint: string; registrationEndpoint?: string; accountManagementEndpoint?: string; + accountManagementActionsSupported?: string[]; }; const isRecord = (value: unknown): value is Record => @@ -49,6 +50,16 @@ const optionalStringProperty = (wellKnown: Record, key: string) } return true; }; +const optionalStringArrayProperty = (wellKnown: Record, key: string): boolean => { + if ( + !!wellKnown[key] && + (!Array.isArray(wellKnown[key]) || !(wellKnown[key]).every((v) => typeof v === "string")) + ) { + logger.error(`Invalid property: ${key}`); + return false; + } + return true; +}; const requiredArrayValue = (wellKnown: Record, key: string, value: any): boolean => { const array = wellKnown[key]; if (!array || !Array.isArray(array) || !array.includes(value)) { @@ -78,6 +89,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer requiredStringProperty(wellKnown, "revocation_endpoint"), optionalStringProperty(wellKnown, "registration_endpoint"), optionalStringProperty(wellKnown, "account_management_uri"), + optionalStringArrayProperty(wellKnown, "account_management_actions_supported"), requiredArrayValue(wellKnown, "response_types_supported", "code"), requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"), requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"), @@ -85,11 +97,12 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer if (!isInvalid) { return { - authorizationEndpoint: wellKnown["authorization_endpoint"], - tokenEndpoint: wellKnown["token_endpoint"], - registrationEndpoint: wellKnown["registration_endpoint"], - accountManagementEndpoint: wellKnown["account_management_uri"], - } as ValidatedIssuerConfig; + authorizationEndpoint: wellKnown["authorization_endpoint"], + tokenEndpoint: wellKnown["token_endpoint"], + registrationEndpoint: wellKnown["registration_endpoint"], + accountManagementEndpoint: wellKnown["account_management_uri"], + accountManagementActionsSupported: wellKnown["account_management_actions_supported"], + }; } logger.error("Issuer configuration not valid"); From a4b9572a113e4d479a56cc5ede09377266fa9f4b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Feb 2024 12:13:03 +0000 Subject: [PATCH 06/10] Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/oidc/register.spec.ts | 17 ++++++----------- src/oidc/register.ts | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/spec/unit/oidc/register.spec.ts b/spec/unit/oidc/register.spec.ts index f0e25784137..372f7d677c4 100644 --- a/spec/unit/oidc/register.spec.ts +++ b/spec/unit/oidc/register.spec.ts @@ -18,10 +18,10 @@ import fetchMockJest from "fetch-mock-jest"; import { OidcError } from "../../../src/oidc/error"; import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register"; +import { makeDelegatedAuthConfig } from "../../test-utils/oidc"; describe("registerOidcClient()", () => { const issuer = "https://auth.com/"; - const registrationEndpoint = "https://auth.com/register"; const clientName = "Element"; const baseUrl = "https://just.testing"; const metadata: OidcRegistrationClientMetadata = { @@ -35,25 +35,20 @@ describe("registerOidcClient()", () => { }; const dynamicClientId = "xyz789"; - const delegatedAuthConfig = { - issuer, - registrationEndpoint, - authorizationEndpoint: issuer + "auth", - tokenEndpoint: issuer + "token", - }; + const delegatedAuthConfig = makeDelegatedAuthConfig(issuer); beforeEach(() => { fetchMockJest.mockClear(); fetchMockJest.resetBehavior(); }); it("should make correct request to register client", async () => { - fetchMockJest.post(registrationEndpoint, { + fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { status: 200, body: JSON.stringify({ client_id: dynamicClientId }), }); expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId); expect(fetchMockJest).toHaveBeenCalledWith( - registrationEndpoint, + delegatedAuthConfig.registrationEndpoint!, expect.objectContaining({ headers: { "Accept": "application/json", @@ -77,7 +72,7 @@ describe("registerOidcClient()", () => { }); it("should throw when registration request fails", async () => { - fetchMockJest.post(registrationEndpoint, { + fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { status: 500, }); await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow( @@ -86,7 +81,7 @@ describe("registerOidcClient()", () => { }); it("should throw when registration response is invalid", async () => { - fetchMockJest.post(registrationEndpoint, { + fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { status: 200, // no clientId in response body: "{}", diff --git a/src/oidc/register.ts b/src/oidc/register.ts index 01ae4942615..8018ef009ad 100644 --- a/src/oidc/register.ts +++ b/src/oidc/register.ts @@ -117,7 +117,7 @@ const doRegistration = async ( * @throws when registration is not supported, on failed request or invalid response */ export const registerOidcClient = async ( - delegatedAuthConfig: Omit & { issuer: string }, + delegatedAuthConfig: OidcClientConfig, clientMetadata: OidcRegistrationClientMetadata, ): Promise => { if (!delegatedAuthConfig.registrationEndpoint) { From 9baef5e9bac82f84592b7e1880900768b6d6e491 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Feb 2024 13:08:03 +0000 Subject: [PATCH 07/10] Validate `account_management_uri` and `account_management_actions_supported` from OIDC Issuer well-known Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/oidc/validate.spec.ts | 8 ++++++++ src/oidc/validate.ts | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index e1f85fa0808..a3fde6ee46a 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -125,6 +125,8 @@ describe("validateOIDCIssuerWellKnown", () => { response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], + account_management_uri: "https://authorize.org/account", + account_management_actions_supported: ["org.matrix.cross_signing_reset"], }; beforeEach(() => { // stub to avoid console litter @@ -157,6 +159,8 @@ describe("validateOIDCIssuerWellKnown", () => { authorizationEndpoint: validWk.authorization_endpoint, tokenEndpoint: validWk.token_endpoint, registrationEndpoint: validWk.registration_endpoint, + accountManagementActionsSupported: ["org.matrix.cross_signing_reset"], + accountManagementEndpoint: "https://authorize.org/account", }); }); @@ -167,6 +171,8 @@ describe("validateOIDCIssuerWellKnown", () => { authorizationEndpoint: validWk.authorization_endpoint, tokenEndpoint: validWk.token_endpoint, registrationEndpoint: undefined, + accountManagementActionsSupported: ["org.matrix.cross_signing_reset"], + accountManagementEndpoint: "https://authorize.org/account", }); }); @@ -186,6 +192,8 @@ describe("validateOIDCIssuerWellKnown", () => { ["code_challenge_methods_supported", undefined], ["code_challenge_methods_supported", "not an array"], ["code_challenge_methods_supported", ["doesnt include S256"]], + ["account_management_uri", { not: "a string" }], + ["account_management_actions_supported", { not: "an array" }], ])("should throw OP support error when %s is %s", (key, value) => { const wk = { ...validWk, diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index a0d062e472d..c806ca80777 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -31,6 +31,8 @@ export type ValidatedIssuerConfig = { authorizationEndpoint: string; tokenEndpoint: string; registrationEndpoint?: string; + accountManagementEndpoint?: string; + accountManagementActionsSupported?: string[]; }; /** @@ -74,6 +76,16 @@ const optionalStringProperty = (wellKnown: Record, key: string) } return true; }; +const optionalStringArrayProperty = (wellKnown: Record, key: string): boolean => { + if ( + !!wellKnown[key] && + (!Array.isArray(wellKnown[key]) || !(wellKnown[key]).every((v) => typeof v === "string")) + ) { + logger.error(`Invalid property: ${key}`); + return false; + } + return true; +}; const requiredArrayValue = (wellKnown: Record, key: string, value: any): boolean => { const array = wellKnown[key]; if (!array || !Array.isArray(array) || !array.includes(value)) { @@ -102,6 +114,8 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer requiredStringProperty(wellKnown, "token_endpoint"), requiredStringProperty(wellKnown, "revocation_endpoint"), optionalStringProperty(wellKnown, "registration_endpoint"), + optionalStringProperty(wellKnown, "account_management_uri"), + optionalStringArrayProperty(wellKnown, "account_management_actions_supported"), requiredArrayValue(wellKnown, "response_types_supported", "code"), requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"), requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"), @@ -109,10 +123,12 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer if (!isInvalid) { return { - authorizationEndpoint: wellKnown["authorization_endpoint"], - tokenEndpoint: wellKnown["token_endpoint"], - registrationEndpoint: wellKnown["registration_endpoint"], - } as ValidatedIssuerConfig; + authorizationEndpoint: wellKnown["authorization_endpoint"], + tokenEndpoint: wellKnown["token_endpoint"], + registrationEndpoint: wellKnown["registration_endpoint"], + accountManagementEndpoint: wellKnown["account_management_uri"], + accountManagementActionsSupported: wellKnown["account_management_actions_supported"], + }; } logger.error("Issuer configuration not valid"); From b91263ba0392508c59382309c6930903d11aa83d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Feb 2024 17:40:59 +0000 Subject: [PATCH 08/10] Fix comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/oidc/register.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oidc/register.ts b/src/oidc/register.ts index 8018ef009ad..6e4948f5065 100644 --- a/src/oidc/register.ts +++ b/src/oidc/register.ts @@ -111,7 +111,7 @@ const doRegistration = async ( /** * Attempts dynamic registration against the configured registration endpoint - * @param delegatedAuthConfig - Auth config from ValidatedServerConfig + * @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 From 664108de837e8c40cc2fd0cae3a56129ce4ecbc0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Feb 2024 09:34:42 +0000 Subject: [PATCH 09/10] Iterate comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/oidc/discovery.ts | 1 + src/oidc/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/oidc/discovery.ts b/src/oidc/discovery.ts index d8a81041a36..7199c8715d4 100644 --- a/src/oidc/discovery.ts +++ b/src/oidc/discovery.ts @@ -25,6 +25,7 @@ import { OidcClientConfig } from "./index"; * Discover and validate delegated auth configuration * - delegated auth issuer openid-configuration is reachable * - delegated auth issuer openid-configuration is configured correctly for us + * Fetches https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein. * When successful, validated metadata is returned * @param issuer - the OIDC issuer as returned by the /auth_issuer API * @returns validated authentication metadata and optionally signing keys diff --git a/src/oidc/index.ts b/src/oidc/index.ts index d3cd0b9f386..7fc31836f83 100644 --- a/src/oidc/index.ts +++ b/src/oidc/index.ts @@ -25,8 +25,8 @@ export * from "./tokenRefresher"; export * from "./validate"; /** - * Validated config for native OIDC authentication - * Contains metadata and signing keys from the issuer's well-known. + * Validated config for native OIDC authentication, as returned by {@link discoverAndValidateOIDCIssuerWellKnown}. + * Contains metadata and signing keys from the issuer's well-known (https://oidc-issuer.example.com/.well-known/openid-configuration). */ export interface OidcClientConfig extends ValidatedIssuerConfig { metadata: ValidatedIssuerMetadata; From 5c6240b19974a0cf1e6e7f24c14ac3b266ecaaf4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Feb 2024 13:52:06 +0000 Subject: [PATCH 10/10] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/oidc/validate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index c5ed1f09830..38d730ba45d 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -124,7 +124,11 @@ export type ValidatedIssuerMetadata = Partial & | "response_types_supported" | "grant_types_supported" | "code_challenge_methods_supported" - >; + > & { + // MSC2965 extensions to the OIDC spec + account_management_uri?: string; + account_management_actions_supported?: string[]; + }; /** * Wraps validateOIDCIssuerWellKnown in a type assertion