Merge pull request #4158 from matrix-org/t3chguy/sso
riot-desktop open SSO in browser so user doesn't have to auth twice
This commit is contained in:
commit
f08d034f84
6 changed files with 86 additions and 52 deletions
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
28
src/Login.js
28
src/Login.js
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
41
src/components/views/elements/SSOButton.js
Normal file
41
src/components/views/elements/SSOButton.js
Normal 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;
|
|
@ -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:",
|
||||||
|
|
Loading…
Reference in a new issue