Merge pull request #3256 from matrix-org/jryans/is-v2-auth

Add support for IS v2 API with authentication
pull/21833/head
J. Ryan Stinnett 2019-07-30 18:13:15 +01:00 committed by GitHub
commit a9a33f5dcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 44 deletions

View File

@ -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);
}
}

92
src/IdentityAuthClient.js Normal file
View File

@ -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;
}
}
}
}

View File

@ -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;
await Promise.delay(500);
if (cancelled) return null;
try {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
if (cancelled) return null;
return MatrixClientPeg.get().getProfileInfo(res.mxid);
}).then((res) => {
if (res === null) return null;
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 = <div className="mx_AddressPickerDialog_error">
{ _t("You have entered an invalid address.") }

View File

@ -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",
@ -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});
}
},