Merge pull request #4158 from matrix-org/t3chguy/sso

riot-desktop open SSO in browser so user doesn't have to auth twice
pull/21833/head
Michael Telatynski 2020-03-03 22:27:48 +00:00 committed by GitHub
commit f08d034f84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 86 additions and 52 deletions

View File

@ -4,6 +4,7 @@
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import {MatrixClient} from "matrix-js-sdk";
import dis from './dispatcher'; import dis from './dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager'; import BaseEventIndexManager from './indexing/BaseEventIndexManager';
@ -164,4 +166,26 @@ export default class BasePlatform {
} }
setLanguage(preferredLangs: string[]) {} 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
}
} }

View File

@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 Matrix from "matrix-js-sdk";
import url from 'url';
export default class Login { export default class Login {
constructor(hsUrl, isUrl, fallbackHsUrl, opts) { constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
this._hsUrl = hsUrl; this._hsUrl = hsUrl;
@ -29,6 +28,7 @@ export default class Login {
this._currentFlowIndex = 0; this._currentFlowIndex = 0;
this._flows = []; this._flows = [];
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
this._tempClient = null; // memoize
} }
getHomeserverUrl() { getHomeserverUrl() {
@ -40,10 +40,12 @@ export default class Login {
} }
setHomeserverUrl(hsUrl) { setHomeserverUrl(hsUrl) {
this._tempClient = null; // clear memoization
this._hsUrl = hsUrl; this._hsUrl = hsUrl;
} }
setIdentityServerUrl(isUrl) { setIdentityServerUrl(isUrl) {
this._tempClient = null; // clear memoization
this._isUrl = isUrl; this._isUrl = isUrl;
} }
@ -52,8 +54,9 @@ export default class Login {
* requests. * requests.
* @returns {MatrixClient} * @returns {MatrixClient}
*/ */
_createTemporaryClient() { createTemporaryClient() {
return Matrix.createClient({ if (this._tempClient) return this._tempClient; // use memoization
return this._tempClient = Matrix.createClient({
baseUrl: this._hsUrl, baseUrl: this._hsUrl,
idBaseUrl: this._isUrl, idBaseUrl: this._isUrl,
}); });
@ -61,7 +64,7 @@ export default class Login {
getFlows() { getFlows() {
const self = this; const self = this;
const client = this._createTemporaryClient(); const client = this.createTemporaryClient();
return client.loginFlows().then(function(result) { return client.loginFlows().then(function(result) {
self._flows = result.flows; self._flows = result.flows;
self._currentFlowIndex = 0; self._currentFlowIndex = 0;
@ -139,21 +142,6 @@ export default class Login {
throw error; 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);
}
} }

View File

@ -27,6 +27,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames"; import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -120,8 +121,8 @@ export default createReactClass({
'm.login.password': this._renderPasswordStep, 'm.login.password': this._renderPasswordStep,
// CAS and SSO are the same thing, modulo the url we link to // CAS and SSO are the same thing, modulo the url we link to
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")), 'm.login.cas': () => this._renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")), 'm.login.sso': () => this._renderSsoStep("sso"),
}; };
this._initLoginLogic(); this._initLoginLogic();
@ -579,7 +580,7 @@ export default createReactClass({
); );
}, },
_renderSsoStep: function(url) { _renderSsoStep: function(loginType) {
const SignInToText = sdk.getComponent('views.auth.SignInToText'); const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null; let onEditServerDetailsClick = null;
@ -600,7 +601,10 @@ export default createReactClass({
<SignInToText serverConfig={this.props.serverConfig} <SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick} /> onEditServerDetailsClick={onEditServerDetailsClick} />
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a> <SSOButton
className="mx_Login_sso_link mx_Login_submit"
matrixClient={this._loginLogic.createTemporaryClient()}
loginType={loginType} />
</div> </div>
); );
}, },

View File

@ -23,8 +23,8 @@ import * as Lifecycle from '../../../Lifecycle';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {sendLoginRequest} from "../../../Login"; import {sendLoginRequest} from "../../../Login";
import url from 'url';
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
const LOGIN_VIEW = { const LOGIN_VIEW = {
LOADING: 1, LOADING: 1,
@ -55,7 +55,6 @@ export default class SoftLogout extends React.Component {
this.state = { this.state = {
loginView: LOGIN_VIEW.LOADING, loginView: LOGIN_VIEW.LOADING,
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount) keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
ssoUrl: null,
busy: false, busy: false,
password: "", password: "",
@ -105,18 +104,6 @@ export default class SoftLogout extends React.Component {
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED; const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
this.setState({loginView: chosenView}); 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) => { 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() { _renderSignInSection() {
if (this.state.loginView === LOGIN_VIEW.LOADING) { if (this.state.loginView === LOGIN_VIEW.LOADING) {
const Spinner = sdk.getComponent("elements.Spinner"); 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) { if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
if (!introText) { if (!introText) {
introText = _t("Sign in and regain access to your account."); introText = _t("Sign in and regain access to your account.");
} // else we already have a message and should use it (key backup warning) } // 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 ( return (
<div> <div>
<p>{introText}</p> <p>{introText}</p>
<AccessibleButton kind='primary' onClick={this.onSsoLogin}> <SSOButton
{_t('Sign in with single sign-on')} matrixClient={MatrixClientPeg.get()}
</AccessibleButton> loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"} />
</div> </div>
); );
} }

View File

@ -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 (
<AccessibleButton {...props} kind="primary" onClick={onClick}>
{_t("Sign in with single sign-on")}
</AccessibleButton>
);
};
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;

View File

@ -1430,6 +1430,7 @@
"This alias is available to use": "This alias is available to use", "This alias is available to use": "This alias is available to use",
"This alias is already in use": "This alias is already in use", "This alias is already in use": "This alias is already in use",
"Room directory": "Room directory", "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...", "And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com", "ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User", "Add User": "Add User",
@ -2002,7 +2003,6 @@
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "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 <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> 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", "Create account": "Create account",
"Failed to fetch avatar URL": "Failed to fetch avatar URL", "Failed to fetch avatar URL": "Failed to fetch avatar URL",
"Set a display name:": "Set a display name:", "Set a display name:": "Set a display name:",