Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 328db8f

Browse files
Kerryrichvdh
andauthored
OIDC: Check static client registration and add login flow (#11088)
* util functions to get static client id * check static client ids in login flow * remove dead code * add trailing slash * comment error enum * spacing * PR tidying * more comments * add ValidatedDelegatedAuthConfig type * Update src/Login.ts Co-authored-by: Richard van der Hoff <[email protected]> * Update src/Login.ts Co-authored-by: Richard van der Hoff <[email protected]> * Update src/utils/ValidatedServerConfig.ts Co-authored-by: Richard van der Hoff <[email protected]> * rename oidc_static_clients to oidc_static_client_ids * comment --------- Co-authored-by: Richard van der Hoff <[email protected]>
1 parent 35f8c52 commit 328db8f

File tree

10 files changed

+456
-45
lines changed

10 files changed

+456
-45
lines changed

src/IConfigOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ export interface IConfigOptions {
194194
existing_issues_url: string;
195195
new_issue_url: string;
196196
};
197+
198+
/**
199+
* Configuration for OIDC issuers where a static client_id has been issued for the app.
200+
* Otherwise dynamic client registration is attempted.
201+
* The issuer URL must have a trailing `/`.
202+
* OPTIONAL
203+
*/
204+
oidc_static_client_ids?: Record<string, string>;
197205
}
198206

