Merge pull request #5578 from matrix-org/t3chguy/sso_fixes
Improve SSO auth flow
This commit is contained in:
commit
62712159f6
17 changed files with 228 additions and 63 deletions
|
@ -177,7 +177,14 @@ export default class InteractiveAuthComponent extends React.Component {
|
|||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
}, () => {
|
||||
if (oldStage != stageType) this._setFocus();
|
||||
if (oldStage !== stageType) {
|
||||
this._setFocus();
|
||||
} else if (
|
||||
!stageState.error && this._stageComponent.current &&
|
||||
this._stageComponent.current.attemptFailed
|
||||
) {
|
||||
this._stageComponent.current.attemptFailed();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -219,6 +219,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private screenAfterLogin?: IScreen;
|
||||
private windowWidth: number;
|
||||
private pageChanging: boolean;
|
||||
private tokenLogin?: boolean;
|
||||
private accountPassword?: string;
|
||||
private accountPasswordTimer?: NodeJS.Timeout;
|
||||
private focusComposer: boolean;
|
||||
|
@ -324,13 +325,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Lifecycle.attemptTokenLogin(
|
||||
this.props.realQueryParams,
|
||||
this.props.defaultDeviceDisplayName,
|
||||
).then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
).then(async (loggedIn) => {
|
||||
if (this.props.realQueryParams?.loginToken) {
|
||||
// remove the loginToken from the URL regardless
|
||||
this.props.onTokenLoginCompleted();
|
||||
}
|
||||
|
||||
// don't do anything else until the page reloads - just stay in
|
||||
// the 'loading' state.
|
||||
return;
|
||||
if (loggedIn) {
|
||||
this.tokenLogin = true;
|
||||
|
||||
// Create and start the client
|
||||
await Lifecycle.restoreFromLocalStorage({
|
||||
ignoreGuest: true,
|
||||
});
|
||||
return this.postLoginSetup();
|
||||
}
|
||||
|
||||
// if the user has followed a login or register link, don't reanimate
|
||||
|
@ -354,6 +362,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
private async postLoginSetup() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillUpdate(props, state) {
|
||||
|
@ -1839,40 +1883,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
await this.postLoginSetup();
|
||||
};
|
||||
|
||||
// complete security / e2e setup has finished
|
||||
|
@ -1916,6 +1927,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
<E2eSetup
|
||||
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||
accountPassword={this.accountPassword}
|
||||
tokenLogin={!!this.tokenLogin}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === Views.LOGGED_IN) {
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class E2eSetup extends React.Component {
|
|||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -33,6 +34,7 @@ export default class E2eSetup extends React.Component {
|
|||
<CreateCrossSigningDialog
|
||||
onFinished={this.props.onFinished}
|
||||
accountPassword={this.props.accountPassword}
|
||||
tokenLogin={this.props.tokenLogin}
|
||||
/>
|
||||
</CompleteSecurityBody>
|
||||
</AuthPage>
|
||||
|
|
|
@ -609,8 +609,12 @@ export class SSOAuthEntry extends React.Component {
|
|||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
attemptFailed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -618,12 +622,35 @@ export class SSOAuthEntry extends React.Component {
|
|||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
attemptFailed = () => {
|
||||
this.setState({
|
||||
attemptFailed: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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._popupWindow = window.open(this._ssoUrl, "_blank");
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
@ -656,10 +683,28 @@ export class SSOAuthEntry extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>;
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.attemptFailed) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ _t("Something went wrong in confirming your identity. Cancel and try again.") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{ errorSection }
|
||||
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -710,8 +755,7 @@ export class FallbackAuthEntry extends React.Component {
|
|||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
this._popupWindow = window.open(url, "_blank");
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
|
|
|
@ -34,6 +34,7 @@ import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
|||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -96,6 +97,9 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else if (this.props.tokenLogin) {
|
||||
// We are hoping the grace period is active
|
||||
await makeRequest({});
|
||||
} else {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
|
@ -144,6 +148,12 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.props.tokenLogin) {
|
||||
// ignore any failures, we are relying on grace period here
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
|
|
|
@ -22,13 +22,33 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
|||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
idp: IIdentityProvider;
|
||||
mini?: boolean;
|
||||
}
|
||||
|
||||
const getIcon = (brand: IdentityProviderBrand | string) => {
|
||||
switch (brand) {
|
||||
case IdentityProviderBrand.Apple:
|
||||
return require(`../../../../res/img/element-icons/brands/apple.svg`);
|
||||
case IdentityProviderBrand.Facebook:
|
||||
return require(`../../../../res/img/element-icons/brands/facebook.svg`);
|
||||
case IdentityProviderBrand.Github:
|
||||
return require(`../../../../res/img/element-icons/brands/github.svg`);
|
||||
case IdentityProviderBrand.Gitlab:
|
||||
return require(`../../../../res/img/element-icons/brands/gitlab.svg`);
|
||||
case IdentityProviderBrand.Google:
|
||||
return require(`../../../../res/img/element-icons/brands/google.svg`);
|
||||
case IdentityProviderBrand.Twitter:
|
||||
return require(`../../../../res/img/element-icons/brands/twitter.svg`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
matrixClient,
|
||||
loginType,
|
||||
|
@ -38,7 +58,6 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
mini,
|
||||
...props
|
||||
}) => {
|
||||
const kind = primary ? "primary" : "primary_outline";
|
||||
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||
|
||||
const onClick = () => {
|
||||
|
@ -46,30 +65,35 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
};
|
||||
|
||||
let icon;
|
||||
if (typeof idp?.icon === "string" && (idp.icon.startsWith("mxc://") || idp.icon.startsWith("https://"))) {
|
||||
icon = <img
|
||||
src={matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true)}
|
||||
height="24"
|
||||
width="24"
|
||||
alt={label}
|
||||
/>;
|
||||
let brandClass;
|
||||
const brandIcon = idp ? getIcon(idp.brand) : null;
|
||||
if (brandIcon) {
|
||||
const brandName = idp.brand.split(".").pop();
|
||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
||||
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
|
||||
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_SSOButton", {
|
||||
[brandClass]: brandClass,
|
||||
mx_SSOButton_mini: mini,
|
||||
mx_SSOButton_default: !idp,
|
||||
mx_SSOButton_primary: primary,
|
||||
});
|
||||
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
{ label }
|
||||
</AccessibleButton>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue