2019-07-29 10:58:47 +00:00
|
|
|
/*
|
|
|
|
Copyright 2019 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.
|
|
|
|
*/
|
|
|
|
|
2019-08-19 16:25:20 +00:00
|
|
|
import { createClient, SERVICE_TYPES } from 'matrix-js-sdk';
|
2019-08-01 11:46:59 +00:00
|
|
|
|
2019-07-29 10:58:47 +00:00
|
|
|
import MatrixClientPeg from './MatrixClientPeg';
|
2019-10-31 11:58:31 +00:00
|
|
|
import Modal from './Modal';
|
|
|
|
import sdk from './index';
|
|
|
|
import { _t } from './languageHandler';
|
2019-08-02 14:21:53 +00:00
|
|
|
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
2019-10-31 11:58:31 +00:00
|
|
|
import {
|
|
|
|
doesAccountDataHaveIdentityServer,
|
|
|
|
doesIdentityServerHaveTerms,
|
|
|
|
useDefaultIdentityServer,
|
|
|
|
} from './utils/IdentityServerUtils';
|
|
|
|
import { abbreviateUrl } from './utils/UrlUtils';
|
|
|
|
|
|
|
|
export class AbortedIdentityActionError extends Error {}
|
2019-07-29 10:58:47 +00:00
|
|
|
|
|
|
|
export default class IdentityAuthClient {
|
2019-08-15 21:59:44 +00: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).
|
|
|
|
*/
|
|
|
|
constructor(identityUrl = null) {
|
2019-07-29 10:58:47 +00:00
|
|
|
this.accessToken = null;
|
2019-07-29 13:41:57 +00:00
|
|
|
this.authEnabled = true;
|
2019-08-15 21:59:44 +00: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 16:25:20 +00:00
|
|
|
this.tempClient = createClient({
|
2019-08-15 21:59:44 +00:00
|
|
|
baseUrl: "", // invalid by design
|
|
|
|
idBaseUrl: identityUrl,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Indicates that we're using the real client, not some workaround.
|
|
|
|
this.tempClient = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get _matrixClient() {
|
|
|
|
return this.tempClient ? this.tempClient : MatrixClientPeg.get();
|
|
|
|
}
|
|
|
|
|
|
|
|
_writeToken() {
|
|
|
|
if (this.tempClient) return; // temporary client: ignore
|
2019-08-15 22:08:18 +00:00
|
|
|
window.localStorage.setItem("mx_is_access_token", this.accessToken);
|
2019-08-15 21:59:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_readToken() {
|
|
|
|
if (this.tempClient) return null; // temporary client: ignore
|
|
|
|
return window.localStorage.getItem("mx_is_access_token");
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hasCredentials() {
|
|
|
|
return this.accessToken != null; // undef or null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a promise that resolves to the access_token string from the IS
|
2019-09-11 12:36:10 +00:00
|
|
|
async getAccessToken({ check = true } = {}) {
|
2019-07-29 13:41:57 +00:00
|
|
|
if (!this.authEnabled) {
|
|
|
|
// The current IS doesn't support authentication
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-07-29 10:58:47 +00:00
|
|
|
let token = this.accessToken;
|
|
|
|
if (!token) {
|
2019-08-15 21:59:44 +00:00
|
|
|
token = this._readToken();
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!token) {
|
2019-08-20 04:54:23 +00:00
|
|
|
token = await this.registerForToken(check);
|
2019-08-02 14:21:53 +00:00
|
|
|
if (token) {
|
|
|
|
this.accessToken = token;
|
2019-08-15 21:59:44 +00:00
|
|
|
this._writeToken();
|
2019-08-02 14:21:53 +00:00
|
|
|
}
|
|
|
|
return token;
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
|
|
|
|
2019-08-20 04:54:23 +00:00
|
|
|
if (check) {
|
|
|
|
try {
|
|
|
|
await this._checkToken(token);
|
|
|
|
} catch (e) {
|
2019-10-31 11:58:31 +00:00
|
|
|
if (
|
|
|
|
e instanceof TermsNotSignedError ||
|
|
|
|
e instanceof AbortedIdentityActionError
|
|
|
|
) {
|
2019-08-20 04:54:23 +00:00
|
|
|
// Retrying won't help this
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
// Retry in case token expired
|
|
|
|
token = await this.registerForToken();
|
|
|
|
if (token) {
|
|
|
|
this.accessToken = token;
|
|
|
|
this._writeToken();
|
|
|
|
}
|
2019-08-02 14:21:53 +00:00
|
|
|
}
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
2019-07-29 13:41:57 +00:00
|
|
|
|
|
|
|
return token;
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
|
|
|
|
2019-07-31 16:30:10 +00:00
|
|
|
async _checkToken(token) {
|
2019-10-31 11:58:31 +00:00
|
|
|
const identityServerUrl = this._matrixClient.getIdentityServerUrl();
|
|
|
|
|
2019-08-01 11:46:59 +00:00
|
|
|
try {
|
2019-08-15 21:59:44 +00:00
|
|
|
await this._matrixClient.getIdentityAccount(token);
|
2019-08-01 11:46:59 +00:00
|
|
|
} catch (e) {
|
|
|
|
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
|
|
|
console.log("Identity Server requires new terms to be agreed to");
|
|
|
|
await startTermsFlow([new Service(
|
2019-08-02 13:43:36 +00:00
|
|
|
SERVICE_TYPES.IS,
|
2019-10-31 11:58:31 +00:00
|
|
|
identityServerUrl,
|
2019-08-01 11:46:59 +00:00
|
|
|
token,
|
|
|
|
)]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
2019-07-30 15:56:19 +00:00
|
|
|
|
2019-10-31 11:58:31 +00:00
|
|
|
if (
|
|
|
|
!this.tempClient &&
|
|
|
|
!doesAccountDataHaveIdentityServer() &&
|
|
|
|
!await doesIdentityServerHaveTerms(identityServerUrl)
|
|
|
|
) {
|
|
|
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
|
|
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
|
|
|
QuestionDialog, {
|
|
|
|
title: _t("Identity server has no terms of service"),
|
|
|
|
description: <div>
|
|
|
|
<p>{_t(
|
|
|
|
"This action requires accessing the default identity server " +
|
|
|
|
"<server /> to validate an email address or phone number, but the server " +
|
|
|
|
"does not have any terms of service.", {},
|
|
|
|
{
|
|
|
|
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>,
|
|
|
|
},
|
|
|
|
)}</p>
|
|
|
|
<p>{_t(
|
|
|
|
"Only continue if you trust the owner of the server.",
|
|
|
|
)}</p>
|
|
|
|
</div>,
|
|
|
|
button: _t("Trust"),
|
|
|
|
});
|
|
|
|
const [confirmed] = await finished;
|
|
|
|
if (confirmed) {
|
|
|
|
useDefaultIdentityServer();
|
|
|
|
} else {
|
|
|
|
throw new AbortedIdentityActionError(
|
|
|
|
"User aborted identity server action without terms",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-31 16:30:10 +00:00
|
|
|
// We should ensure the token in `localStorage` is cleared
|
2019-07-30 09:09:38 +00: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.
|
2019-07-30 16:19:59 +00:00
|
|
|
// See also https://github.com/vector-im/riot-web/issues/10455.
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
|
|
|
|
2019-08-20 04:54:23 +00:00
|
|
|
async registerForToken(check=true) {
|
2019-07-29 13:41:57 +00:00
|
|
|
try {
|
|
|
|
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
2019-07-30 09:05:57 +00:00
|
|
|
const { access_token: identityAccessToken } =
|
2019-08-15 21:59:44 +00:00
|
|
|
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
2019-08-20 04:54:23 +00:00
|
|
|
if (check) await this._checkToken(identityAccessToken);
|
2019-07-30 09:05:57 +00:00
|
|
|
return identityAccessToken;
|
2019-08-02 14:21:53 +00:00
|
|
|
} catch (e) {
|
|
|
|
if (e.cors === "rejected" || e.httpStatus === 404) {
|
2019-07-29 13:41:57 +00:00
|
|
|
// Assume IS only supports deprecated v1 API for now
|
|
|
|
// TODO: Remove this path once v2 is only supported version
|
2019-07-30 09:39:07 +00:00
|
|
|
// See https://github.com/vector-im/riot-web/issues/10443
|
2019-07-29 13:41:57 +00:00
|
|
|
console.warn("IS doesn't support v2 auth");
|
|
|
|
this.authEnabled = false;
|
2019-07-31 16:30:10 +00:00
|
|
|
return;
|
2019-07-29 13:41:57 +00:00
|
|
|
}
|
2019-08-02 14:21:53 +00:00
|
|
|
throw e;
|
2019-07-29 13:41:57 +00:00
|
|
|
}
|
2019-07-29 10:58:47 +00:00
|
|
|
}
|
|
|
|
}
|