Iterate Multi-SSO support

This commit is contained in:
Michael Telatynski 2020-11-24 12:09:11 +00:00
parent b1ca1eb3f5
commit f7d7182dc9
7 changed files with 100 additions and 79 deletions

View file

@ -37,6 +37,10 @@ limitations under the License.
color: $authpage-primary-color; color: $authpage-primary-color;
} }
h4 {
text-align: center;
}
a:link, a:link,
a:hover, a:hover,
a:visited { a:visited {
@ -146,15 +150,14 @@ limitations under the License.
display: block; display: block;
text-align: center; text-align: center;
width: 100%; width: 100%;
margin-top: 24px;
> a { > a {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
} }
} }
form + .mx_AuthBody_changeFlow { .mx_SSOButtons + .mx_AuthBody_changeFlow {
margin-top: 0; margin-top: 24px;
} }
.mx_AuthBody_spinner { .mx_AuthBody_spinner {

View file

@ -29,8 +29,8 @@ interface ILoginOptions {
} }
// TODO: Move this to JS SDK // TODO: Move this to JS SDK
interface ILoginFlow { interface IPasswordFlow {
type: "m.login.password" | "m.login.cas"; type: "m.login.password";
} }
export interface IIdentityProvider { export interface IIdentityProvider {
@ -40,13 +40,13 @@ export interface IIdentityProvider {
} }
export interface ISSOFlow { export interface ISSOFlow {
type: "m.login.sso"; type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
identity_providers: IIdentityProvider[]; identity_providers: IIdentityProvider[];
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858 "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
} }
export type LoginFlow = ISSOFlow | ILoginFlow; export type LoginFlow = ISSOFlow | IPasswordFlow;
// TODO: Move this to JS SDK // TODO: Move this to JS SDK
/* eslint-disable camelcase */ /* eslint-disable camelcase */

View file

@ -2009,6 +2009,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
fragmentAfterLogin={fragmentAfterLogin}
{...this.getServerProperties()} {...this.getServerProperties()}
/> />
); );

View file

