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
OIDC: use oidc-client-ts (#3544)
* use oidc-client-ts during oidc discovery * export new type for auth config * deprecate generateAuthorizationUrl in favour of generateOidcAuthorizationUrl * testing util for oidc configurations * test generateOidcAuthorizationUrl * lint * test discovery * dont pass whole client wellknown to oidc validation funcs * add nonce * use client userState for homeserver
This commit is contained in:
@@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { OidcClient, WebStorageStateStore } from "oidc-client-ts";
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { Method } from "../http-api";
|
||||
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
|
||||
import { logger } from "../logger";
|
||||
import { randomString } from "../randomstring";
|
||||
import { OidcError } from "./error";
|
||||
import { validateIdToken, ValidatedIssuerConfig } from "./validate";
|
||||
import { validateIdToken, ValidatedIssuerConfig, ValidatedIssuerMetadata, UserState } from "./validate";
|
||||
|
||||
/**
|
||||
* Authorization parameters which are used in the authentication request of an OIDC auth code flow.
|
||||
@@ -35,6 +37,11 @@ export type AuthorizationParams = {
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Generate the scope used in authorization request with OIDC OP
|
||||
* @returns scope
|
||||
*/
|
||||
const generateScope = (): string => {
|
||||
const deviceId = randomString(10);
|
||||
return `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:${deviceId}`;
|
||||
@@ -74,6 +81,7 @@ export const generateAuthorizationParams = ({ redirectUri }: { redirectUri: stri
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated use generateOidcAuthorizationUrl
|
||||
* Generate a URL to attempt authorization with the OP
|
||||
* See https://openid.net/specs/openid-connect-basic-1_0.html#CodeRequest
|
||||
* @param authorizationUrl - endpoint to attempt authorization with the OP
|
||||
@@ -101,6 +109,49 @@ export const generateAuthorizationUrl = async (
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Generate a URL to attempt authorization with the OP
|
||||
* See https://openid.net/specs/openid-connect-basic-1_0.html#CodeRequest
|
||||
* @param oidcClientSettings - oidc configuration
|
||||
* @param homeserverName - used as state
|
||||
* @returns a Promise with the url as a string
|
||||
*/
|
||||
export const generateOidcAuthorizationUrl = async ({
|
||||
metadata,
|
||||
redirectUri,
|
||||
clientId,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
nonce,
|
||||
}: {
|
||||
clientId: string;
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
redirectUri: string;
|
||||
nonce: string;
|
||||
}): Promise<string> => {
|
||||
const scope = await generateScope();
|
||||
const oidcClient = new OidcClient({
|
||||
...metadata,
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
authority: metadata.issuer,
|
||||
response_mode: "query",
|
||||
response_type: "code",
|
||||
scope,
|
||||
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
||||
});
|
||||
const userState: UserState = { homeserverUrl, nonce, identityServerUrl };
|
||||
const request = await oidcClient.createSigninRequest({
|
||||
state: userState,
|
||||
nonce,
|
||||
});
|
||||
|
||||
return request.url;
|
||||
};
|
||||
|
||||
/**
|
||||
* The expected response type from the token endpoint during authorization code flow
|
||||
* Normalized to always use capitalized 'Bearer' for token_type
|
||||
|
||||
61
src/oidc/discovery.ts
Normal file
61
src/oidc/discovery.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 { MetadataService, OidcClientSettingsStore, SigningKey } from "oidc-client-ts";
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { isValidatedIssuerMetadata, ValidatedIssuerMetadata, validateWellKnownAuthentication } from "./validate";
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @returns validated authentication metadata and optionally signing keys
|
||||
* @throws when delegated auth config is invalid or unreachable
|
||||
*/
|
||||
export const discoverAndValidateAuthenticationConfig = async (
|
||||
authenticationConfig?: IDelegatedAuthConfig,
|
||||
): Promise<
|
||||
IDelegatedAuthConfig & {
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
signingKeys?: SigningKey[];
|
||||
}
|
||||
> => {
|
||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authenticationConfig);
|
||||
|
||||
// create a temporary settings store so we can use metadata service for discovery
|
||||
const settings = new OidcClientSettingsStore({
|
||||
authority: homeserverAuthenticationConfig.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
|
||||
});
|
||||
const metadataService = new MetadataService(settings);
|
||||
const metadata = await metadataService.getMetadata();
|
||||
const signingKeys = (await metadataService.getSigningKeys()) ?? undefined;
|
||||
|
||||
isValidatedIssuerMetadata(metadata);
|
||||
|
||||
return {
|
||||
...homeserverAuthenticationConfig,
|
||||
metadata,
|
||||
signingKeys,
|
||||
};
|
||||
};
|
||||
@@ -15,8 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { OidcMetadata } from "oidc-client-ts";
|
||||
|
||||
import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client";
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { OidcError } from "./error";
|
||||
|
||||
@@ -39,9 +40,7 @@ export type ValidatedIssuerConfig = {
|
||||
* @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<IDelegatedAuthConfig>(wellKnown);
|
||||
|
||||
export const validateWellKnownAuthentication = (authentication?: IDelegatedAuthConfig): IDelegatedAuthConfig => {
|
||||
if (!authentication) {
|
||||
throw new Error(OidcError.NotSupported);
|
||||
}
|
||||
@@ -101,6 +100,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
|
||||
const isInvalid = [
|
||||
requiredStringProperty(wellKnown, "authorization_endpoint"),
|
||||
requiredStringProperty(wellKnown, "token_endpoint"),
|
||||
requiredStringProperty(wellKnown, "revocation_endpoint"),
|
||||
optionalStringProperty(wellKnown, "registration_endpoint"),
|
||||
requiredArrayValue(wellKnown, "response_types_supported", "code"),
|
||||
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
|
||||
@@ -119,6 +119,36 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
|
||||
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"
|
||||
>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard JWT claims.
|
||||
*
|
||||
@@ -199,3 +229,19 @@ export const validateIdToken = (idToken: string | undefined, issuer: string, cli
|
||||
throw new Error(OidcError.InvalidIdToken);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* State we ask OidcClient to store when starting oidc authorization flow (in `generateOidcAuthorizationUrl`)
|
||||
* so that we can access it on return from the OP and complete login
|
||||
*/
|
||||
export type UserState = {
|
||||
/**
|
||||
* Remember which server we were trying to login to
|
||||
*/
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
/**
|
||||
* Used to validate id token
|
||||
*/
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user