diff --git a/src/AddThreepid.js b/src/AddThreepid.js index adeaf84a69d..8bd30990024 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -17,6 +17,7 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import { _t } from './languageHandler'; +import IdentityAuthClient from './IdentityAuthClient'; /** * Allows a user to add a third party identifier to their homeserver and, @@ -103,24 +104,29 @@ export default class AddThreepid { /** * Takes a phone number verification code as entered by the user and validates * it with the ID server, then if successful, adds the phone number. - * @param {string} token phone number verification code as entered by the user + * @param {string} msisdnToken phone number verification code as entered by the user * @return {Promise} Resolves if the phone number was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why * the request failed. */ - haveMsisdnToken(token) { - return MatrixClientPeg.get().submitMsisdnToken( - this.sessionId, this.clientSecret, token, - ).then((result) => { - if (result.errcode) { - throw result; - } - const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; - return MatrixClientPeg.get().addThreePid({ - sid: this.sessionId, - client_secret: this.clientSecret, - id_server: identityServerDomain, - }, this.bind); - }); + async haveMsisdnToken(msisdnToken) { + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); + const result = await MatrixClientPeg.get().submitMsisdnToken( + this.sessionId, + this.clientSecret, + msisdnToken, + identityAccessToken, + ); + if (result.errcode) { + throw result; + } + + const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; + return MatrixClientPeg.get().addThreePid({ + sid: this.sessionId, + client_secret: this.clientSecret, + id_server: identityServerDomain, + }, this.bind); } } diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js new file mode 100644 index 00000000000..133cca774b2 --- /dev/null +++ b/src/IdentityAuthClient.js @@ -0,0 +1,92 @@ +/* +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. +*/ + +import MatrixClientPeg from './MatrixClientPeg'; + +export default class IdentityAuthClient { + constructor() { + this.accessToken = null; + this.authEnabled = true; + } + + hasCredentials() { + return this.accessToken != null; // undef or null + } + + // Returns a promise that resolves to the access_token string from the IS + async getAccessToken() { + if (!this.authEnabled) { + // The current IS doesn't support authentication + return null; + } + + let token = this.accessToken; + if (!token) { + token = window.localStorage.getItem("mx_is_access_token"); + } + + if (!token) { + token = await this.registerForToken(); + this.accessToken = token; + window.localStorage.setItem("mx_is_access_token", token); + } + + try { + await this._checkToken(token); + } catch (e) { + // Retry in case token expired + token = await this.registerForToken(); + this.accessToken = token; + window.localStorage.setItem("mx_is_access_token", token); + } + + return token; + } + + _checkToken(token) { + // TODO: Test current API token via `/account` endpoint + + // At the moment, Sydent doesn't implement `/account`, so we can't use + // that yet. We could try a lookup for a null address perhaps...? + // Sydent doesn't currently expire tokens, but we should still be testing + // them in any case. + // See also https://github.com/vector-im/riot-web/issues/10452. + + // In any case, we should ensure the token in `localStorage` is cleared + // 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. + // See also https://github.com/vector-im/riot-web/issues/10455. + } + + async registerForToken() { + try { + const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); + const { access_token: identityAccessToken } = + await MatrixClientPeg.get().registerWithIdentityServer(hsOpenIdToken); + await this._checkToken(identityAccessToken); + return identityAccessToken; + } catch (err) { + if (err.cors === "rejected" || err.httpStatus === 404) { + // Assume IS only supports deprecated v1 API for now + // TODO: Remove this path once v2 is only supported version + // See https://github.com/vector-im/riot-web/issues/10443 + console.warn("IS doesn't support v2 auth"); + this.authEnabled = false; + } + } + } +} diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 5c004deebd5..6cb5a278fd5 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018, 2019 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +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. @@ -18,13 +19,15 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; + import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; -import * as Email from "../../../email"; +import * as Email from '../../../email'; +import IdentityAuthClient from '../../../IdentityAuthClient'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -71,12 +74,11 @@ module.exports = React.createClass({ getInitialState: function() { return { - error: false, - + // Whether to show an error message because of an invalid address + invalidAddressError: false, // List of UserAddressType objects representing // the list of addresses we're going to invite selectedList: [], - // Whether a search is ongoing busy: false, // An error message generated during the user directory search @@ -443,12 +445,12 @@ module.exports = React.createClass({ }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); if (addrType === 'email') { - this._lookupThreepid(addrType, query).done(); + this._lookupThreepid(addrType, query); } } this.setState({ suggestedList, - error: false, + invalidAddressError: false, }, () => { if (this.addressSelector) this.addressSelector.moveSelectionTop(); }); @@ -492,13 +494,13 @@ module.exports = React.createClass({ selectedList, suggestedList: [], query: "", - error: hasError ? true : this.state.error, + invalidAddressError: hasError ? true : this.state.invalidAddressError, }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); return hasError ? null : selectedList; }, - _lookupThreepid: function(medium, address) { + _lookupThreepid: async function(medium, address) { let cancelled = false; // Note that we can't safely remove this after we're done // because we don't know that it's the same one, so we just @@ -509,28 +511,41 @@ module.exports = React.createClass({ }; // wait a bit to let the user finish typing - return Promise.delay(500).then(() => { - if (cancelled) return null; - return MatrixClientPeg.get().lookupThreePid(medium, address); - }).then((res) => { - if (res === null || !res.mxid) return null; - if (cancelled) return null; + await Promise.delay(500); + if (cancelled) return null; - return MatrixClientPeg.get().getProfileInfo(res.mxid); - }).then((res) => { - if (res === null) return null; + try { + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); if (cancelled) return null; + + const lookup = await MatrixClientPeg.get().lookupThreePid( + medium, + address, + undefined /* callback */, + identityAccessToken, + ); + if (cancelled || lookup === null || !lookup.mxid) return null; + + const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); + if (cancelled || profile === null) return null; + this.setState({ suggestedList: [{ // a UserAddressType addressType: medium, address: address, - displayName: res.displayname, - avatarMxc: res.avatar_url, + displayName: profile.displayname, + avatarMxc: profile.avatar_url, isKnown: true, }], }); - }); + } catch (e) { + console.error(e); + this.setState({ + searchError: _t('Something went wrong!'), + }); + } }, _getFilteredSuggestions: function() { @@ -597,7 +612,7 @@ module.exports = React.createClass({ let error; let addressSelector; - if (this.state.error) { + if (this.state.invalidAddressError) { const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t])); error =