199207
export interface ISsoRedirectOptions {

src/Login.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,37 @@ limitations under the License.
1919
import { createClient } from "matrix-js-sdk/src/matrix";
2020
import { MatrixClient } from "matrix-js-sdk/src/client";
2121
import { logger } from "matrix-js-sdk/src/logger";
22-
import { DELEGATED_OIDC_COMPATIBILITY, ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
22+
import { DELEGATED_OIDC_COMPATIBILITY, ILoginFlow, ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
2323

2424
import { IMatrixClientCreds } from "./MatrixClientPeg";
2525
import SecurityCustomisations from "./customisations/Security";
26+
import { ValidatedDelegatedAuthConfig } from "./utils/ValidatedServerConfig";
27+
import { getOidcClientId } from "./utils/oidc/registerClient";
28+
import { IConfigOptions } from "./IConfigOptions";
29+
import SdkConfig from "./SdkConfig";
30+
31+
/**
32+
* Login flows supported by this client
33+
* LoginFlow type use the client API /login endpoint
34+
* OidcNativeFlow is specific to this client
35+
*/
36+
export type ClientLoginFlow = LoginFlow | OidcNativeFlow;
2637

2738
interface ILoginOptions {
2839
defaultDeviceDisplayName?: string;
40+
/**
41+
* Delegated auth config from server's .well-known.
42+
*
43+
* If this property is set, we will attempt an OIDC login using the delegated auth settings.
44+
* The caller is responsible for checking that OIDC is enabled in the labs settings.
45+
*/
46+
delegatedAuthentication?: ValidatedDelegatedAuthConfig;
2947
}
3048

3149
export default class Login {
32-
private flows: Array<LoginFlow> = [];
50+
private flows: Array<ClientLoginFlow> = [];
3351
private readonly defaultDeviceDisplayName?: string;
52+
private readonly delegatedAuthentication?: ValidatedDelegatedAuthConfig;
3453
private tempClient: MatrixClient | null = null; // memoize
3554

3655
public constructor(
@@ -40,6 +59,7 @@ export default class Login {
4059
opts: ILoginOptions,
4160
) {
4261
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
62+
this.delegatedAuthentication = opts.delegatedAuthentication;
4363
}
4464

4565
public getHomeserverUrl(): string {
@@ -75,7 +95,22 @@ export default class Login {
7595
return this.tempClient;
7696
}
7797

78-
public async getFlows(): Promise<Array<LoginFlow>> {
98+
public async getFlows(): Promise<Array<ClientLoginFlow>> {
99+
// try to use oidc native flow if we have delegated auth config
100+
if (this.delegatedAuthentication) {
101+
try {
102+
const oidcFlow = await tryInitOidcNativeFlow(
103+
this.delegatedAuthentication,
104+
SdkConfig.get().brand,
105+
SdkConfig.get().oidc_static_client_ids,
106+
);
107+
return [oidcFlow];
108+
} catch (error) {
109+
logger.error(error);
110+
}
111+
}
112+
113+
// oidc native flow not supported, continue with matrix login
79114
const client = this.createTemporaryClient();
80115
const { flows }: { flows: LoginFlow[] } = await client.loginFlows();
81116
// If an m.login.sso flow is present which is also flagged as being for MSC3824 OIDC compatibility then we only
@@ -151,6 +186,43 @@ export default class Login {
151186
}
152187
}
153188

189+
/**
190+
* Describes the OIDC native login flow
191+
* Separate from js-sdk's `LoginFlow` as this does not use the same /login flow
192+
* to which that type belongs.
193+
*/
194+
export interface OidcNativeFlow extends ILoginFlow {
195+
type: "oidcNativeFlow";
196+
// this client's id as registered with the configured OIDC OP
197+
clientId: string;
198+
}
199+
/**
200+
* Prepares an OidcNativeFlow for logging into the server.
201+
*
202+
* Finds a static clientId for configured issuer, or attempts dynamic registration with the OP, and wraps the
203+
* results.
204+
*
205+
* @param delegatedAuthConfig Auth config from ValidatedServerConfig
206+
* @param clientName Client name to register with the OP, eg 'Element', used during client registration with OP
207+
* @param staticOidcClientIds static client config from config.json, used during client registration with OP
208+
* @returns Promise<OidcNativeFlow> when oidc native authentication flow is supported and correctly configured
209+
* @throws when client can't register with OP, or any unexpected error
210+
*/
211+
const tryInitOidcNativeFlow = async (
212+
delegatedAuthConfig: ValidatedDelegatedAuthConfig,
213+
brand: string,
214+
oidcStaticClientIds?: IConfigOptions["oidc_static_client_ids"],
215+
): Promise<OidcNativeFlow> => {
216+
const clientId = await getOidcClientId(delegatedAuthConfig, brand, window.location.origin, oidcStaticClientIds);
217+
218+
const flow = {
219+
type: "oidcNativeFlow",
220+
clientId,
221+
} as OidcNativeFlow;
222+
223+
return flow;
224+
};
225+
154226
/**
155227
* Send a login request to the given server, and format the response
156228
* as a MatrixClientCreds

src/components/structures/auth/Login.tsx

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ limitations under the License.
1717
import React, { ReactNode } from "react";
1818
import classNames from "classnames";
1919
import { logger } from "matrix-js-sdk/src/logger";
20-
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
20+
import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
2121

2222
import { _t, _td, UserFriendlyError } from "../../../languageHandler";
23-
import Login from "../../../Login";
23+
import Login, { ClientLoginFlow } from "../../../Login";
2424
import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils";
2525
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
2626
import AuthPage from "../../views/auth/AuthPage";
@@ -38,6 +38,7 @@ import AuthHeader from "../../views/auth/AuthHeader";
3838
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
3939
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
4040
import { filterBoolean } from "../../../utils/arrays";
41+
import { Features } from "../../../settings/Settings";
4142

4243
// These are used in several places, and come from the js-sdk's autodiscovery
4344
// stuff. We define them here so that they'll be picked up by i18n.
@@ -84,7 +85,7 @@ interface IState {
8485
// can we attempt to log in or are there validation errors?
8586
canTryLogin: boolean;
8687

87-
flows?: LoginFlow[];
88+
flows?: ClientLoginFlow[];
8889

8990
// used for preserving form values when changing homeserver
9091
username: string;
@@ -110,13 +111,17 @@ type OnPasswordLogin = {
110111
*/
111112
export default class LoginComponent extends React.PureComponent<IProps, IState> {
112113
private unmounted = false;
114+
private oidcNativeFlowEnabled = false;
113115
private loginLogic!: Login;
114116

115117
private readonly stepRendererMap: Record<string, () => ReactNode>;
116118

117119
public constructor(props: IProps) {
118120
super(props);
119121

122+
// only set on a config level, so we don't need to watch
123+
this.oidcNativeFlowEnabled = SettingsStore.getValue(Features.OidcNativeFlow);
124+
120125
this.state = {
121126
busy: false,
122127
errorText: null,
@@ -156,7 +161,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
156161
public componentDidUpdate(prevProps: IProps): void {
157162
if (
158163
prevProps.serverConfig.hsUrl !== this.props.serverConfig.hsUrl ||
159-
prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl
164+
prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl ||
165+
// delegatedAuthentication is only set by buildValidatedConfigFromDiscovery and won't be modified
166+
// so shallow comparison is fine
167+
prevProps.serverConfig.delegatedAuthentication !== this.props.serverConfig.delegatedAuthentication
160168
) {
161169
// Ensure that we end up actually logging in to the right place
162170
this.initLoginLogic(this.props.serverConfig);
@@ -322,28 +330,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
322330
}
323331
};
324332

325-
private async initLoginLogic({ hsUrl, isUrl }: ValidatedServerConfig): Promise<void> {
326-
let isDefaultServer = false;
327-
if (
328-
this.props.serverConfig.isDefault &&
329-
hsUrl === this.props.serverConfig.hsUrl &&
330-
isUrl === this.props.serverConfig.isUrl
331-
) {
332-
isDefaultServer = true;
333-
}
334-
335-
const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl! : null;
336-
337-
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
338-
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
339-
});
340-
this.loginLogic = loginLogic;
341-
342-
this.setState({
343-
busy: true,
344-
loginIncorrect: false,
345-
});
346-
333+
private async checkServerLiveliness({
334+
hsUrl,
335+
isUrl,
336+
}: Pick<ValidatedServerConfig, "hsUrl" | "isUrl">): Promise<void> {
347337
// Do a quick liveliness check on the URLs
348338
try {
349339
const { warning } = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
@@ -361,9 +351,38 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
361351
} catch (e) {
362352
this.setState({
363353
busy: false,
364-
...AutoDiscoveryUtils.authComponentStateForError(e),
354+
...AutoDiscoveryUtils.authComponentStateForError(e as Error),
365355
});
366356
}
357+
}
358+
359+
private async initLoginLogic({ hsUrl, isUrl }: ValidatedServerConfig): Promise<void> {
360+
let isDefaultServer = false;
361+
if (
362+
this.props.serverConfig.isDefault &&
363+
hsUrl === this.props.serverConfig.hsUrl &&
364+
isUrl === this.props.serverConfig.isUrl
365+
) {
366+
isDefaultServer = true;
367+
}
368+
369+
const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl! : null;
370+
371+
this.setState({
372+
busy: true,
373+
loginIncorrect: false,
374+
});
375+
376+
await this.checkServerLiveliness({ hsUrl, isUrl });
377+
378+
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
379+
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
380+
// if native OIDC is enabled in the client pass the server's delegated auth settings
381+
delegatedAuthentication: this.oidcNativeFlowEnabled
382+
? this.props.serverConfig.delegatedAuthentication
383+
: undefined,
384+
});
385+
this.loginLogic = loginLogic;
367386

368387
loginLogic
369388
.getFlows()
@@ -401,7 +420,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
401420
});
402421
}
403422

404-
private isSupportedFlow = (flow: LoginFlow): boolean => {
423+
private isSupportedFlow = (flow: ClientLoginFlow): boolean => {
405424
// technically the flow can have multiple steps, but no one does this
406425
// for login and loginLogic doesn't support it so we can ignore it.
407426
if (!this.stepRendererMap[flow.type]) {

src/utils/AutoDiscoveryUtils.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ limitations under the License.
1616

1717
import React, { ReactNode } from "react";
1818
import { AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/autodiscovery";
19-
import { IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/client";
19+
import { M_AUTHENTICATION } from "matrix-js-sdk/src/client";
2020
import { logger } from "matrix-js-sdk/src/logger";
2121
import { IClientWellKnown } from "matrix-js-sdk/src/matrix";
22-
import { ValidatedIssuerConfig } from "matrix-js-sdk/src/oidc/validate";
2322

2423
import { _t, UserFriendlyError } from "../languageHandler";
2524
import SdkConfig from "../SdkConfig";
26-
import { ValidatedServerConfig } from "./ValidatedServerConfig";
25+
import { ValidatedDelegatedAuthConfig, ValidatedServerConfig } from "./ValidatedServerConfig";
2726

2827
const LIVELINESS_DISCOVERY_ERRORS: string[] = [
2928
AutoDiscovery.ERROR_INVALID_HOMESERVER,
@@ -266,14 +265,14 @@ export default class AutoDiscoveryUtils {
266265
if (discoveryResult[M_AUTHENTICATION.stable!]?.state === AutoDiscovery.SUCCESS) {
267266
const { authorizationEndpoint, registrationEndpoint, tokenEndpoint, account, issuer } = discoveryResult[
268267
M_AUTHENTICATION.stable!
269-
] as IDelegatedAuthConfig & ValidatedIssuerConfig;
270-
delegatedAuthentication = {
268+
] as ValidatedDelegatedAuthConfig;
269+
delegatedAuthentication = Object.freeze({
271270
authorizationEndpoint,
272271
registrationEndpoint,
273272
tokenEndpoint,
274273
account,
275274
issuer,
276-
};
275+
});
277276
}
278277

279278
return {

src/utils/ValidatedServerConfig.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
import { IDelegatedAuthConfig } from "matrix-js-sdk/src/client";
1818
import { ValidatedIssuerConfig } from "matrix-js-sdk/src/oidc/validate";
1919

20+
export type ValidatedDelegatedAuthConfig = IDelegatedAuthConfig & ValidatedIssuerConfig;
21+
2022
export interface ValidatedServerConfig {
2123
hsUrl: string;
2224
hsName: string;
@@ -30,5 +32,11 @@ export interface ValidatedServerConfig {
3032

3133
warning: string | Error;
3234

33-
delegatedAuthentication?: IDelegatedAuthConfig & ValidatedIssuerConfig;
35+
/**
36+
* Config related to delegated authentication
37+
* Included when delegated auth is configured and valid, otherwise undefined
38+
* From homeserver .well-known m.authentication, and issuer's .well-known/openid-configuration
39+
* Used for OIDC native flow authentication
40+
*/
41+
delegatedAuthentication?: ValidatedDelegatedAuthConfig;
3442
}

src/utils/oidc/error.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* OIDC error strings, intended for logging
19+
*/
20+
export enum OidcClientError {
21+
DynamicRegistrationNotSupported = "Dynamic registration not supported",
22+
DynamicRegistrationFailed = "Dynamic registration failed",
23+
DynamicRegistrationInvalid = "Dynamic registration invalid response",
24+
}

0 commit comments

Comments
 (0)