Implementation of MSC3824 to make the client OIDC-aware (#8681)
This commit is contained in:
parent
32bd350b7e
commit
d698193196
11 changed files with 240 additions and 29 deletions
|
@ -18,7 +18,7 @@ import React, { ReactNode } from "react";
|
|||
import { ConnectionError, MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
|
||||
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import Login from "../../../Login";
|
||||
|
@ -345,6 +345,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
this.loginLogic.createTemporaryClient(),
|
||||
ssoKind,
|
||||
this.props.fragmentAfterLogin,
|
||||
SSOAction.REGISTER,
|
||||
);
|
||||
} else {
|
||||
// Don't intercept - just go through to the register page
|
||||
|
@ -549,6 +550,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
|
||||
action={SSOAction.LOGIN}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ import React, { Fragment, ReactNode } from "react";
|
|||
import { IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ISSOFlow } from "matrix-js-sdk/src/@types/auth";
|
||||
import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { messageForResourceLimitError } from "../../../utils/ErrorUtils";
|
||||
|
@ -539,6 +539,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
flow={this.state.ssoFlow}
|
||||
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
action={SSOAction.REGISTER}
|
||||
/>
|
||||
<h2 className="mx_AuthBody_centered">
|
||||
{_t("%(ssoButtons)s Or %(usernamePassword)s", {
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
|
||||
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
@ -256,6 +256,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
|
||||
action={SSOAction.LOGIN}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -19,7 +19,13 @@ import { chunk } from "lodash";
|
|||
import classNames from "classnames";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
|
||||
import { IdentityProviderBrand, IIdentityProvider, ISSOFlow } from "matrix-js-sdk/src/@types/auth";
|
||||
import {
|
||||
IdentityProviderBrand,
|
||||
IIdentityProvider,
|
||||
ISSOFlow,
|
||||
DELEGATED_OIDC_COMPATIBILITY,
|
||||
SSOAction,
|
||||
} from "matrix-js-sdk/src/@types/auth";
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
@ -28,9 +34,10 @@ import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
|||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
interface ISSOButtonProps extends IProps {
|
||||
idp?: IIdentityProvider;
|
||||
mini?: boolean;
|
||||
action?: SSOAction;
|
||||
}
|
||||
|
||||
const getIcon = (brand: IdentityProviderBrand | string): string | null => {
|
||||
|
@ -79,20 +86,29 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
idp,
|
||||
primary,
|
||||
mini,
|
||||
action,
|
||||
flow,
|
||||
...props
|
||||
}) => {
|
||||
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||
let label: string;
|
||||
if (idp) {
|
||||
label = _t("Continue with %(provider)s", { provider: idp.name });
|
||||
} else if (DELEGATED_OIDC_COMPATIBILITY.findIn<boolean>(flow)) {
|
||||
label = _t("Continue");
|
||||
} else {
|
||||
label = _t("Sign in with single sign-on");
|
||||
}
|
||||
|
||||
const onClick = (): void => {
|
||||
const authenticationType = getAuthenticationType(idp?.brand ?? "");
|
||||
PosthogAnalytics.instance.setAuthenticationType(authenticationType);
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id);
|
||||
PlatformPeg.get()?.startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id, action);
|
||||
};
|
||||
|
||||
let icon;
|
||||
let brandClass;
|
||||
const brandIcon = idp ? getIcon(idp.brand) : null;
|
||||
if (brandIcon) {
|
||||
let icon: JSX.Element | undefined;
|
||||
let brandClass: string | undefined;
|
||||
const brandIcon = idp?.brand ? getIcon(idp.brand) : null;
|
||||
if (idp?.brand && brandIcon) {
|
||||
const brandName = idp.brand.split(".").pop();
|
||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
|
@ -101,12 +117,16 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
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,
|
||||
});
|
||||
const brandPart = brandClass ? { [brandClass]: brandClass } : undefined;
|
||||
const classes = classNames(
|
||||
"mx_SSOButton",
|
||||
{
|
||||
mx_SSOButton_mini: mini,
|
||||
mx_SSOButton_default: !idp,
|
||||
mx_SSOButton_primary: primary,
|
||||
},
|
||||
brandPart,
|
||||
);
|
||||
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
|
@ -128,14 +148,15 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
flow: ISSOFlow;
|
||||
loginType?: "sso" | "cas";
|
||||
loginType: "sso" | "cas";
|
||||
fragmentAfterLogin?: string;
|
||||
primary?: boolean;
|
||||
action?: SSOAction;
|
||||
}
|
||||
|
||||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary }) => {
|
||||
const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary, action }) => {
|
||||
const providers = flow.identity_providers || [];
|
||||
if (providers.length < 2) {
|
||||
return (
|
||||
|
@ -146,6 +167,8 @@ const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentA
|
|||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={providers[0]}
|
||||
primary={primary}
|
||||
action={action}
|
||||
flow={flow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -167,6 +190,8 @@ const SSOButtons: React.FC<IProps> = ({ matrixClient, flow, loginType, fragmentA
|
|||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
action={action}
|
||||
flow={flow}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ import React from "react";
|
|||
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import ProfileSettings from "../../ProfileSettings";
|
||||
|
@ -79,6 +80,7 @@ interface IState {
|
|||
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
|
||||
canChangePassword: boolean;
|
||||
idServerName: string;
|
||||
externalAccountManagementUrl?: string;
|
||||
}
|
||||
|
||||
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
||||
|
@ -106,6 +108,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
||||
canChangePassword: false,
|
||||
idServerName: null,
|
||||
externalAccountManagementUrl: undefined,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -161,7 +164,10 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
// the enabled flag value.
|
||||
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
||||
|
||||
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
|
||||
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(cli.getClientWellKnown());
|
||||
const externalAccountManagementUrl = delegatedAuthConfig?.account;
|
||||
|
||||
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword, externalAccountManagementUrl });
|
||||
}
|
||||
|
||||
private async getThreepidState(): Promise<void> {
|
||||
|
@ -348,9 +354,37 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
passwordChangeForm = null;
|
||||
}
|
||||
|
||||
let externalAccountManagement: JSX.Element | undefined;
|
||||
if (this.state.externalAccountManagementUrl) {
|
||||
const { hostname } = new URL(this.state.externalAccountManagementUrl);
|
||||
|
||||
externalAccountManagement = (
|
||||
<>
|
||||
<p className="mx_SettingsTab_subsectionText" data-testid="external-account-management-outer">
|
||||
{_t(
|
||||
"Your account details are managed separately at <code>%(hostname)s</code>.",
|
||||
{ hostname },
|
||||
{ code: (sub) => <code>{sub}</code> },
|
||||
)}
|
||||
</p>
|
||||
<AccessibleButton
|
||||
onClick={null}
|
||||
element="a"
|
||||
kind="primary"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={this.state.externalAccountManagementUrl}
|
||||
data-testid="external-account-management-link"
|
||||
>
|
||||
{_t("Manage account")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
|
||||
{externalAccountManagement}
|
||||
<p className="mx_SettingsTab_subsectionText">{passwordChangeText}</p>
|
||||
{passwordChangeForm}
|
||||
{threepidSection}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue