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;
}
h4 {
text-align: center;
}
a:link,
a:hover,
a:visited {
@ -146,15 +150,14 @@ limitations under the License.
display: block;
text-align: center;
width: 100%;
margin-top: 24px;
> a {
font-weight: $font-semi-bold;
}
}
form + .mx_AuthBody_changeFlow {
margin-top: 0;
.mx_SSOButtons + .mx_AuthBody_changeFlow {
margin-top: 24px;
}
.mx_AuthBody_spinner {

View file

@ -29,8 +29,8 @@ interface ILoginOptions {
}
// TODO: Move this to JS SDK
interface ILoginFlow {
type: "m.login.password" | "m.login.cas";
interface IPasswordFlow {
type: "m.login.password";
}
export interface IIdentityProvider {
@ -40,13 +40,13 @@ export interface IIdentityProvider {
}
export interface ISSOFlow {
type: "m.login.sso";
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers: IIdentityProvider[];
"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
/* eslint-disable camelcase */

View file

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

View file

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

View file

@ -438,7 +438,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
if (supportedFlows.length > 0) {
this.setState({
currentFlow: this.getCurrentFlowStep(),
flows: supportedFlows,
});
return;
}
@ -520,22 +520,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
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
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
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;
return (
<div>
<SSOButtons
matrixClient={this.loginLogic.createTemporaryClient()}
flow={flow}
@ -599,7 +589,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
fragmentAfterLogin={this.props.fragmentAfterLogin}
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 React, {ComponentProps, ReactNode} from 'react';
import React, {ReactNode} from 'react';
import {MatrixClient} from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
@ -28,8 +28,9 @@ import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage";
import Login from "../../../Login";
import Login, {ISSOFlow} from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
// Phases
enum Phase {
@ -47,6 +48,7 @@ interface IProps {
clientSecret?: string;
sessionId?: string;
idSid?: string;
fragmentAfterLogin?: string;
// Called when the user has logged in. Params:
// - 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,
// this is the user ID that's logged in.
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> {
loginLogic: Login;
constructor(props) {
super(props);
@ -141,6 +145,11 @@ export default class Registration extends React.Component<IProps, IState> {
serverErrorIsFatal: false,
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() {
@ -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);
}
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({
matrixClient: cli,
serverRequiresIdServer,
ssoFlow,
busy: false,
});
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
// 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.
try {
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) {
if (ssoFlow) {
// Redirect to login page - server probably expects SSO only
dis.dispatch({action: 'start_login'});
} else {
@ -299,10 +314,6 @@ export default class Registration extends React.Component<IProps, IState> {
flows: [],
});
}
} catch (e) {
console.error("Failed to get login flows to check for SSO support", e);
showGenericError(e);
}
} else {
console.log("Unable to query for supported registration methods.", 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.
// (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).
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
if (this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
return <div>
<ServerTypeSelector
selected={this.state.serverType}
@ -543,13 +554,6 @@ export default class Registration extends React.Component<IProps, IState> {
</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;
switch (this.state.serverType) {
case ServerType.FREE:
@ -559,7 +563,9 @@ export default class Registration extends React.Component<IProps, IState> {
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>;
break;
case ServerType.ADVANCED:
@ -568,7 +574,9 @@ export default class Registration extends React.Component<IProps, IState> {
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
showIdentityServerIfRequiredByHomeserver={true}
{...serverDetailsProps}
onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>;
break;
}
@ -583,7 +591,7 @@ export default class Registration extends React.Component<IProps, IState> {
}
private renderRegisterComponent() {
if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
if (this.state.phase !== Phase.Registration) {
return null;
}
@ -610,7 +618,23 @@ export default class Registration extends React.Component<IProps, IState> {
<Spinner />
</div>;
} 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}
defaultEmail={this.state.formVals.email}
defaultPhoneCountry={this.state.formVals.phoneCountry}
@ -621,7 +645,8 @@ export default class Registration extends React.Component<IProps, IState> {
serverConfig={this.props.serverConfig}
canSubmit={!this.state.serverErrorIsFatal}
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
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="#">
{ _t('Go back') }
</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,
// wire up the server details edit link.
let editLink = null;
if (PHASES_ENABLED &&
!SdkConfig.get()['disable_custom_urls'] &&
if (!SdkConfig.get()['disable_custom_urls'] &&
this.state.serverType !== ServerType.FREE &&
!this.state.doingUIAuth
) {

View file

@ -2517,6 +2517,8 @@
"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.",
"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>",
"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",