2019-07-29 11:58:47 +01:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2019-07-29 11:58:47 +01:00
|
|
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
|
|
|
|
2025-01-06 11:18:54 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-09 14:57:16 +01:00
|
|
|
Please see LICENSE files in the repository root for full details.
|
2019-07-29 11:58:47 +01:00
|
|
|
*/
|
|
|
|
|
|
2021-09-27 09:54:55 +02:00
|
|
|
import React from "react";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { SERVICE_TYPES, createClient, type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
2021-10-22 17:23:32 -05:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2019-08-01 12:46:59 +01:00
|
|
|
|
2021-06-29 13:11:58 +01:00
|
|
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
2019-10-31 11:58:31 +00:00
|
|
|
import Modal from "./Modal";
|
|
|
|
|
import { _t } from "./languageHandler";
|
2019-08-02 15:21:53 +01:00
|
|
|
import { Service, startTermsFlow, TermsNotSignedError } from "./Terms";
|
2019-10-31 11:58:31 +00:00
|
|
|
import {
|
|
|
|
|
doesAccountDataHaveIdentityServer,
|
|
|
|
|
doesIdentityServerHaveTerms,
|
2022-07-29 12:01:15 +01:00
|
|
|
setToDefaultIdentityServer,
|
2019-10-31 11:58:31 +00:00
|
|
|
} from "./utils/IdentityServerUtils";
|
2021-09-27 09:54:55 +02:00
|
|
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
|
|
|
|
import { abbreviateUrl } from "./utils/UrlUtils";
|
2021-09-21 17:48:09 +02:00
|
|
|
|
2019-10-31 11:58:31 +00:00
|
|
|
export class AbortedIdentityActionError extends Error {}
|
2019-07-29 11:58:47 +01:00
|
|
|
|
|
|
|
|
export default class IdentityAuthClient {
|
2023-05-05 09:11:14 +01:00
|
|
|
private accessToken: string | null = null;
|
|
|
|
|
private tempClient?: MatrixClient;
|
2021-09-28 07:48:56 +02:00
|
|
|
private authEnabled = true;
|
2021-09-27 09:54:55 +02:00
|
|
|
|
2019-08-15 15:59:44 -06:00
|
|
|
/**
|
|
|
|
|
* Creates a new identity auth client
|
|
|
|
|
* @param {string} identityUrl The URL to contact the identity server with.
|
|
|
|
|
* When provided, this class will operate solely within memory, refusing to
|
|
|
|
|
* persist any information such as tokens. Default null (not provided).
|
|
|
|
|
*/
|
2022-12-16 12:29:59 +00:00
|
|
|
public constructor(identityUrl?: string) {
|
2019-08-15 15:59:44 -06:00
|
|
|
if (identityUrl) {
|
|
|
|
|
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
|
|
|
|
// do identity server auth. The functions don't take an identity URL
|
|
|
|
|
// though, and making all of them take one could lead to developer
|
|
|
|
|
// confusion about what the idBaseUrl does on a client. Therefore, we
|
|
|
|
|
// just make a new client and live with it.
|
2019-08-19 10:25:20 -06:00
|
|
|
this.tempClient = createClient({
|
2019-08-15 15:59:44 -06:00
|
|
|
baseUrl: "", // invalid by design
|
|
|
|
|
idBaseUrl: identityUrl,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-08 10:27:03 +00:00
|
|
|
// This client must not be used for general operations as it may not have a baseUrl or be running (tempClient).
|
|
|
|
|
private get identityClient(): MatrixClient {
|
|
|
|
|
return this.tempClient ?? this.matrixClient;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-27 09:54:55 +02:00
|
|
|
private get matrixClient(): MatrixClient {
|
2024-01-08 10:27:03 +00:00
|
|
|
return MatrixClientPeg.safeGet();
|
2019-08-15 15:59:44 -06:00
|
|
|
}
|
|
|
|
|
|
2021-09-27 09:54:55 +02:00
|
|
|
private writeToken(): void {
|
2019-08-15 15:59:44 -06:00
|
|
|
if (this.tempClient) return; // temporary client: ignore
|
2023-05-25 09:39:23 +01:00
|
|
|
if (this.accessToken) {
|
|
|
|
|
window.localStorage.setItem("mx_is_access_token", this.accessToken);
|
|
|
|
|
} else {
|
|
|
|
|
window.localStorage.removeItem("mx_is_access_token");
|
|
|
|
|
}
|
2019-08-15 15:59:44 -06:00
|
|
|
}
|
|
|
|
|
|
2023-02-16 17:21:44 +00:00
|
|
|
private readToken(): string | null {
|
2019-08-15 15:59:44 -06:00
|
|
|
if (this.tempClient) return null; // temporary client: ignore
|
|
|
|
|
return window.localStorage.getItem("mx_is_access_token");
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns a promise that resolves to the access_token string from the IS
|
2023-02-16 17:21:44 +00:00
|
|
|
public async getAccessToken({ check = true } = {}): Promise<string | null> {
|
2019-07-29 14:41:57 +01:00
|
|
|
if (!this.authEnabled) {
|
|
|
|
|
// The current IS doesn't support authentication
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 17:21:44 +00:00
|
|
|
let token: string | null = this.accessToken;
|
2019-07-29 11:58:47 +01:00
|
|
|
if (!token) {
|
2021-09-27 09:54:55 +02:00
|
|
|
token = this.readToken();
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
2019-08-19 22:54:23 -06:00
|
|
|
token = await this.registerForToken(check);
|
2019-08-02 15:21:53 +01:00
|
|
|
if (token) {
|
|
|
|
|
this.accessToken = token;
|
2021-09-27 09:54:55 +02:00
|
|
|
this.writeToken();
|
2019-08-02 15:21:53 +01:00
|
|
|
}
|
|
|
|
|
return token;
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
|
|
|
|
|
2019-08-19 22:54:23 -06:00
|
|
|
if (check) {
|
|
|
|
|
try {
|
2021-09-27 09:54:55 +02:00
|
|
|
await this.checkToken(token);
|
2019-08-19 22:54:23 -06:00
|
|
|
} catch (e) {
|
2019-10-31 11:58:31 +00:00
|
|
|
if (e instanceof TermsNotSignedError || e instanceof AbortedIdentityActionError) {
|
2019-08-19 22:54:23 -06:00
|
|
|
// Retrying won't help this
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
// Retry in case token expired
|
|
|
|
|
token = await this.registerForToken();
|
|
|
|
|
if (token) {
|
|
|
|
|
this.accessToken = token;
|
2021-09-27 09:54:55 +02:00
|
|
|
this.writeToken();
|
2019-08-19 22:54:23 -06:00
|
|
|
}
|
2019-08-02 15:21:53 +01:00
|
|
|
}
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
2019-07-29 14:41:57 +01:00
|
|
|
|
|
|
|
|
return token;
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
|
|
|
|
|
2021-09-27 09:54:55 +02:00
|
|
|
private async checkToken(token: string): Promise<void> {
|
2024-01-08 10:27:03 +00:00
|
|
|
const identityServerUrl = this.identityClient.getIdentityServerUrl()!;
|
2019-10-31 11:58:31 +00:00
|
|
|
|
2019-08-01 12:46:59 +01:00
|
|
|
try {
|
2024-01-08 10:27:03 +00:00
|
|
|
await this.identityClient.getIdentityAccount(token);
|
2019-08-01 12:46:59 +01:00
|
|
|
} catch (e) {
|
2023-05-16 14:25:43 +01:00
|
|
|
if (e instanceof MatrixError && e.errcode === "M_TERMS_NOT_SIGNED") {
|
2021-09-21 17:48:09 +02:00
|
|
|
logger.log("Identity server requires new terms to be agreed to");
|
2023-06-01 14:43:24 +01:00
|
|
|
await startTermsFlow(this.matrixClient, [new Service(SERVICE_TYPES.IS, identityServerUrl, token)]);
|
2019-08-01 12:46:59 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
2019-07-30 16:56:19 +01:00
|
|
|
|
2019-10-31 11:58:31 +00:00
|
|
|
if (
|
|
|
|
|
!this.tempClient &&
|
2023-05-23 16:24:12 +01:00
|
|
|
!doesAccountDataHaveIdentityServer(this.matrixClient) &&
|
|
|
|
|
!(await doesIdentityServerHaveTerms(this.matrixClient, identityServerUrl))
|
2019-10-31 11:58:31 +00:00
|
|
|
) {
|
2022-06-14 17:51:51 +01:00
|
|
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
2023-09-22 16:39:40 +01:00
|
|
|
title: _t("terms|identity_server_no_terms_title"),
|
2022-06-14 17:51:51 +01:00
|
|
|
description: (
|
|
|
|
|
<div>
|
|
|
|
|
<p>
|
|
|
|
|
{_t(
|
2023-09-22 16:39:40 +01:00
|
|
|
"terms|identity_server_no_terms_description_1",
|
2022-06-14 17:51:51 +01:00
|
|
|
{},
|
|
|
|
|
{
|
2024-09-13 12:09:41 +01:00
|
|
|
server: () => <strong>{abbreviateUrl(identityServerUrl)}</strong>,
|
2022-06-14 17:51:51 +01:00
|
|
|
},
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
2023-09-22 16:39:40 +01:00
|
|
|
<p>{_t("terms|identity_server_no_terms_description_2")}</p>
|
2022-06-14 17:51:51 +01:00
|
|
|
</div>
|
|
|
|
|
),
|
2023-08-23 11:57:22 +01:00
|
|
|
button: _t("action|trust"),
|
2022-06-14 17:51:51 +01:00
|
|
|
});
|
2019-10-31 11:58:31 +00:00
|
|
|
const [confirmed] = await finished;
|
|
|
|
|
if (confirmed) {
|
2023-05-23 16:24:12 +01:00
|
|
|
setToDefaultIdentityServer(this.matrixClient);
|
2019-10-31 11:58:31 +00:00
|
|
|
} else {
|
|
|
|
|
throw new AbortedIdentityActionError("User aborted identity server action without terms");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-31 17:30:10 +01:00
|
|
|
// We should ensure the token in `localStorage` is cleared
|
2019-07-30 10:09:38 +01:00
|
|
|
// appropriately. We already clear storage on sign out, but we'll need
|
|
|
|
|
// additional clearing when changing ISes in settings as part of future
|
|
|
|
|
// privacy work.
|
2020-08-03 16:02:26 +01:00
|
|
|
// See also https://github.com/vector-im/element-web/issues/10455.
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
|
|
|
|
|
2021-09-27 09:54:55 +02:00
|
|
|
public async registerForToken(check = true): Promise<string> {
|
2023-06-21 17:29:44 +01:00
|
|
|
const hsOpenIdToken = await MatrixClientPeg.safeGet().getOpenIdToken();
|
2020-03-09 17:05:13 -06:00
|
|
|
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
2024-01-08 10:27:03 +00:00
|
|
|
const { access_token: accessToken, token } =
|
|
|
|
|
await this.identityClient.registerWithIdentityServer(hsOpenIdToken);
|
2024-10-16 17:38:22 +01:00
|
|
|
const identityAccessToken = token || accessToken;
|
2021-09-27 09:54:55 +02:00
|
|
|
if (check) await this.checkToken(identityAccessToken);
|
2020-03-09 17:05:13 -06:00
|
|
|
return identityAccessToken;
|
2019-07-29 11:58:47 +01:00
|
|
|
}
|
|
|
|
|
}
|