From 1eb60ef1c4742cce236c68dfd323bad3c4017730 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jul 2019 12:43:16 -0600 Subject: [PATCH 1/4] Support SSO for rehydrating a soft-logged-out session. Fixes https://github.com/vector-im/riot-web/issues/10238 --- src/components/structures/MatrixChat.js | 67 ++++++++++------ src/components/structures/auth/SoftLogout.js | 80 +++++++++++++++++++- 2 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a93492cd416..063276c3099 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -283,29 +283,32 @@ export default React.createClass({ } // the first thing to do is to try the token params in the query-string - Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { - if (loggedIn) { - this.props.onTokenLoginCompleted(); - - // don't do anything else until the page reloads - just stay in - // the 'loading' state. - return; - } + // if the session isn't soft logged out (ie: is a clean session being logged in) + if (!Lifecycle.isSoftLogout()) { + Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { + if (loggedIn) { + this.props.onTokenLoginCompleted(); + + // don't do anything else until the page reloads - just stay in + // the 'loading' state. + return; + } - // if the user has followed a login or register link, don't reanimate - // the old creds, but rather go straight to the relevant page - const firstScreen = this._screenAfterLogin ? - this._screenAfterLogin.screen : null; + // if the user has followed a login or register link, don't reanimate + // the old creds, but rather go straight to the relevant page + const firstScreen = this._screenAfterLogin ? + this._screenAfterLogin.screen : null; - if (firstScreen === 'login' || + if (firstScreen === 'login' || firstScreen === 'register' || firstScreen === 'forgot_password') { - this._showScreenAfterLogin(); - return; - } + this._showScreenAfterLogin(); + return; + } - return this._loadSession(); - }); + return this._loadSession(); + }); + } if (SettingsStore.getValue("showCookieBar")) { this.setState({ @@ -1250,10 +1253,7 @@ export default React.createClass({ this._screenAfterLogin = null; } else if (localStorage && localStorage.getItem('mx_last_room_id')) { // Before defaulting to directory, show the last viewed room - dis.dispatch({ - action: 'view_room', - room_id: localStorage.getItem('mx_last_room_id'), - }); + this._viewLastRoom(); } else { if (MatrixClientPeg.get().isGuest()) { dis.dispatch({action: 'view_welcome_page'}); @@ -1267,6 +1267,13 @@ export default React.createClass({ } }, + _viewLastRoom: function() { + dis.dispatch({ + action: 'view_room', + room_id: localStorage.getItem('mx_last_room_id'), + }); + }, + /** * Called when the session is logged out */ @@ -1565,6 +1572,17 @@ export default React.createClass({ action: 'start_password_recovery', params: params, }); + } else if (screen === 'soft_logout') { + if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId()) { + // Logged in - visit a room + this._viewLastRoom(); + } else { + // Ultimately triggers soft_logout if needed + dis.dispatch({ + action: 'start_login', + params: params, + }); + } } else if (screen == 'new') { dis.dispatch({ action: 'view_create_room', @@ -1957,7 +1975,10 @@ export default React.createClass({ if (this.state.view === VIEWS.SOFT_LOGOUT) { const SoftLogout = sdk.getComponent('structures.auth.SoftLogout'); return ( - + ); } diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index bd6a5912e2f..9342c6086e0 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import {_t} from '../../../languageHandler'; import sdk from '../../../index'; import dis from '../../../dispatcher'; @@ -24,6 +25,7 @@ import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; import MatrixClientPeg from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; +import url from 'url'; const LOGIN_VIEW = { LOADING: 1, @@ -41,7 +43,11 @@ const FLOWS_TO_VIEWS = { export default class SoftLogout extends React.Component { static propTypes = { - // Nothing. + // Query parameters from MatrixChat + realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken} + + // Called when the SSO login completes + onTokenLoginCompleted: PropTypes.func, }; constructor() { @@ -67,6 +73,7 @@ export default class SoftLogout extends React.Component { displayName, loginView: LOGIN_VIEW.LOADING, keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount) + ssoUrl: null, busy: false, password: "", @@ -75,6 +82,12 @@ export default class SoftLogout extends React.Component { } componentDidMount(): void { + // We've ended up here when we don't need to - navigate to login + if (!Lifecycle.isSoftLogout()) { + dis.dispatch({action: "on_logged_in"}); + return; + } + this._initLogin(); MatrixClientPeg.get().flagAllGroupSessionsForBackup().then(remaining => { @@ -95,6 +108,15 @@ export default class SoftLogout extends React.Component { }; async _initLogin() { + const requiredQueryParams = ['homeserver', 'loginToken']; + const hasAllParams = requiredQueryParams + .filter(p => Object.keys(this.props.realQueryParams).includes(p)).length === requiredQueryParams.length; + if (this.props.realQueryParams && hasAllParams) { + this.setState({loginView: LOGIN_VIEW.LOADING}); + this.trySsoLogin(); + return; + } + // Note: we don't use the existing Login class because it is heavily flow-based. We don't // care about login flows here, unless it is the single flow we support. const client = MatrixClientPeg.get(); @@ -102,6 +124,18 @@ export default class SoftLogout extends React.Component { const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED; this.setState({loginView: chosenView}); + + if (chosenView === LOGIN_VIEW.CAS || chosenView === LOGIN_VIEW.SSO) { + const client = MatrixClientPeg.get(); + + const appUrl = url.parse(window.location.href, true); + appUrl.hash = ""; // Clear #/soft_logout off the URL + appUrl.query["homeserver"] = client.getHomeserverUrl(); + appUrl.query["identityServer"] = client.getIdentityServerUrl(); + + const ssoUrl = client.getSsoLoginUrl(url.format(appUrl), chosenView === LOGIN_VIEW.CAS ? "cas" : "sso"); + this.setState({ssoUrl}); + } } onPasswordChange = (ev) => { @@ -152,6 +186,42 @@ export default class SoftLogout extends React.Component { }); }; + async trySsoLogin() { + this.setState({busy: true}); + + const hsUrl = this.props.realQueryParams['homeserver']; + const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl(); + const loginType = "m.login.token"; + const loginParams = { + token: this.props.realQueryParams['loginToken'], + device_id: MatrixClientPeg.get().getDeviceId(), + }; + + let credentials = null; + try { + credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); + } catch (e) { + console.error(e); + this.setState({busy: false, loginView: LOGIN_VIEW.UNSUPPORTED}); + return; + } + + Lifecycle.hydrateSession(credentials).then(() => { + if (this.props.onTokenLoginCompleted) this.props.onTokenLoginCompleted(); + }).catch((e) => { + console.error(e); + this.setState({busy: false, loginView: LOGIN_VIEW.UNSUPPORTED}); + }); + } + + onSsoLogin = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true}); + window.location.href = this.state.ssoUrl; + }; + _renderSignInSection() { if (this.state.loginView === LOGIN_VIEW.LOADING) { const Spinner = sdk.getComponent("elements.Spinner"); @@ -202,8 +272,12 @@ export default class SoftLogout extends React.Component { } if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) { - // TODO: TravisR - https://github.com/vector-im/riot-web/issues/10238 - return