@ -319,6 +319,7 @@ export default class ForgotPassword extends React.Component {
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
autoComplete="new-password"
/> />
<Field <Field
name="reset_password_confirm" name="reset_password_confirm"
@ -328,6 +329,7 @@ export default class ForgotPassword extends React.Component {
onChange={this.onInputChanged.bind(this, "password2")} onChange={this.onInputChanged.bind(this, "password2")}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
autoComplete="new-password"
/> />
</div> </div>
<span>{_t( <span>{_t(

View file

@ -438,7 +438,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
if (supportedFlows.length > 0) { if (supportedFlows.length > 0) {
this.setState({ this.setState({
currentFlow: this.getCurrentFlowStep(), flows: supportedFlows,
}); });
return; return;
} }
@ -520,22 +520,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
return null; return null;
} }
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
return null;
}
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
return <ServerConfig return <ServerConfig
serverConfig={this.props.serverConfig} serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange} onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
{...serverDetailsProps} onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>; />;
} }
@ -591,7 +582,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow; const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
return ( return (
<div>
<SSOButtons <SSOButtons
matrixClient={this.loginLogic.createTemporaryClient()} matrixClient={this.loginLogic.createTemporaryClient()}
flow={flow} flow={flow}
@ -599,7 +589,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
fragmentAfterLogin={this.props.fragmentAfterLogin} fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find(flow => flow.type === "m.login.password")} primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
/> />
</div>
); );
}; };

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import React, {ComponentProps, ReactNode} from 'react'; import React, {ReactNode} from 'react';
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
@ -28,8 +28,9 @@ import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import Login from "../../../Login"; import Login, {ISSOFlow} from "../../../Login";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
// Phases // Phases
enum Phase { enum Phase {
@ -47,6 +48,7 @@ interface IProps {
clientSecret?: string; clientSecret?: string;
sessionId?: string; sessionId?: string;
idSid?: string; idSid?: string;
fragmentAfterLogin?: string;
// Called when the user has logged in. Params: // Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
@ -116,12 +118,14 @@ interface IState {
// if a different user ID to the one we just registered is logged in, // if a different user ID to the one we just registered is logged in,
// this is the user ID that's logged in. // this is the user ID that's logged in.
differentLoggedInUserId?: string; differentLoggedInUserId?: string;
// the SSO flow definition, this is fetched from /login as that's the only
// place it is exposed.
ssoFlow?: ISSOFlow;
} }
// Enable phases for registration
const PHASES_ENABLED = true;
export default class Registration extends React.Component<IProps, IState> { export default class Registration extends React.Component<IProps, IState> {
loginLogic: Login;
constructor(props) { constructor(props) {
super(props); super(props);
@ -141,6 +145,11 @@ export default class Registration extends React.Component<IProps, IState> {
serverErrorIsFatal: false, serverErrorIsFatal: false,
serverDeadError: "", serverDeadError: "",
}; };
const {hsUrl, isUrl} = this.props.serverConfig;
this.loginLogic = new Login(hsUrl, isUrl, null, {
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
});
} }
componentDidMount() { componentDidMount() {
@ -252,9 +261,21 @@ export default class Registration extends React.Component<IProps, IState> {
console.log("Unable to determine is server needs id_server param", e); console.log("Unable to determine is server needs id_server param", e);
} }
this.loginLogic.setHomeserverUrl(hsUrl);
this.loginLogic.setIdentityServerUrl(isUrl);
let ssoFlow: ISSOFlow;
try {
const loginFlows = await this.loginLogic.getFlows();
ssoFlow = loginFlows.find(f => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow;
} catch (e) {
console.error("Failed to get login flows to check for SSO support", e);
}
this.setState({ this.setState({
matrixClient: cli, matrixClient: cli,
serverRequiresIdServer, serverRequiresIdServer,
ssoFlow,
busy: false, busy: false,
}); });
const showGenericError = (e) => { const showGenericError = (e) => {
@ -282,13 +303,7 @@ export default class Registration extends React.Component<IProps, IState> {
// At this point registration is pretty much disabled, but before we do that let's // At this point registration is pretty much disabled, but before we do that let's
// quickly check to see if the server supports SSO instead. If it does, we'll send // quickly check to see if the server supports SSO instead. If it does, we'll send
// the user off to the login page to figure their account out. // the user off to the login page to figure their account out.
try { if (ssoFlow) {
const loginLogic = new Login(hsUrl, isUrl, null, {
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
});
const flows = await loginLogic.getFlows();
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
if (hasSsoFlow) {
// Redirect to login page - server probably expects SSO only // Redirect to login page - server probably expects SSO only
dis.dispatch({action: 'start_login'}); dis.dispatch({action: 'start_login'});
} else { } else {
@ -299,10 +314,6 @@ export default class Registration extends React.Component<IProps, IState> {
flows: [], flows: [],
}); });
} }
} catch (e) {
console.error("Failed to get login flows to check for SSO support", e);
showGenericError(e);
}
} else { } else {
console.log("Unable to query for supported registration methods.", e); console.log("Unable to query for supported registration methods.", e);
showGenericError(e); showGenericError(e);
@ -534,7 +545,7 @@ export default class Registration extends React.Component<IProps, IState> {
// which is always shown if we allow custom URLs at all. // which is always shown if we allow custom URLs at all.
// (if there's a fatal server error, we need to show the full server // (if there's a fatal server error, we need to show the full server
// config as the user may need to change servers to resolve the error). // config as the user may need to change servers to resolve the error).
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) { if (this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
return <div> return <div>
<ServerTypeSelector <ServerTypeSelector
selected={this.state.serverType} selected={this.state.serverType}
@ -543,13 +554,6 @@ export default class Registration extends React.Component<IProps, IState> {
</div>; </div>;
} }
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
let serverDetails = null; let serverDetails = null;
switch (this.state.serverType) { switch (this.state.serverType) {
case ServerType.FREE: case ServerType.FREE:
@ -559,7 +563,9 @@ export default class Registration extends React.Component<IProps, IState> {
serverConfig={this.props.serverConfig} serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange} onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
{...serverDetailsProps} onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>; />;
break; break;
case ServerType.ADVANCED: case ServerType.ADVANCED:
@ -568,7 +574,9 @@ export default class Registration extends React.Component<IProps, IState> {
onServerConfigChange={this.props.onServerConfigChange} onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
showIdentityServerIfRequiredByHomeserver={true} showIdentityServerIfRequiredByHomeserver={true}
{...serverDetailsProps} onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>; />;
break; break;
} }
@ -583,7 +591,7 @@ export default class Registration extends React.Component<IProps, IState> {
} }
private renderRegisterComponent() { private renderRegisterComponent() {
if (PHASES_ENABLED && this.state.phase !== Phase.Registration) { if (this.state.phase !== Phase.Registration) {
return null; return null;
} }
@ -610,7 +618,23 @@ export default class Registration extends React.Component<IProps, IState> {
<Spinner /> <Spinner />
</div>; </div>;
} else if (this.state.flows.length) { } else if (this.state.flows.length) {
return <RegistrationForm let ssoSection;
if (this.state.ssoFlow) {
ssoSection = <React.Fragment>
<h4>{_t("Continue with")}</h4>
<SSOButtons
matrixClient={this.loginLogic.createTemporaryClient()}
flow={this.state.ssoFlow}
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
<h4>{_t("Or")}</h4>
</React.Fragment>;
}
return <React.Fragment>
{ ssoSection }
<RegistrationForm
defaultUsername={this.state.formVals.username} defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email} defaultEmail={this.state.formVals.email}
defaultPhoneCountry={this.state.formVals.phoneCountry} defaultPhoneCountry={this.state.formVals.phoneCountry}
@ -621,7 +645,8 @@ export default class Registration extends React.Component<IProps, IState> {
serverConfig={this.props.serverConfig} serverConfig={this.props.serverConfig}
canSubmit={!this.state.serverErrorIsFatal} canSubmit={!this.state.serverErrorIsFatal}
serverRequiresIdServer={this.state.serverRequiresIdServer} serverRequiresIdServer={this.state.serverRequiresIdServer}
/>; />
</React.Fragment>;
} }
} }
@ -658,7 +683,7 @@ export default class Registration extends React.Component<IProps, IState> {
// Only show the 'go back' button if you're not looking at the form // Only show the 'go back' button if you're not looking at the form
let goBack; let goBack;
if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) { if (this.state.phase !== Phase.Registration || this.state.doingUIAuth) {
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#"> goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
{ _t('Go back') } { _t('Go back') }
</a>; </a>;
@ -725,8 +750,7 @@ export default class Registration extends React.Component<IProps, IState> {
// If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type, // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
// wire up the server details edit link. // wire up the server details edit link.
let editLink = null; let editLink = null;
if (PHASES_ENABLED && if (!SdkConfig.get()['disable_custom_urls'] &&
!SdkConfig.get()['disable_custom_urls'] &&
this.state.serverType !== ServerType.FREE && this.state.serverType !== ServerType.FREE &&
!this.state.doingUIAuth !this.state.doingUIAuth
) { ) {

View file

@ -2517,6 +2517,8 @@
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
"Continue with": "Continue with",
"Or": "Or",
"Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>", "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
"Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
"Continue with previous account": "Continue with previous account", "Continue with previous account": "Continue with previous account",