From 1eb60ef1c4742cce236c68dfd323bad3c4017730 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jul 2019 12:43:16 -0600 Subject: [PATCH] Support SSO for rehydrating a soft-logged-out session. Fixes https://github.com/vector-im/riot-web/issues/10238 --- src/components/structures/MatrixChat.js | 65 ++++++++++------ src/components/structures/auth/SoftLogout.js | 80 +++++++++++++++++++- 2 files changed, 120 insertions(+), 25 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a93492cd41..063276c309 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(); + // 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; - } + // 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 bd6a5912e2..9342c6086e 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