PLACEHOLDER

; + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + {_t('Sign in with single sign-on')} + + ); } // Default: assume unsupported From 041379fa3c07ca622e6e0b9ebe6c14a098253ec9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jul 2019 23:44:14 -0600 Subject: [PATCH 2/4] Don't refuse the soft logout page if the user is soft logged out --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 063276c3099..db5b7d034b3 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1573,7 +1573,7 @@ export default React.createClass({ params: params, }); } else if (screen === 'soft_logout') { - if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId()) { + if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId() && !Lifecycle.isSoftLogout()) { // Logged in - visit a room this._viewLastRoom(); } else { From 2ca6633fdaabea39e40e8880202ed78b61a4933b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jul 2019 23:55:20 -0600 Subject: [PATCH 3/4] Update copy as per design --- src/components/structures/auth/SoftLogout.js | 34 +++++++++++++------- src/i18n/strings/en_EN.json | 9 +++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 9342c6086e0..c58754e9f51 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -228,6 +228,13 @@ export default class SoftLogout extends React.Component { return ; } + let introText = null; // null is translated to something area specific in this function + if (this.state.keyBackupNeeded) { + introText = _t( + "Regain access to your account and recover encryption keys stored on this device. " + + "Without them, you won’t be able to read all of your secure messages on any device."); + } + if (this.state.loginView === LOGIN_VIEW.PASSWORD) { const Field = sdk.getComponent("elements.Field"); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -237,12 +244,9 @@ export default class SoftLogout extends React.Component { error = {this.state.errorText}; } - let introText = _t("Enter your password to sign in and regain access to your account."); - if (this.state.keyBackupNeeded) { - introText = _t( - "Regain access your account and recover encryption keys stored on this device. " + - "Without them, you won’t be able to read all of your secure messages on any device."); - } + if (!introText) { + introText = _t("Enter your password to sign in and regain access to your account."); + } // else we already have a message and should use it (key backup warning) return (
@@ -273,18 +277,26 @@ export default class SoftLogout extends React.Component { if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + if (!introText) { + introText = _t("Sign in and regain access to your account."); + } // else we already have a message and should use it (key backup warning) + return ( - - {_t('Sign in with single sign-on')} - +
+

{introText}

+ + {_t('Sign in with single sign-on')} + +
); } - // Default: assume unsupported + // Default: assume unsupported/error return (

