diff --git a/src/BasePlatform.js b/src/BasePlatform.js index a935f4a767..5d809eb28f 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -4,6 +4,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2020 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,6 +19,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {MatrixClient} from "matrix-js-sdk"; import dis from './dispatcher'; import BaseEventIndexManager from './indexing/BaseEventIndexManager'; @@ -164,4 +166,26 @@ export default class BasePlatform { } setLanguage(preferredLangs: string[]) {} + + getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { + const url = new URL(window.location.href); + // XXX: at this point, the fragment will always be #/login, which is no + // use to anyone. Ideally, we would get the intended fragment from + // MatrixChat.screenAfterLogin so that you could follow #/room links etc + // through an SSO login. + url.hash = ""; + url.searchParams.set("homeserver", hsUrl); + url.searchParams.set("identityServer", isUrl); + return url; + } + + /** + * Begin Single Sign On flows. + * @param {MatrixClient} mxClient the matrix client using which we should start the flow + * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. + */ + startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") { + const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); + window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO + } } diff --git a/src/Login.js b/src/Login.js index d9ce8adaaa..1590e5ac28 100644 --- a/src/Login.js +++ b/src/Login.js @@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 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. @@ -19,8 +20,6 @@ limitations under the License. import Matrix from "matrix-js-sdk"; -import url from 'url'; - export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { this._hsUrl = hsUrl; @@ -29,6 +28,7 @@ export default class Login { this._currentFlowIndex = 0; this._flows = []; this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; + this._tempClient = null; // memoize } getHomeserverUrl() { @@ -40,10 +40,12 @@ export default class Login { } setHomeserverUrl(hsUrl) { + this._tempClient = null; // clear memoization this._hsUrl = hsUrl; } setIdentityServerUrl(isUrl) { + this._tempClient = null; // clear memoization this._isUrl = isUrl; } @@ -52,8 +54,9 @@ export default class Login { * requests. * @returns {MatrixClient} */ - _createTemporaryClient() { - return Matrix.createClient({ + createTemporaryClient() { + if (this._tempClient) return this._tempClient; // use memoization + return this._tempClient = Matrix.createClient({ baseUrl: this._hsUrl, idBaseUrl: this._isUrl, }); @@ -61,7 +64,7 @@ export default class Login { getFlows() { const self = this; - const client = this._createTemporaryClient(); + const client = this.createTemporaryClient(); return client.loginFlows().then(function(result) { self._flows = result.flows; self._currentFlowIndex = 0; @@ -139,21 +142,6 @@ export default class Login { throw error; }); } - - getSsoLoginUrl(loginType) { - const client = this._createTemporaryClient(); - const parsedUrl = url.parse(window.location.href, true); - - // XXX: at this point, the fragment will always be #/login, which is no - // use to anyone. Ideally, we would get the intended fragment from - // MatrixChat.screenAfterLogin so that you could follow #/room links etc - // through an SSO login. - parsedUrl.hash = ""; - - parsedUrl.query["homeserver"] = client.getHomeserverUrl(); - parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); - return client.getSsoLoginUrl(url.format(parsedUrl), loginType); - } } diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 589accacc3..b918334d79 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -27,6 +27,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import AuthPage from "../../views/auth/AuthPage"; +import SSOButton from "../../views/elements/SSOButton"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -120,8 +121,8 @@ export default createReactClass({ 'm.login.password': this._renderPasswordStep, // CAS and SSO are the same thing, modulo the url we link to - 'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")), - 'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")), + 'm.login.cas': () => this._renderSsoStep("cas"), + 'm.login.sso': () => this._renderSsoStep("sso"), }; this._initLoginLogic(); @@ -579,7 +580,7 @@ export default createReactClass({ ); }, - _renderSsoStep: function(url) { + _renderSsoStep: function(loginType) { const SignInToText = sdk.getComponent('views.auth.SignInToText'); let onEditServerDetailsClick = null; @@ -600,7 +601,10 @@ export default createReactClass({ - { _t('Sign in with single sign-on') } + ); }, diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 8481b3fc43..d38fcf3883 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -23,8 +23,8 @@ import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; -import url from 'url'; import AuthPage from "../../views/auth/AuthPage"; +import SSOButton from "../../views/elements/SSOButton"; const LOGIN_VIEW = { LOADING: 1, @@ -55,7 +55,6 @@ export default class SoftLogout extends React.Component { this.state = { loginView: LOGIN_VIEW.LOADING, keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount) - ssoUrl: null, busy: false, password: "", @@ -105,18 +104,6 @@ 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) => { @@ -195,14 +182,6 @@ export default class SoftLogout extends React.Component { }); } - 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"); @@ -257,8 +236,6 @@ 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) @@ -266,9 +243,9 @@ export default class SoftLogout extends React.Component { return (

{introText}

- - {_t('Sign in with single sign-on')} - +
); } diff --git a/src/components/views/elements/SSOButton.js b/src/components/views/elements/SSOButton.js new file mode 100644 index 0000000000..3e0757924b --- /dev/null +++ b/src/components/views/elements/SSOButton.js @@ -0,0 +1,41 @@ +/* +Copyright 2020 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 React from 'react'; +import PropTypes from 'prop-types'; + +import PlatformPeg from "../../../PlatformPeg"; +import AccessibleButton from "./AccessibleButton"; +import {_t} from "../../../languageHandler"; + +const SSOButton = ({matrixClient, loginType, ...props}) => { + const onClick = () => { + PlatformPeg.get().startSingleSignOn(matrixClient, loginType); + }; + + return ( + + {_t("Sign in with single sign-on")} + + ); +}; + +SSOButton.propTypes = { + matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client + loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis +}; + +export default SSOButton; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ab18c58e90..4ec9c1e281 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1430,6 +1430,7 @@ "This alias is available to use": "This alias is available to use", "This alias is already in use": "This alias is already in use", "Room directory": "Room directory", + "Sign in with single sign-on": "Sign in with single sign-on", "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", @@ -2002,7 +2003,6 @@ "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", - "Sign in with single sign-on": "Sign in with single sign-on", "Create account": "Create account", "Failed to fetch avatar URL": "Failed to fetch avatar URL", "Set a display name:": "Set a display name:",