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

Commit d698193

Browse files
authored
Implementation of MSC3824 to make the client OIDC-aware (#8681)
1 parent 32bd350 commit d698193

File tree

11 files changed

+240
-29
lines changed

11 files changed

+240
-29
lines changed

src/BasePlatform.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/crypto/olmlib";
2222
import { logger } from "matrix-js-sdk/src/logger";
2323
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2424
import { Room } from "matrix-js-sdk/src/models/room";
25+
import { SSOAction } from "matrix-js-sdk/src/@types/auth";
2526

2627
import dis from "./dispatcher/dispatcher";
2728
import BaseEventIndexManager from "./indexing/BaseEventIndexManager";
@@ -308,9 +309,9 @@ export default abstract class BasePlatform {
308309
return null;
309310
}
310311

311-
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
312+
protected getSSOCallbackUrl(fragmentAfterLogin = ""): URL {
312313
const url = new URL(window.location.href);
313-
url.hash = fragmentAfterLogin || "";
314+
url.hash = fragmentAfterLogin;
314315
return url;
315316
}
316317

@@ -319,13 +320,15 @@ export default abstract class BasePlatform {
319320
* @param {MatrixClient} mxClient the matrix client using which we should start the flow
320321
* @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO.
321322
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
323+
* @param {SSOAction} action the SSO flow to indicate to the IdP, optional.
322324
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
323325
*/
324326
public startSingleSignOn(
325327
mxClient: MatrixClient,
326328
loginType: "sso" | "cas",
327-
fragmentAfterLogin: string,
329+
fragmentAfterLogin?: string,
328330
idpId?: string,
331+
action?: SSOAction,
329332
): void {
330333
// persist hs url and is url for when the user is returned to the app with the login token
331334
localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
@@ -336,7 +339,7 @@ export default abstract class BasePlatform {
336339
localStorage.setItem(SSO_IDP_ID_KEY, idpId);
337340
}
338341
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
339-
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
342+
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId, action); // redirect to SSO
340343
}
341344

342345
/**

src/Lifecycle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
2323
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
2424
import { QueryDict } from "matrix-js-sdk/src/utils";
2525
import { logger } from "matrix-js-sdk/src/logger";
26+
import { SSOAction } from "matrix-js-sdk/src/@types/auth";
2627

2728
import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg";
2829
import SecurityCustomisations from "./customisations/Security";
@@ -248,7 +249,7 @@ export function attemptTokenLogin(
248249
idBaseUrl: identityServer,
249250
});
250251
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
251-
PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
252+
PlatformPeg.get()?.startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId, SSOAction.LOGIN);
252253
}
253254
},
254255
});

src/Login.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ 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 { ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
22+
import { DELEGATED_OIDC_COMPATIBILITY, ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
2323

2424
import { IMatrixClientCreds } from "./MatrixClientPeg";
2525
import SecurityCustomisations from "./customisations/Security";
@@ -32,7 +32,6 @@ export default class Login {
3232
private hsUrl: string;
3333
private isUrl: string;
3434
private fallbackHsUrl: string;
35-
// TODO: Flows need a type in JS SDK
3635
private flows: Array<LoginFlow>;
3736
private defaultDeviceDisplayName: string;
3837
private tempClient: MatrixClient;
@@ -81,8 +80,13 @@ export default class Login {
8180

8281
public async getFlows(): Promise<Array<LoginFlow>> {
8382
const client = this.createTemporaryClient();
84-
const { flows } = await client.loginFlows();
85-
this.flows = flows;
83+
const { flows }: { flows: LoginFlow[] } = await client.loginFlows();
84+
// If an m.login.sso flow is present which is also flagged as being for MSC3824 OIDC compatibility then we only
85+
// return that flow as (per MSC3824) it is the only one that the user should be offered to give the best experience
86+
const oidcCompatibilityFlow = flows.find(
87+
(f) => f.type === "m.login.sso" && DELEGATED_OIDC_COMPATIBILITY.findIn(f),
88+
);
89+
this.flows = oidcCompatibilityFlow ? [oidcCompatibilityFlow] : flows;
8690
return this.flows;
8791
}
8892

src/components/structures/auth/Login.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React, { ReactNode } from "react";
1818
import { ConnectionError, MatrixError } from "matrix-js-sdk/src/http-api";
1919
import classNames from "classnames";
2020
import { logger } from "matrix-js-sdk/src/logger";
21-
import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
21+
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
2222

2323
import { _t, _td } from "../../../languageHandler";
2424
import Login from "../../../Login";
@@ -345,6 +345,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
345345
this.loginLogic.createTemporaryClient(),
346346
ssoKind,
347347
this.props.fragmentAfterLogin,
348+
SSOAction.REGISTER,
348349
);
349350
} else {
350351
// Don't intercept - just go through to the register page
@@ -549,6 +550,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
549550
loginType={loginType}
550551
fragmentAfterLogin={this.props.fragmentAfterLogin}
551552
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
553+
action={SSOAction.LOGIN}
552554
/>
553555
);
554556
};

src/components/structures/auth/Registration.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import React, { Fragment, ReactNode } from "react";
1919
import { IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/client";
2020
import classNames from "classnames";
2121
import { logger } from "matrix-js-sdk/src/logger";
22-
import { ISSOFlow } from "matrix-js-sdk/src/@types/auth";
22+
import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
2323

2424
import { _t, _td } from "../../../languageHandler";
2525
import { messageForResourceLimitError } from "../../../utils/ErrorUtils";
@@ -539,6 +539,7 @@ export default class Registration extends React.Component<IProps, IState> {
539539
flow={this.state.ssoFlow}
540540
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
541541
fragmentAfterLogin={this.props.fragmentAfterLogin}
542+
action={SSOAction.REGISTER}
542543
/>
543544
<h2 className="mx_AuthBody_centered">
544545
{_t("%(ssoButtons)s Or %(usernamePassword)s", {

src/components/structures/auth/SoftLogout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import React from "react";
1818
import { logger } from "matrix-js-sdk/src/logger";
1919
import { Optional } from "matrix-events-sdk";
20-
import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
20+
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
2121

2222
import { _t } from "../../../languageHandler";
2323
import dis from "../../../dispatcher/dispatcher";
@@ -256,6 +256,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
256256
loginType={loginType}
257257
fragmentAfterLogin={this.props.fragmentAfterLogin}
258258
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
259+
action={SSOAction.LOGIN}
259260
/>
260261
</div>
261262
);

src/components/views/elements/SSOButtons.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import { chunk } from "lodash";
1919
import classNames from "classnames";
2020
import { MatrixClient } from "matrix-js-sdk/src/client";
2121
import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
22-
import { IdentityProviderBrand, IIdentityProvider, ISSOFlow } from "matrix-js-sdk/src/@types/auth";
22+
import {
23+
IdentityProviderBrand,
24+
IIdentityProvider,
25+
ISSOFlow,
26+
DELEGATED_OIDC_COMPATIBILITY,
27+
SSOAction,
28+
} from "matrix-js-sdk/src/@types/auth";
2329

2430
import PlatformPeg from "../../../PlatformPeg";
2531
import AccessibleButton from "./AccessibleButton";
@@ -28,9 +34,10 @@ import AccessibleTooltipButton from "./AccessibleTooltipButton";
2834
import { mediaFromMxc } from "../../../customisations/Media";
2935
import { PosthogAnalytics } from "../../../PosthogAnalytics";
3036

31-
interface ISSOButtonProps extends Omit<IProps, "flow"> {
37+
interface ISSOButtonProps extends IProps {
3238
idp?: IIdentityProvider;
3339
mini?: boolean;
40+
action?: SSOAction;
3441
}
3542

3643
const getIcon = (brand: IdentityProviderBrand | string): string | null => {
@@ -79,20 +86,29 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
7986
idp,
8087
primary,
8188
mini,
89+
action,
90+
flow,
8291
...props
8392
}) => {
84-
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
93+
let label: string;
94+
if (idp) {
95+
label = _t("Continue with %(provider)s", { provider: idp.name });
96+
} else if (DELEGATED_OIDC_COMPATIBILITY.findIn<boolean>(flow)) {
97+
label = _t("Continue");
98+
} else {
99+
label = _t("Sign in with single sign-on");
100+
}
85101

86102
const onClick = (): void => {
87103
const authenticationType = getAuthenticationType(idp?.brand ?? "");
88104
PosthogAnalytics.instance.setAuthenticationType(authenticationType);
89-
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id);
105+
PlatformPeg.get()?.startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id, action);
90106
};
91107

92-
let icon;
93-
let brandClass;
94-
const brandIcon = idp ? getIcon(idp.brand) : null;
95-
if (brandIcon) {
108+
let icon: JSX.Element | undefined;
109+
let brandClass: string | undefined;
110+
const brandIcon = idp?.brand ? getIcon(idp.brand) : null;
111+
if (idp?.brand && brandIcon) {
96112
const brandName = idp.brand.split(".").pop();
97113
brandClass = `mx_SSOButton_brand_${brandName}`;
98114
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
@@ -101,12 +117,16 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
101117
icon = <img src={src} height="24" width="24" alt={idp.name} />;
102118
}
103119

104-
const classes = classNames("mx_SSOButton", {
105-
[brandClass]: brandClass,
106-
mx_SSOButton_mini: mini,
107-
mx_SSOButton_default: !idp,
108-
mx_SSOButton_primary: primary,
109-
});
120+
const brandPart = brandClass ? { [brandClass]: brandClass } : undefined;
121+
const classes = classNames(
122+
"mx_SSOButton",
123+
{
124+
mx_SSOButton_mini: mini,
125+
mx_SSOButton_default: !idp,
126+
mx_SSOButton_primary: primary,
127+
},
128+
brandPart,
129+
);
110130

111131
if (mini) {
112132
// TODO fallback icon
@@ -128,14 +148,15 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
128148
interface IProps {
129149
matrixClient: MatrixClient;
130150
flow: ISSOFlow;
131-
loginType?: "sso" | "cas";
151+
loginType: "sso" | "cas";
132152
fragmentAfterLogin?: string;
133153
primary?: boolean;
154+
action?: SSOAction;
134155
}
135156

136157
const MAX_PER_ROW = 6;
137158

138-
const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary }) => {
159+
const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary, action }) => {
139160
const providers = flow.identity_providers || [];
140161
if (providers.length < 2) {
141162
return (
@@ -146,6 +167,8 @@ const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentA
146167
fragmentAfterLogin={fragmentAfterLogin}
147168
idp={providers[0]}
148169
primary={primary}
170+
action={action}
171+
flow={flow}
149172
/>
150173
</div>
151174
);
@@ -167,6 +190,8 @@ const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentA
167190
idp={idp}
168191
mini={true}
169192
primary={primary}
193+
action={action}
194+
flow={flow}
170195
/>
171196
))}
172197
</div>

src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React from "react";
2020
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
2121
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
2222
import { logger } from "matrix-js-sdk/src/logger";
23+
import { IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
2324

2425
import { _t } from "../../../../../languageHandler";
2526
import ProfileSettings from "../../ProfileSettings";
@@ -79,6 +80,7 @@ interface IState {
7980
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
8081
canChangePassword: boolean;
8182
idServerName: string;
83+
externalAccountManagementUrl?: string;
8284
}
8385

8486
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
@@ -106,6 +108,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
106108
loading3pids: true, // whether or not the emails and msisdns have been loaded
107109
canChangePassword: false,
108110
idServerName: null,
111+
externalAccountManagementUrl: undefined,
109112
};
110113

111114
this.dispatcherRef = dis.register(this.onAction);
@@ -161,7 +164,10 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
161164
// the enabled flag value.
162165
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
163166

164-
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
167+
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(cli.getClientWellKnown());
168+
const externalAccountManagementUrl = delegatedAuthConfig?.account;
169+
170+
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword, externalAccountManagementUrl });
165171
}
166172

167173
private async getThreepidState(): Promise<void> {
@@ -348,9 +354,37 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
348354
passwordChangeForm = null;
349355
}
350356

357+
let externalAccountManagement: JSX.Element | undefined;
358+
if (this.state.externalAccountManagementUrl) {
359+
const { hostname } = new URL(this.state.externalAccountManagementUrl);
360+
361+
externalAccountManagement = (
362+
<>
363+
<p className="mx_SettingsTab_subsectionText" data-testid="external-account-management-outer">
364+
{_t(
365+
"Your account details are managed separately at <code>%(hostname)s</code>.",
366+
{ hostname },
367+
{ code: (sub) => <code>{sub}</code> },
368+
)}
369+
</p>
370+
<AccessibleButton
371+
onClick={null}
372+
element="a"
373+
kind="primary"
374+
target="_blank"
375+
rel="noreferrer noopener"
376+
href={this.state.externalAccountManagementUrl}
377+
data-testid="external-account-management-link"
378+
>
379+
{_t("Manage account")}
380+
</AccessibleButton>
381+
</>
382+
);
383+
}
351384
return (
352385
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
353386
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
387+
{externalAccountManagement}
354388
<p className="mx_SettingsTab_subsectionText">{passwordChangeText}</p>
355389
{passwordChangeForm}
356390
{threepidSection}

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,8 @@
15321532
"Email addresses": "Email addresses",
15331533
"Phone numbers": "Phone numbers",
15341534
"Set a new account password...": "Set a new account password...",
1535+
"Your account details are managed separately at <code>%(hostname)s</code>.": "Your account details are managed separately at <code>%(hostname)s</code>.",
1536+
"Manage account": "Manage account",
15351537
"Account": "Account",
15361538
"Language and region": "Language and region",
15371539
"Spell check": "Spell check",

0 commit comments

Comments
 (0)