{_t( - "Cannot re-authenticate with your account. Please contact your " + + "You cannot sign in to your account. Please contact your " + "homeserver admin for more information.", )}

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c2e9d760b76..449c6ce6bcf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -93,7 +93,6 @@ "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", "Unnamed Room": "Unnamed Room", "Error": "Error", - "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", @@ -928,6 +927,7 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", + "View Source": "View Source", "Error decrypting audio": "Error decrypting audio", "Reply": "Reply", "Edit": "Edit", @@ -1127,6 +1127,7 @@ "Start chatting": "Start chatting", "Click on the button below to start chatting!": "Click on the button below to start chatting!", "Start Chatting": "Start Chatting", + "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)", "Removing…": "Removing…", "Confirm Removal": "Confirm Removal", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", @@ -1319,7 +1320,6 @@ "Cancel Sending": "Cancel Sending", "Forward Message": "Forward Message", "Pin Message": "Pin Message", - "View Source": "View Source", "View Decrypted Source": "View Decrypted Source", "Unhide Preview": "Unhide Preview", "Share Permalink": "Share Permalink", @@ -1589,10 +1589,11 @@ "Create your account": "Create your account", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Failed to re-authenticate": "Failed to re-authenticate", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.", "Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.", - "Regain access your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Regain access your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.", "Forgotten your password?": "Forgotten your password?", - "Cannot re-authenticate with your account. Please contact your homeserver admin for more information.": "Cannot re-authenticate with your account. Please contact your homeserver admin for more information.", + "Sign in and regain access to your account.": "Sign in and regain access to your account.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "You cannot sign in to your account. Please contact your homeserver admin for more information.", "You're signed out": "You're signed out", "Your homeserver (%(domainName)s) admin has signed you out of your account %(displayName)s (%(userId)s).": "Your homeserver (%(domainName)s) admin has signed you out of your account %(displayName)s (%(userId)s).", "Clear personal data": "Clear personal data", From ce11eff1b8b8c28d01268e39b122cf1e8def8424 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 10 Jul 2019 08:01:32 -0600 Subject: [PATCH 4/4] Simplify parameter check --- src/components/structures/auth/SoftLogout.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index c58754e9f51..8bfa458c281 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -108,10 +108,9 @@ export default class SoftLogout extends React.Component { }; async _initLogin() { - const requiredQueryParams = ['homeserver', 'loginToken']; - const hasAllParams = requiredQueryParams - .filter(p => Object.keys(this.props.realQueryParams).includes(p)).length === requiredQueryParams.length; - if (this.props.realQueryParams && hasAllParams) { + const queryParams = this.props.realQueryParams; + const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken']; + if (hasAllParams) { this.setState({loginView: LOGIN_VIEW.LOADING}); this.trySsoLogin(); return;