diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
index 85007aeecb..05cddf2c48 100644
--- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss
+++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
@@ -60,3 +60,14 @@ limitations under the License.
.mx_InteractiveAuthEntryComponents_passwordSection {
width: 300px;
}
+
+.mx_InteractiveAuthEntryComponents_sso_buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ margin-top: 20px;
+
+ .mx_AccessibleButton {
+ margin-left: 5px;
+ }
+}
diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index b87071745d..de39525588 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -36,12 +36,20 @@ limitations under the License.
font-weight: 600;
}
+.mx_AccessibleButton_kind_primary_outline {
+ color: $button-primary-bg-color;
+ background-color: $button-secondary-bg-color;
+ border: 1px solid $button-primary-bg-color;
+ font-weight: 600;
+}
+
.mx_AccessibleButton_kind_secondary {
color: $accent-color;
font-weight: 600;
}
-.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled {
+.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled,
+.mx_AccessibleButton_kind_primary_outline.mx_AccessibleButton_disabled {
opacity: 0.4;
}
@@ -60,7 +68,14 @@ limitations under the License.
background-color: $button-danger-bg-color;
}
-.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
+.mx_AccessibleButton_kind_danger_outline {
+ color: $button-danger-bg-color;
+ background-color: $button-secondary-bg-color;
+ border: 1px solid $button-danger-bg-color;
+}
+
+.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
+.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}
diff --git a/src/AddThreepid.js b/src/AddThreepid.js
index 7a3250d0ca..f06f7c187d 100644
--- a/src/AddThreepid.js
+++ b/src/AddThreepid.js
@@ -21,6 +21,7 @@ import * as sdk from './index';
import Modal from './Modal';
import { _t } from './languageHandler';
import IdentityAuthClient from './IdentityAuthClient';
+import {SSOAuthEntry} from "./components/views/auth/InteractiveAuthEntryComponents";
function getIdServerDomain() {
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
@@ -188,11 +189,31 @@ export default class AddThreepid {
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
+
+ const dialogAesthetics = {
+ [SSOAuthEntry.PHASE_PREAUTH]: {
+ title: _t("Use Single Sign On to continue"),
+ body: _t("Confirm adding this email address by using " +
+ "Single Sign On to prove your identity."),
+ continueText: _t("Single Sign On"),
+ continueKind: "primary",
+ },
+ [SSOAuthEntry.PHASE_POSTAUTH]: {
+ title: _t("Confirm adding email"),
+ body: _t("Click the button below to confirm adding this email address."),
+ continueText: _t("Confirm"),
+ continueKind: "primary",
+ },
+ };
const { finished } = Modal.createTrackedDialog('Add Email', '', InteractiveAuthDialog, {
title: _t("Add Email Address"),
matrixClient: MatrixClientPeg.get(),
authData: e.data,
makeRequest: this._makeAddThreepidOnlyRequest,
+ aestheticsForStagePhases: {
+ [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
+ [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
+ },
});
return finished;
}
@@ -285,11 +306,30 @@ export default class AddThreepid {
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
+ const dialogAesthetics = {
+ [SSOAuthEntry.PHASE_PREAUTH]: {
+ title: _t("Use Single Sign On to continue"),
+ body: _t("Confirm adding this phone number by using " +
+ "Single Sign On to prove your identity."),
+ continueText: _t("Single Sign On"),
+ continueKind: "primary",
+ },
+ [SSOAuthEntry.PHASE_POSTAUTH]: {
+ title: _t("Confirm adding phone number"),
+ body: _t("Click the button below to confirm adding this phone number."),
+ continueText: _t("Confirm"),
+ continueKind: "primary",
+ },
+ };
const { finished } = Modal.createTrackedDialog('Add MSISDN', '', InteractiveAuthDialog, {
title: _t("Add Phone Number"),
matrixClient: MatrixClientPeg.get(),
authData: e.data,
makeRequest: this._makeAddThreepidOnlyRequest,
+ aestheticsForStagePhases: {
+ [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
+ [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
+ },
});
return finished;
}
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index f4adb5751f..2492bf79a0 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 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.
@@ -24,6 +24,8 @@ import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryCom
import * as sdk from '../../index';
+export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
+
export default createReactClass({
displayName: 'InteractiveAuth',
@@ -47,7 +49,7 @@ export default createReactClass({
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param {object} result The result of the authenticated call
- // if successful, otherwise the error object
+ // if successful, otherwise the error object.
// @param {object} extra Additional information about the UI Auth
// process:
// * emailSid {string} If email auth was performed, the sid of
@@ -75,6 +77,15 @@ export default createReactClass({
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged: PropTypes.bool,
+
+ // Called when the stage changes, or the stage's phase changes. First
+ // argument is the stage, second is the phase. Some stages do not have
+ // phases and will be counted as 0 (numeric).
+ onStagePhaseChange: PropTypes.func,
+
+ // continueText and continueKind are passed straight through to the AuthEntryComponent.
+ continueText: PropTypes.string,
+ continueKind: PropTypes.string,
},
getInitialState: function() {
@@ -204,6 +215,16 @@ export default createReactClass({
this._authLogic.submitAuthDict(authData);
},
+ _onPhaseChange: function(newPhase) {
+ if (this.props.onStagePhaseChange) {
+ this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
+ }
+ },
+
+ _onStageCancel: function() {
+ this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
+ },
+
_renderCurrentStage: function() {
const stage = this.state.authStage;
if (!stage) {
@@ -232,6 +253,10 @@ export default createReactClass({
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
showContinue={!this.props.continueIsManaged}
+ onPhaseChange={this._onPhaseChange}
+ continueText={this.props.continueText}
+ continueKind={this.props.continueKind}
+ onCancel={this._onStageCancel}
/>
);
},
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index e731b4cc01..4e2f444844 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 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.
@@ -25,6 +25,7 @@ import classnames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
+import AccessibleButton from "../elements/AccessibleButton";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore";
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
+ * onPhaseChange: A function which is called when the stage's phase changes. If
+ * the stage has no phases, call this with DEFAULT_PHASE. Takes
+ * one argument, the phase, and is always defined/required.
+ * continueText: For stages which have a continue button, the text to use.
+ * continueKind: For stages which have a continue button, the style of button to
+ * use. For example, 'danger' or 'primary'.
+ * onCancel A function with no arguments which is called by the stage if the
+ * user knowingly cancelled/dismissed the authentication attempt.
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
*/
+export const DEFAULT_PHASE = 0;
+
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
@@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@@ -175,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
_onCaptchaResponse: function(response) {
@@ -236,6 +257,11 @@ export const TermsAuthEntry = createReactClass({
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
componentWillMount: function() {
@@ -378,6 +404,11 @@ export const EmailIdentityAuthEntry = createReactClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@@ -420,6 +451,11 @@ export const MsisdnAuthEntry = createReactClass({
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@@ -564,6 +600,91 @@ export const MsisdnAuthEntry = createReactClass({
},
});
+export class SSOAuthEntry extends React.Component {
+ static propTypes = {
+ matrixClient: PropTypes.object.isRequired,
+ authSessionId: PropTypes.string.isRequired,
+ loginType: PropTypes.string.isRequired,
+ submitAuthDict: PropTypes.func.isRequired,
+ errorText: PropTypes.string,
+ onPhaseChange: PropTypes.func.isRequired,
+ continueText: PropTypes.string,
+ continueKind: PropTypes.string,
+ onCancel: PropTypes.func,
+ };
+
+ static LOGIN_TYPE = "m.login.sso";
+ static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
+
+ static PHASE_PREAUTH = 1; // button to start SSO
+ static PHASE_POSTAUTH = 2; // button to confirm SSO completed
+
+ _ssoUrl: string;
+
+ constructor(props) {
+ super(props);
+
+ // We actually send the user through fallback auth so we don't have to
+ // deal with a redirect back to us, losing application context.
+ this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
+ this.props.loginType,
+ this.props.authSessionId,
+ );
+
+ this.state = {
+ phase: SSOAuthEntry.PHASE_PREAUTH,
+ };
+ }
+
+ componentDidMount(): void {
+ this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
+ }
+
+ onStartAuthClick = () => {
+ // Note: We don't use PlatformPeg's startSsoAuth functions because we almost
+ // certainly will need to open the thing in a new tab to avoid losing application
+ // context.
+
+ window.open(this._ssoUrl, '_blank');
+ this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
+ this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
+ };
+
+ onConfirmClick = () => {
+ this.props.submitAuthDict({});
+ };
+
+ render() {
+ let continueButton = null;
+ const cancelButton = (
+