You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-23 17:02:25 +03:00
Switch OIDC primarily to new /auth_metadata API (#4626)
This commit is contained in:
committed by
GitHub
parent
61375ef38a
commit
c0e30ceca0
@@ -23,7 +23,7 @@ import {
|
||||
BearerTokenResponse,
|
||||
UserState,
|
||||
validateBearerTokenResponse,
|
||||
ValidatedIssuerMetadata,
|
||||
ValidatedAuthMetadata,
|
||||
validateIdToken,
|
||||
validateStoredUserState,
|
||||
} from "./validate.ts";
|
||||
@@ -138,7 +138,7 @@ export const generateOidcAuthorizationUrl = async ({
|
||||
urlState,
|
||||
}: {
|
||||
clientId: string;
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
metadata: ValidatedAuthMetadata;
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
redirectUri: string;
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
|
||||
|
||||
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate.ts";
|
||||
import { validateAuthMetadata } from "./validate.ts";
|
||||
import { Method, timeoutSignal } from "../http-api/index.ts";
|
||||
import { OidcClientConfig } from "./index.ts";
|
||||
|
||||
@@ -30,6 +30,7 @@ import { OidcClientConfig } from "./index.ts";
|
||||
* @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
|
||||
* @deprecated in favour of {@link MatrixClient#getAuthMetadata}
|
||||
*/
|
||||
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
|
||||
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
|
||||
@@ -38,23 +39,29 @@ export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Pr
|
||||
signal: timeoutSignal(5000),
|
||||
});
|
||||
const issuerWellKnown = await issuerWellKnownResponse.json();
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
|
||||
return validateAuthMetadataAndKeys(issuerWellKnown);
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Validate the authentication metadata and fetch the signing keys from the jwks_uri in the metadata
|
||||
* @param authMetadata - the authentication metadata to validate
|
||||
* @returns validated authentication metadata and signing keys
|
||||
*/
|
||||
export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promise<OidcClientConfig> => {
|
||||
const validatedIssuerConfig = validateAuthMetadata(authMetadata);
|
||||
|
||||
// create a temporary settings store, so we can use metadata service for discovery
|
||||
const settings = new OidcClientSettingsStore({
|
||||
authority: issuer,
|
||||
authority: validatedIssuerConfig.issuer,
|
||||
metadata: validatedIssuerConfig,
|
||||
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
|
||||
});
|
||||
const metadataService = new MetadataService(settings);
|
||||
const metadata = await metadataService.getMetadata();
|
||||
const signingKeys = (await metadataService.getSigningKeys()) ?? undefined;
|
||||
|
||||
isValidatedIssuerMetadata(metadata);
|
||||
|
||||
return {
|
||||
...validatedIssuerConfig,
|
||||
metadata,
|
||||
signingKeys,
|
||||
signingKeys: await metadataService.getSigningKeys(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { SigningKey } from "oidc-client-ts";
|
||||
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate.ts";
|
||||
import { ValidatedAuthMetadata } from "./validate.ts";
|
||||
|
||||
export * from "./authorize.ts";
|
||||
export * from "./discovery.ts";
|
||||
@@ -28,7 +28,6 @@ export * from "./validate.ts";
|
||||
* 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;
|
||||
signingKeys?: SigningKey[];
|
||||
export interface OidcClientConfig extends ValidatedAuthMetadata {
|
||||
signingKeys: SigningKey[] | null;
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ export const registerOidcClient = async (
|
||||
delegatedAuthConfig: OidcClientConfig,
|
||||
clientMetadata: OidcRegistrationClientMetadata,
|
||||
): Promise<string> => {
|
||||
if (!delegatedAuthConfig.registrationEndpoint) {
|
||||
if (!delegatedAuthConfig.registration_endpoint) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
|
||||
if (grantTypes.some((scope) => !delegatedAuthConfig.metadata.grant_types_supported.includes(scope))) {
|
||||
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const registerOidcClient = async (
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(delegatedAuthConfig.registrationEndpoint, {
|
||||
const response = await fetch(delegatedAuthConfig.registration_endpoint, {
|
||||
method: Method.Post,
|
||||
headers,
|
||||
body: JSON.stringify(metadata),
|
||||
|
||||
@@ -77,11 +77,12 @@ export class OidcTokenRefresher {
|
||||
const scope = generateScope(deviceId);
|
||||
|
||||
this.oidcClient = new OidcClient({
|
||||
...config.metadata,
|
||||
metadata: config,
|
||||
signingKeys: config.signingKeys ?? undefined,
|
||||
client_id: clientId,
|
||||
scope,
|
||||
redirect_uri: redirectUri,
|
||||
authority: config.metadata.issuer,
|
||||
authority: config.issuer,
|
||||
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -20,13 +20,28 @@ import { IdTokenClaims, OidcMetadata, SigninResponse } from "oidc-client-ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { OidcError } from "./error.ts";
|
||||
|
||||
export type ValidatedIssuerConfig = {
|
||||
authorizationEndpoint: string;
|
||||
tokenEndpoint: string;
|
||||
registrationEndpoint?: string;
|
||||
accountManagementEndpoint?: string;
|
||||
accountManagementActionsSupported?: string[];
|
||||
};
|
||||
/**
|
||||
* Metadata from OIDC authority discovery
|
||||
* With validated properties required in type
|
||||
*/
|
||||
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||
Pick<
|
||||
OidcMetadata,
|
||||
| "issuer"
|
||||
| "authorization_endpoint"
|
||||
| "token_endpoint"
|
||||
| "revocation_endpoint"
|
||||
| "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[];
|
||||
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
|
||||
// even though it is part of the OIDC spec
|
||||
prompt_values_supported?: string[];
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
@@ -67,78 +82,39 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
* Validates issuer `.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
|
||||
* @param authMetadata - 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)) {
|
||||
export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetadata => {
|
||||
if (!isRecord(authMetadata)) {
|
||||
logger.error("Issuer configuration not found or malformed");
|
||||
throw new Error(OidcError.OpSupport);
|
||||
}
|
||||
|
||||
const isInvalid = [
|
||||
requiredStringProperty(wellKnown, "authorization_endpoint"),
|
||||
requiredStringProperty(wellKnown, "token_endpoint"),
|
||||
requiredStringProperty(wellKnown, "revocation_endpoint"),
|
||||
optionalStringProperty(wellKnown, "registration_endpoint"),
|
||||
optionalStringProperty(wellKnown, "account_management_uri"),
|
||||
optionalStringProperty(wellKnown, "device_authorization_endpoint"),
|
||||
optionalStringArrayProperty(wellKnown, "account_management_actions_supported"),
|
||||
requiredArrayValue(wellKnown, "response_types_supported", "code"),
|
||||
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),
|
||||
requiredStringProperty(authMetadata, "issuer"),
|
||||
requiredStringProperty(authMetadata, "authorization_endpoint"),
|
||||
requiredStringProperty(authMetadata, "token_endpoint"),
|
||||
requiredStringProperty(authMetadata, "revocation_endpoint"),
|
||||
optionalStringProperty(authMetadata, "registration_endpoint"),
|
||||
optionalStringProperty(authMetadata, "account_management_uri"),
|
||||
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
|
||||
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
|
||||
requiredArrayValue(authMetadata, "response_types_supported", "code"),
|
||||
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
|
||||
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
|
||||
].some((isValid) => !isValid);
|
||||
|
||||
if (!isInvalid) {
|
||||
return {
|
||||
authorizationEndpoint: <string>wellKnown["authorization_endpoint"],
|
||||
tokenEndpoint: <string>wellKnown["token_endpoint"],
|
||||
registrationEndpoint: <string>wellKnown["registration_endpoint"],
|
||||
accountManagementEndpoint: <string>wellKnown["account_management_uri"],
|
||||
accountManagementActionsSupported: <string[]>wellKnown["account_management_actions_supported"],
|
||||
};
|
||||
return authMetadata as ValidatedAuthMetadata;
|
||||
}
|
||||
|
||||
logger.error("Issuer configuration not valid");
|
||||
throw new Error(OidcError.OpSupport);
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata from OIDC authority discovery
|
||||
* With validated properties required in type
|
||||
*/
|
||||
export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
|
||||
Pick<
|
||||
OidcMetadata,
|
||||
| "issuer"
|
||||
| "authorization_endpoint"
|
||||
| "token_endpoint"
|
||||
| "registration_endpoint"
|
||||
| "revocation_endpoint"
|
||||
| "response_types_supported"
|
||||
| "grant_types_supported"
|
||||
| "code_challenge_methods_supported"
|
||||
| "device_authorization_endpoint"
|
||||
> & {
|
||||
// MSC2965 extensions to the OIDC spec
|
||||
account_management_uri?: string;
|
||||
account_management_actions_supported?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps validateOIDCIssuerWellKnown in a type assertion
|
||||
* that asserts expected properties are present
|
||||
* (Typescript assertions cannot be arrow functions)
|
||||
* @param metadata - issuer openid-configuration response
|
||||
* @throws when metadata validation fails
|
||||
*/
|
||||
export function isValidatedIssuerMetadata(
|
||||
metadata: Partial<OidcMetadata>,
|
||||
): asserts metadata is ValidatedIssuerMetadata {
|
||||
validateOIDCIssuerWellKnown(metadata);
|
||||
}
|
||||
|
||||
export const decodeIdToken = (token: string): IdTokenClaims => {
|
||||
try {
|
||||
return jwtDecode<IdTokenClaims>(token);
|
||||
|
||||
Reference in New Issue
Block a user