Skip to content

Commit fb06072

Browse files
authored
Support for stable m.oauth UIA stage from MSC4312 (#31704)
* Support for stable m.oauth UIA stage from MSC4312 * Unit tests * readonly props
1 parent f079224 commit fb06072

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

src/components/structures/InteractiveAuth.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export default class InteractiveAuthComponent<T> extends React.Component<Interac
117117
AuthType.UnstableRegistrationToken,
118118
AuthType.Sso,
119119
AuthType.SsoUnstable,
120+
AuthType.OAuth,
120121
CustomAuthType.MasCrossSigningReset,
121122
],
122123
});

src/components/views/auth/InteractiveAuthEntryComponents.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,7 @@ export class FallbackAuthEntry<T extends object> extends React.Component<IAuthEn
951951
}
952952

953953
export enum CustomAuthType {
954-
// Workaround for MAS requiring non-UIA authentication for resetting cross-signing.
954+
// This is the unstable value from MSC4312
955955
MasCrossSigningReset = "org.matrix.cross_signing_reset",
956956
}
957957

@@ -960,7 +960,8 @@ export class MasUnlockCrossSigningAuthEntry extends FallbackAuthEntry<{
960960
url?: string;
961961
};
962962
}> {
963-
public static LOGIN_TYPE = CustomAuthType.MasCrossSigningReset;
963+
public static readonly LOGIN_TYPE = AuthType.OAuth;
964+
public static readonly UNSTABLE_LOGIN_TYPE = CustomAuthType.MasCrossSigningReset;
964965

965966
private onGoToAccountClick = (): void => {
966967
if (!this.props.stageParams?.url) return;
@@ -1017,6 +1018,7 @@ export interface IStageComponent extends React.ComponentClass<React.PropsWithRef
10171018

10181019
export default function getEntryComponentForLoginType(loginType: AuthType | CustomAuthType): IStageComponent {
10191020
switch (loginType) {
1021+
case AuthType.OAuth:
10201022
case CustomAuthType.MasCrossSigningReset:
10211023
return MasUnlockCrossSigningAuthEntry;
10221024
case AuthType.Password:
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { fireEvent, render, type RenderResult } from "jest-matrix-react";
10+
11+
import InteractiveAuthComponent from "../../../../../src/components/structures/InteractiveAuth";
12+
import { getMockClientWithEventEmitter, unmockClientPeg } from "../../../../test-utils";
13+
14+
describe("InteractiveAuthComponent", function () {
15+
const mockClient = getMockClientWithEventEmitter({
16+
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
17+
getDomain: jest.fn().mockReturnValue("test.local"),
18+
});
19+
const authUrl = "https://test.local/oauth?action=foo";
20+
const onAuthFinished = jest.fn();
21+
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
22+
23+
const defaultProps = {
24+
matrixClient: mockClient,
25+
makeRequest: jest.fn().mockResolvedValue(undefined),
26+
onAuthFinished: jest.fn(),
27+
};
28+
const getComponent = (props = {}) => render(<InteractiveAuthComponent {...defaultProps} {...props} />);
29+
30+
beforeEach(function () {
31+
jest.clearAllMocks();
32+
jest.spyOn(global.window, "open").mockImplementation();
33+
});
34+
35+
afterAll(() => {
36+
unmockClientPeg();
37+
});
38+
39+
const getSubmitButton = ({ container }: RenderResult) => container.querySelector(".mx_Dialog_nonDialogButton");
40+
41+
it("should use an m.oauth stage", async () => {
42+
const authData = {
43+
session: "sess",
44+
flows: [{ stages: ["m.oauth"] }],
45+
params: {
46+
"m.oauth": {
47+
url: authUrl,
48+
},
49+
},
50+
};
51+
52+
const wrapper = getComponent({ makeRequest, onAuthFinished, authData });
53+
54+
const submitNode = getSubmitButton(wrapper);
55+
expect(submitNode).toBeTruthy();
56+
57+
// click button; should trigger the auth URL to be opened
58+
fireEvent.click(submitNode!);
59+
60+
expect(global.window.open).toHaveBeenCalledWith(authUrl, "_blank");
61+
});
62+
63+
it("should use an unstable org.matrix.cross_signing_reset stage", async () => {
64+
const authData = {
65+
session: "sess",
66+
flows: [{ stages: ["org.matrix.cross_signing_reset"] }],
67+
params: {
68+
"org.matrix.cross_signing_reset": {
69+
url: authUrl,
70+
},
71+
},
72+
};
73+
74+
const wrapper = getComponent({ makeRequest, onAuthFinished, authData });
75+
76+
const submitNode = getSubmitButton(wrapper);
77+
expect(submitNode).toBeTruthy();
78+
79+
// click button; should trigger the auth URL to be opened
80+
fireEvent.click(submitNode!);
81+
82+
expect(global.window.open).toHaveBeenCalledWith(authUrl, "_blank");
83+
});
84+
85+
it("should use the first flow when both stable and unstable are present", async () => {
86+
const authData = {
87+
session: "sess",
88+
flows: [{ stages: ["org.matrix.cross_signing_reset"] }, { stages: ["m.oauth"] }],
89+
params: {
90+
"org.matrix.cross_signing_reset": {
91+
url: authUrl,
92+
},
93+
"m.oauth": {
94+
url: "https://should.not.be/opened",
95+
},
96+
},
97+
};
98+
99+
const wrapper = getComponent({ makeRequest, onAuthFinished, authData });
100+
101+
const submitNode = getSubmitButton(wrapper);
102+
expect(submitNode).toBeTruthy();
103+
104+
// click button; should trigger the auth URL to be opened
105+
fireEvent.click(submitNode!);
106+
107+
expect(global.window.open).toHaveBeenCalledWith(authUrl, "_blank");
108+
});
109+
});

0 commit comments

Comments
 (0)