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

Add support for IS v2 API with authentication #3256

Merged
merged 14 commits into from
Jul 30, 2019
36 changes: 21 additions & 15 deletions src/AddThreepid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
92 changes: 92 additions & 0 deletions src/IdentityAuthClient.js
Original file line number Diff line number Diff line change
@@ -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
jryans marked this conversation as resolved.
Show resolved Hide resolved
// 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;
}
}
}
}
57 changes: 36 additions & 21 deletions src/components/views/dialogs/AddressPickerDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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
Expand All @@ -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);
jryans marked this conversation as resolved.
Show resolved Hide resolved
this.setState({
searchError: _t('Something went wrong!'),
});
}
},

_getFilteredSuggestions: function() {
Expand Down Expand Up @@ -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 = <div className="mx_AddressPickerDialog_error">
{ _t("You have entered an invalid address.") }
Expand Down
22 changes: 14 additions & 8 deletions src/components/views/rooms/RoomPreviewBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import IdentityAuthClient from '../../../IdentityAuthClient';

const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
Expand Down Expand Up @@ -104,21 +105,26 @@ module.exports = React.createClass({
}
},

_checkInvitedEmail: function() {
_checkInvitedEmail: async function() {
// If this is an invite and we've been told what email
// address was invited, fetch the user's list of Threepids
// so we can check them against the one that was invited
if (this.props.inviterName && this.props.invitedEmail) {
this.setState({busy: true});
MatrixClientPeg.get().lookupThreePid(
'email', this.props.invitedEmail,
).finally(() => {
this.setState({busy: false});
}).done((result) => {
try {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
const result = await MatrixClientPeg.get().lookupThreePid(
'email',
this.props.invitedEmail,
undefined /* callback */,
identityAccessToken,
);
this.setState({invitedEmailMxid: result.mxid});
}, (err) => {
} catch (err) {
this.setState({threePidFetchError: err});
});
}
this.setState({busy: false});
}
},

Expand Down