2019-07-03 16:46:37 -06:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2022-02-23 09:22:37 -07:00
|
|
|
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
|
2019-07-03 16:46:37 -06:00
|
|
|
|
2024-09-09 14:57:16 +01:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2019-07-03 16:46:37 -06:00
|
|
|
*/
|
|
|
|
|
|
2023-02-13 11:39:16 +00:00
|
|
|
import React, { ChangeEvent, SyntheticEvent } from "react";
|
2021-10-22 17:23:32 -05:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2022-02-23 09:22:37 -07:00
|
|
|
import { Optional } from "matrix-events-sdk";
|
2023-08-24 14:27:32 +01:00
|
|
|
import { LoginFlow, MatrixError, SSOAction, SSOFlow } from "matrix-js-sdk/src/matrix";
|
2021-10-22 17:23:32 -05:00
|
|
|
|
2021-06-29 13:11:58 +01:00
|
|
|
import { _t } from "../../../languageHandler";
|
2020-05-13 20:41:41 -06:00
|
|
|
import dis from "../../../dispatcher/dispatcher";
|
2019-07-03 16:46:37 -06:00
|
|
|
import * as Lifecycle from "../../../Lifecycle";
|
|
|
|
|
import Modal from "../../../Modal";
|
2023-03-10 14:55:06 +00:00
|
|
|
import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
|
2022-10-25 17:00:23 -06:00
|
|
|
import { sendLoginRequest } from "../../../Login";
|
2019-12-12 19:31:52 -07:00
|
|
|
import AuthPage from "../../views/auth/AuthPage";
|
2021-06-29 13:11:58 +01:00
|
|
|
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
|
2020-11-23 17:10:15 +00:00
|
|
|
import SSOButtons from "../../views/elements/SSOButtons";
|
2021-07-03 11:24:33 +02:00
|
|
|
import ConfirmWipeDeviceDialog from "../../views/dialogs/ConfirmWipeDeviceDialog";
|
|
|
|
|
import Field from "../../views/elements/Field";
|
|
|
|
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
|
|
|
|
import Spinner from "../../views/elements/Spinner";
|
|
|
|
|
import AuthHeader from "../../views/auth/AuthHeader";
|
|
|
|
|
import AuthBody from "../../views/auth/AuthBody";
|
2023-10-16 10:35:25 +13:00
|
|
|
import { SDKContext } from "../../../contexts/SDKContext";
|
2019-07-04 16:45:40 -06:00
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
enum LoginView {
|
|
|
|
|
Loading,
|
|
|
|
|
Password,
|
|
|
|
|
CAS, // SSO, but old
|
|
|
|
|
SSO,
|
|
|
|
|
PasswordWithSocialSignOn,
|
|
|
|
|
Unsupported,
|
|
|
|
|
}
|
2019-07-04 16:45:40 -06:00
|
|
|
|
2023-02-13 11:39:16 +00:00
|
|
|
const STATIC_FLOWS_TO_VIEWS: Record<string, LoginView> = {
|
2022-02-23 09:22:37 -07:00
|
|
|
"m.login.password": LoginView.Password,
|
|
|
|
|
"m.login.cas": LoginView.CAS,
|
|
|
|
|
"m.login.sso": LoginView.SSO,
|
2019-07-04 16:45:40 -06:00
|
|
|
};
|
2019-07-03 16:46:37 -06:00
|
|
|
|
2021-04-06 12:26:50 +01:00
|
|
|
interface IProps {
|
|
|
|
|
// Query parameters from MatrixChat
|
|
|
|
|
realQueryParams: {
|
|
|
|
|
loginToken?: string;
|
2019-07-03 16:46:37 -06:00
|
|
|
};
|
2021-04-06 12:26:50 +01:00
|
|
|
fragmentAfterLogin?: string;
|
2019-07-03 16:46:37 -06:00
|
|
|
|
2021-04-06 12:26:50 +01:00
|
|
|
// Called when the SSO login completes
|
2021-07-01 23:23:03 +01:00
|
|
|
onTokenLoginCompleted: () => void;
|
2021-04-06 12:26:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IState {
|
2022-02-23 09:22:37 -07:00
|
|
|
loginView: LoginView;
|
2021-04-06 12:26:50 +01:00
|
|
|
busy: boolean;
|
|
|
|
|
password: string;
|
|
|
|
|
errorText: string;
|
|
|
|
|
flows: LoginFlow[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default class SoftLogout extends React.Component<IProps, IState> {
|
2023-10-16 10:35:25 +13:00
|
|
|
public static contextType = SDKContext;
|
2024-08-01 17:14:28 +01:00
|
|
|
public declare context: React.ContextType<typeof SDKContext>;
|
2023-10-16 10:35:25 +13:00
|
|
|
|
|
|
|
|
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
|
|
|
|
super(props, context);
|
|
|
|
|
|
2019-07-03 16:46:37 -06:00
|
|
|
this.state = {
|
2022-02-23 09:22:37 -07:00
|
|
|
loginView: LoginView.Loading,
|
2019-07-04 16:45:40 -06:00
|
|
|
busy: false,
|
|
|
|
|
password: "",
|
|
|
|
|
errorText: "",
|
2021-04-06 12:26:50 +01:00
|
|
|
flows: [],
|
2019-07-03 16:46:37 -06:00
|
|
|
};
|
2019-07-08 11:51:22 -06:00
|
|
|
}
|
2019-07-04 16:45:40 -06:00
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
public componentDidMount(): void {
|
2019-07-08 12:43:16 -06:00
|
|
|
// We've ended up here when we don't need to - navigate to login
|
|
|
|
|
if (!Lifecycle.isSoftLogout()) {
|
2021-06-29 13:11:58 +01:00
|
|
|
dis.dispatch({ action: "start_login" });
|
2019-07-08 12:43:16 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-26 14:02:53 +01:00
|
|
|
this.initLogin();
|
2019-07-03 16:46:37 -06:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onClearAll = (): void => {
|
2022-06-14 17:51:51 +01:00
|
|
|
Modal.createDialog(ConfirmWipeDeviceDialog, {
|
2019-07-03 16:46:37 -06:00
|
|
|
onFinished: (wipeData) => {
|
|
|
|
|
if (!wipeData) return;
|
|
|
|
|
|
2021-09-21 17:48:09 +02:00
|
|
|
logger.log("Clearing data from soft-logged-out session");
|
2023-10-16 10:35:25 +13:00
|
|
|
Lifecycle.logout(this.context.oidcClientStore);
|
2019-07-03 16:46:37 -06:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private async initLogin(): Promise<void> {
|
2019-07-10 08:01:32 -06:00
|
|
|
const queryParams = this.props.realQueryParams;
|
2023-03-31 09:26:15 +01:00
|
|
|
const hasAllParams = queryParams?.["loginToken"];
|
2019-07-10 08:01:32 -06:00
|
|
|
if (hasAllParams) {
|
2022-02-23 09:22:37 -07:00
|
|
|
this.setState({ loginView: LoginView.Loading });
|
2023-10-26 19:57:28 +13:00
|
|
|
|
|
|
|
|
const loggedIn = await this.trySsoLogin();
|
|
|
|
|
if (loggedIn) return;
|
2019-07-08 12:43:16 -06:00
|
|
|
}
|
|
|
|
|
|
2019-07-04 17:00:09 -06:00
|
|
|
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
|
|
|
|
|
// care about login flows here, unless it is the single flow we support.
|
2023-06-15 15:11:49 +01:00
|
|
|
const client = MatrixClientPeg.safeGet();
|
2020-11-23 17:10:15 +00:00
|
|
|
const flows = (await client.loginFlows()).flows;
|
2022-02-23 09:22:37 -07:00
|
|
|
const loginViews = flows.map((f) => STATIC_FLOWS_TO_VIEWS[f.type]);
|
2019-07-04 16:45:40 -06:00
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
const isSocialSignOn = loginViews.includes(LoginView.Password) && loginViews.includes(LoginView.SSO);
|
|
|
|
|
const firstView = loginViews.filter((f) => !!f)[0] || LoginView.Unsupported;
|
|
|
|
|
const chosenView = isSocialSignOn ? LoginView.PasswordWithSocialSignOn : firstView;
|
2020-11-23 17:10:15 +00:00
|
|
|
this.setState({ flows, loginView: chosenView });
|
2019-07-04 16:45:40 -06:00
|
|
|
}
|
|
|
|
|
|
2023-02-13 11:39:16 +00:00
|
|
|
private onPasswordChange = (ev: ChangeEvent<HTMLInputElement>): void => {
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ password: ev.target.value });
|
2019-07-03 16:46:37 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onForgotPassword = (): void => {
|
2021-06-29 13:11:58 +01:00
|
|
|
dis.dispatch({ action: "start_password_recovery" });
|
2019-07-04 16:45:40 -06:00
|
|
|
};
|
|
|
|
|
|
2023-02-13 11:39:16 +00:00
|
|
|
private onPasswordLogin = async (ev: SyntheticEvent): Promise<void> => {
|
2019-07-04 16:45:40 -06:00
|
|
|
ev.preventDefault();
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ busy: true });
|
2019-07-04 16:45:40 -06:00
|
|
|
|
2023-06-15 15:11:49 +01:00
|
|
|
const cli = MatrixClientPeg.safeGet();
|
|
|
|
|
const hsUrl = cli.getHomeserverUrl();
|
|
|
|
|
const isUrl = cli.getIdentityServerUrl();
|
2019-07-04 16:45:40 -06:00
|
|
|
const loginType = "m.login.password";
|
|
|
|
|
const loginParams = {
|
|
|
|
|
identifier: {
|
|
|
|
|
type: "m.id.user",
|
2023-06-15 15:11:49 +01:00
|
|
|
user: cli.getUserId(),
|
2019-07-04 16:45:40 -06:00
|
|
|
},
|
|
|
|
|
password: this.state.password,
|
2023-06-15 15:11:49 +01:00
|
|
|
device_id: cli.getDeviceId() ?? undefined,
|
2019-07-04 16:45:40 -06:00
|
|
|
};
|
|
|
|
|
|
2023-03-10 14:55:06 +00:00
|
|
|
let credentials: IMatrixClientCreds;
|
2019-07-04 16:45:40 -06:00
|
|
|
try {
|
|
|
|
|
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
|
|
|
|
|
} catch (e) {
|
2023-09-28 12:51:30 +05:30
|
|
|
let errorText = _t("auth|failed_soft_logout_homeserver");
|
2023-05-16 14:25:43 +01:00
|
|
|
if (
|
|
|
|
|
e instanceof MatrixError &&
|
|
|
|
|
e.errcode === "M_FORBIDDEN" &&
|
|
|
|
|
(e.httpStatus === 401 || e.httpStatus === 403)
|
|
|
|
|
) {
|
2023-09-19 07:17:31 +01:00
|
|
|
errorText = _t("auth|incorrect_password");
|
2019-07-04 16:45:40 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
busy: false,
|
|
|
|
|
errorText: errorText,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-14 18:10:41 +02:00
|
|
|
Lifecycle.setLoggedIn(credentials).catch((e) => {
|
2021-10-15 16:30:53 +02:00
|
|
|
logger.error(e);
|
2023-09-19 07:17:31 +01:00
|
|
|
this.setState({ busy: false, errorText: _t("auth|failed_soft_logout_auth") });
|
2019-07-04 16:45:40 -06:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-26 19:57:28 +13:00
|
|
|
/**
|
|
|
|
|
* Attempt to login via SSO
|
|
|
|
|
* @returns A promise that resolves to a boolean - true when sso login was successful
|
|
|
|
|
*/
|
|
|
|
|
private async trySsoLogin(): Promise<boolean> {
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ busy: true });
|
2019-07-08 12:43:16 -06:00
|
|
|
|
2020-06-25 21:59:46 +01:00
|
|
|
const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
|
2023-03-31 09:26:15 +01:00
|
|
|
if (!hsUrl) {
|
|
|
|
|
logger.error("Homeserver URL unknown for SSO login callback");
|
|
|
|
|
this.setState({ busy: false, loginView: LoginView.Unsupported });
|
2023-10-26 19:57:28 +13:00
|
|
|
return false;
|
2023-03-31 09:26:15 +01:00
|
|
|
}
|
|
|
|
|
|
2023-06-15 15:11:49 +01:00
|
|
|
const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.safeGet().getIdentityServerUrl();
|
2019-07-08 12:43:16 -06:00
|
|
|
const loginType = "m.login.token";
|
|
|
|
|
const loginParams = {
|
|
|
|
|
token: this.props.realQueryParams["loginToken"],
|
2023-06-15 15:11:49 +01:00
|
|
|
device_id: MatrixClientPeg.safeGet().getDeviceId() ?? undefined,
|
2019-07-08 12:43:16 -06:00
|
|
|
};
|
|
|
|
|
|
2023-03-10 14:55:06 +00:00
|
|
|
let credentials: IMatrixClientCreds;
|
2019-07-08 12:43:16 -06:00
|
|
|
try {
|
|
|
|
|
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
|
|
|
|
|
} catch (e) {
|
2021-10-15 16:30:53 +02:00
|
|
|
logger.error(e);
|
2022-02-23 09:22:37 -07:00
|
|
|
this.setState({ busy: false, loginView: LoginView.Unsupported });
|
2023-10-26 19:57:28 +13:00
|
|
|
return false;
|
2019-07-08 12:43:16 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-14 18:10:41 +02:00
|
|
|
return Lifecycle.setLoggedIn(credentials)
|
2019-07-08 12:43:16 -06:00
|
|
|
.then(() => {
|
2023-10-26 19:57:28 +13:00
|
|
|
if (this.props.onTokenLoginCompleted) {
|
|
|
|
|
this.props.onTokenLoginCompleted();
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2019-07-08 12:43:16 -06:00
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
2021-10-15 16:30:53 +02:00
|
|
|
logger.error(e);
|
2022-02-23 09:22:37 -07:00
|
|
|
this.setState({ busy: false, loginView: LoginView.Unsupported });
|
2023-10-26 19:57:28 +13:00
|
|
|
return false;
|
2019-07-08 12:43:16 -06:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
private renderPasswordForm(introText: Optional<string>): JSX.Element {
|
2023-03-10 14:55:06 +00:00
|
|
|
let error: JSX.Element | undefined;
|
2022-02-23 09:22:37 -07:00
|
|
|
if (this.state.errorText) {
|
|
|
|
|
error = <span className="mx_Login_error">{this.state.errorText}</span>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form onSubmit={this.onPasswordLogin}>
|
|
|
|
|
{introText ? <p>{introText}</p> : null}
|
|
|
|
|
{error}
|
|
|
|
|
<Field
|
|
|
|
|
type="password"
|
2023-08-22 18:47:33 +01:00
|
|
|
label={_t("common|password")}
|
2022-02-23 09:22:37 -07:00
|
|
|
onChange={this.onPasswordChange}
|
|
|
|
|
value={this.state.password}
|
|
|
|
|
disabled={this.state.busy}
|
|
|
|
|
/>
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
onClick={this.onPasswordLogin}
|
|
|
|
|
kind="primary"
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={this.state.busy}
|
|
|
|
|
>
|
2023-08-23 11:57:22 +01:00
|
|
|
{_t("action|sign_in")}
|
2022-02-23 09:22:37 -07:00
|
|
|
</AccessibleButton>
|
|
|
|
|
<AccessibleButton onClick={this.onForgotPassword} kind="link">
|
2023-09-19 07:17:31 +01:00
|
|
|
{_t("auth|forgot_password_prompt")}
|
2022-02-23 09:22:37 -07:00
|
|
|
</AccessibleButton>
|
|
|
|
|
</form>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderSsoForm(introText: Optional<string>): JSX.Element {
|
|
|
|
|
const loginType = this.state.loginView === LoginView.CAS ? "cas" : "sso";
|
2023-08-15 16:00:17 +01:00
|
|
|
const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as SSOFlow;
|
2022-02-23 09:22:37 -07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{introText ? <p>{introText}</p> : null}
|
|
|
|
|
<SSOButtons
|
2023-06-15 15:11:49 +01:00
|
|
|
matrixClient={MatrixClientPeg.safeGet()}
|
2022-02-23 09:22:37 -07:00
|
|
|
flow={flow}
|
|
|
|
|
loginType={loginType}
|
|
|
|
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
|
|
|
|
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
|
2023-01-27 11:06:10 +00:00
|
|
|
action={SSOAction.LOGIN}
|
2022-02-23 09:22:37 -07:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private renderSignInSection(): JSX.Element {
|
2022-02-23 09:22:37 -07:00
|
|
|
if (this.state.loginView === LoginView.Loading) {
|
2019-07-04 16:45:40 -06:00
|
|
|
return <Spinner />;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
if (this.state.loginView === LoginView.Password) {
|
2023-09-19 07:17:31 +01:00
|
|
|
return this.renderPasswordForm(_t("auth|soft_logout_intro_password"));
|
2019-07-04 16:45:40 -06:00
|
|
|
}
|
|
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
if (this.state.loginView === LoginView.SSO || this.state.loginView === LoginView.CAS) {
|
2023-09-19 07:17:31 +01:00
|
|
|
return this.renderSsoForm(_t("auth|soft_logout_intro_sso"));
|
2022-02-23 09:22:37 -07:00
|
|
|
}
|
2020-11-23 17:10:15 +00:00
|
|
|
|
2022-02-23 09:22:37 -07:00
|
|
|
if (this.state.loginView === LoginView.PasswordWithSocialSignOn) {
|
|
|
|
|
// We render both forms with no intro/error to ensure the layout looks reasonably
|
|
|
|
|
// okay enough.
|
|
|
|
|
//
|
|
|
|
|
// Note: "mx_AuthBody_centered" text taken from registration page.
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2023-09-19 07:17:31 +01:00
|
|
|
<p>{_t("auth|soft_logout_intro_sso")}</p>
|
2022-02-23 09:22:37 -07:00
|
|
|
{this.renderSsoForm(null)}
|
2022-08-01 08:31:14 +01:00
|
|
|
<h2 className="mx_AuthBody_centered">
|
2023-09-13 09:30:56 +01:00
|
|
|
{_t("auth|sso_or_username_password", {
|
2022-02-23 09:22:37 -07:00
|
|
|
ssoButtons: "",
|
|
|
|
|
usernamePassword: "",
|
|
|
|
|
}).trim()}
|
2022-08-01 08:31:14 +01:00
|
|
|
</h2>
|
2022-02-23 09:22:37 -07:00
|
|
|
{this.renderPasswordForm(null)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
2019-07-04 16:45:40 -06:00
|
|
|
}
|
|
|
|
|
|
2019-07-09 23:55:20 -06:00
|
|
|
// Default: assume unsupported/error
|
2023-09-19 07:17:31 +01:00
|
|
|
return <p>{_t("auth|soft_logout_intro_unsupported_auth")}</p>;
|
2019-07-04 16:45:40 -06:00
|
|
|
}
|
|
|
|
|
|
2023-02-13 17:01:43 +00:00
|
|
|
public render(): React.ReactNode {
|
2019-07-03 16:46:37 -06:00
|
|
|
return (
|
|
|
|
|
<AuthPage>
|
|
|
|
|
<AuthHeader />
|
|
|
|
|
<AuthBody>
|
2023-09-19 07:17:31 +01:00
|
|
|
<h1>{_t("auth|soft_logout_heading")}</h1>
|
2019-07-03 16:46:37 -06:00
|
|
|
|
2023-08-23 11:57:22 +01:00
|
|
|
<h2>{_t("action|sign_in")}</h2>
|
2021-07-19 22:43:11 +01:00
|
|
|
<div>{this.renderSignInSection()}</div>
|
2019-07-04 15:22:25 -06:00
|
|
|
|
2023-09-28 12:51:30 +05:30
|
|
|
<h2>{_t("auth|soft_logout_subheading")}</h2>
|
|
|
|
|
<p>{_t("auth|soft_logout_warning")}</p>
|
2019-07-04 15:22:25 -06:00
|
|
|
<div>
|
|
|
|
|
<AccessibleButton onClick={this.onClearAll} kind="danger">
|
2023-10-03 19:17:26 +01:00
|
|
|
{_t("auth|soft_logout|clear_data_button")}
|
2019-07-04 15:22:25 -06:00
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>
|
2019-07-03 16:46:37 -06:00
|
|
|
</AuthBody>
|
|
|
|
|
</AuthPage>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|