Skip to content

Commit 64d3802

Browse files
authored
Fall back to OIDC response_mode query if fragment unsupported (#33169)
* Fall back to OIDC response_mode query if fragment unsupported * Tidy comments * Fix test
1 parent 583eae6 commit 64d3802

File tree

10 files changed

+75
-30
lines changed

10 files changed

+75
-30
lines changed

apps/web/src/Lifecycle.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,10 @@ export async function attemptDelegatedAuthLogin(
277277
defaultDeviceDisplayName?: string,
278278
fragmentAfterLogin?: string,
279279
): Promise<boolean> {
280-
if (urlParams.oidc) {
281-
console.log("We have OIDC params - attempting OIDC login");
282-
return attemptOidcNativeLogin(urlParams["oidc"]);
280+
if (urlParams.oidc_fragment) {
281+
return attemptOidcNativeLogin(urlParams.oidc_fragment, "fragment");
282+
} else if (urlParams.oidc_query) {
283+
return attemptOidcNativeLogin(urlParams.oidc_query, "query");
283284
}
284285

285286
return attemptTokenLogin(urlParams["legacy_sso"], defaultDeviceDisplayName, fragmentAfterLogin);
@@ -288,12 +289,18 @@ export async function attemptDelegatedAuthLogin(
288289
/**
289290
* Attempt to login by completing OIDC authorization code flow
290291
* @param urlParams subset of app-load url parameters relating to oidc auth
292+
* @param responseMode - the response_mode used in the auth request
291293
* @returns Promise that resolves to true when login succeeded, else false
292294
*/
293-
async function attemptOidcNativeLogin(urlParams: NonNullable<URLParams["oidc"]>): Promise<boolean> {
295+
async function attemptOidcNativeLogin(
296+
urlParams: NonNullable<URLParams["oidc_fragment"]>,
297+
responseMode: "fragment" | "query",
298+
): Promise<boolean> {
299+
console.log("We have OIDC params - attempting OIDC login");
300+
294301
try {
295302
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } =
296-
await completeOidcLogin(urlParams);
303+
await completeOidcLogin(urlParams, responseMode);
297304

298305
const {
299306
user_id: userId,
@@ -1036,7 +1043,7 @@ export function isLoggingOut(): boolean {
10361043
* By the time this method is called, we have successfully logged in if necessary, and the client has been set up with
10371044
* the access token.
10381045
*
1039-
* Emits {@link Acction.WillStartClient} before starting the client, and {@link Action.ClientStarted} when the client has
1046+
* Emits {@link Action.WillStartClient} before starting the client, and {@link Action.ClientStarted} when the client has
10401047
* been started.
10411048
*
10421049
* @param client the matrix client to start

apps/web/src/components/structures/MatrixChat.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
350350
);
351351

352352
// remove the loginToken or auth code from the URL regardless
353-
if (!!this.props.urlParams.legacy_sso || !!this.props.urlParams.oidc) {
353+
if (
354+
!!this.props.urlParams.legacy_sso ||
355+
!!this.props.urlParams.oidc_fragment ||
356+
!!this.props.urlParams.oidc_query
357+
) {
354358
this.props.onTokenLoginCompleted(this.props.urlParams, this.getFragmentAfterLogin());
355359
}
356360

@@ -408,7 +412,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
408412
* {@link onWillStartClient} and {@link onClientStarted} will already have been called (but not necessarily
409413
* completed).
410414
*
411-
* This method either calls {@link onLiggedIn} directly, or switches to {@link Views.E2E_SETUP} or
415+
* This method either calls {@link onLoggedIn} directly, or switches to {@link Views.E2E_SETUP} or
412416
* {@link Views.COMPLETE_SECURITY}, which will later call {@link onCompleteSecurityE2eSetupFinished}.
413417
*/
414418
private async postLoginSetup(): Promise<void> {

apps/web/src/utils/oidc/authorize.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { type URLParams } from "../../vector/url_utils.ts";
2323
* @param clientId this client's id as registered with configured issuer
2424
* @param homeserverUrl target homeserver
2525
* @param identityServerUrl OPTIONAL target identity server
26+
* @param isRegistration if true will set the prompt to "create"
2627
* @returns Promise that resolves after we have navigated to auth endpoint
2728
*/
2829
export const startOidcLogin = async (
@@ -47,7 +48,7 @@ export const startOidcLogin = async (
4748
nonce,
4849
prompt,
4950
urlState: PlatformPeg.get()?.getOidcClientState(),
50-
responseMode: "fragment",
51+
responseMode: delegatedAuthConfig.response_modes_supported?.includes("fragment") ? "fragment" : "query",
5152
});
5253

5354
window.location.href = authorizationUrl;
@@ -57,15 +58,20 @@ export const startOidcLogin = async (
5758
* Gets `code` and `state` response params
5859
*
5960
* @param urlParams - the parameters to read
61+
* @param responseMode - the response_mode used in the auth request
6062
* @returns code and state
6163
* @throws when code and state are not valid strings
6264
*/
63-
const getCodeAndStateFromParams = ({
64-
code,
65-
state,
66-
}: NonNullable<URLParams["oidc"]>): { code: string; state: string } => {
65+
const getCodeAndStateFromParams = (
66+
{ code, state }: NonNullable<URLParams["oidc_fragment"]>,
67+
responseMode: "fragment" | "query",
68+
): { code: string; state: string } => {
6769
if (!code || typeof code !== "string" || !state || typeof state !== "string") {
68-
throw new Error(OidcClientError.InvalidQueryParameters);
70+
if (responseMode === "fragment") {
71+
throw new Error(OidcClientError.InvalidFragmentParameters);
72+
} else {
73+
throw new Error(OidcClientError.InvalidQueryParameters);
74+
}
6975
}
7076
return { code, state };
7177
};
@@ -91,15 +97,17 @@ type CompleteOidcLoginResponse = {
9197
/**
9298
* Attempt to complete authorization code flow to get an access token
9399
* @param urlParams the parameters extracted from the app-load URI.
100+
* @param responseMode - the response_mode used in the auth request
94101
* @returns Promise that resolves with a CompleteOidcLoginResponse when login was successful
95102
* @throws When we failed to get a valid access token
96103
*/
97104
export const completeOidcLogin = async (
98-
urlParams: NonNullable<URLParams["oidc"]>,
105+
urlParams: NonNullable<URLParams["oidc_fragment"]>,
106+
responseMode: "fragment" | "query",
99107
): Promise<CompleteOidcLoginResponse> => {
100-
const { code, state } = getCodeAndStateFromParams(urlParams);
108+
const { code, state } = getCodeAndStateFromParams(urlParams, responseMode);
101109
const { homeserverUrl, tokenResponse, idTokenClaims, identityServerUrl, oidcClientSettings } =
102-
await completeAuthorizationCodeGrant(code, state, "fragment");
110+
await completeAuthorizationCodeGrant(code, state, responseMode);
103111

104112
return {
105113
homeserverUrl,

apps/web/src/utils/oidc/error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { _t } from "../../languageHandler";
1717
*/
1818
export enum OidcClientError {
1919
InvalidQueryParameters = "Invalid query parameters for OIDC native login. `code` and `state` are required.",
20+
InvalidFragmentParameters = "Invalid fragment parameters for OIDC native login. `code` and `state` are required.",
2021
}
2122

2223
/**
@@ -30,6 +31,7 @@ export const getOidcErrorMessage = (error: Error): string | ReactNode => {
3031
case OidcError.MissingOrInvalidStoredState:
3132
return _t("auth|oidc|missing_or_invalid_stored_state");
3233
case OidcClientError.InvalidQueryParameters:
34+
case OidcClientError.InvalidFragmentParameters:
3335
case OidcError.CodeExchangeFailed:
3436
case OidcError.InvalidBearerTokenResponse:
3537
case OidcError.InvalidIdToken:

apps/web/src/vector/app.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function onTokenLoginCompleted(urlParams: URLParams, fragmentAfterLogin: string)
4444
const url = new URL(window.location.href);
4545

4646
// if we did a token login, we're now left with the login token as query param in the url; clear it out
47-
for (const param in { ...urlParams.legacy_sso }) {
47+
for (const param in { ...urlParams.legacy_sso, ...urlParams.oidc_query }) {
4848
url.searchParams.delete(param);
4949
}
5050

@@ -112,7 +112,7 @@ export async function loadApp(urlParams: URLParams, matrixChatRef: React.Ref<Mat
112112
// Before we continue, let's see if we're supposed to do an SSO redirect
113113
const [userId] = await Lifecycle.getStoredSessionOwner();
114114
const hasPossibleToken = !!userId;
115-
const isReturningFromSso = !!urlParams.legacy_sso || !!urlParams.oidc;
115+
const isReturningFromSso = !!urlParams.legacy_sso || !!urlParams.oidc_fragment || !!urlParams.oidc_query;
116116
const ssoRedirects = config.sso_redirect_options || {};
117117
let autoRedirect = ssoRedirects.immediate === true;
118118
// XXX: This path matching is a bit brittle, but better to do it early instead of in the app code.

apps/web/src/vector/url_utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@ const urlParameterConfig = {
5555
location: "query",
5656
},
5757
// Fragment params for OIDC login, added by the Identity Provider
58-
oidc: {
58+
oidc_fragment: {
5959
keys: ["code", "state"],
6060
location: "fragment",
6161
},
62+
// Query params for OIDC login, added by the Identity Provider, used as fallback when fragment is unsupported
63+
oidc_query: {
64+
keys: ["code", "state"],
65+
location: "query",
66+
},
6267
// Fragment params relating to 3pid (email) invites, added in url within the invite email itself
6368
threepid: {
6469
keys: ["client_secret", "session_id", "hs_url", "is_url", "sid"],

apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ describe("<MatrixChat />", () => {
321321
const code = "test-oidc-auth-code";
322322
const state = "test-oidc-state";
323323
const urlParams = {
324-
oidc: {
324+
oidc_fragment: {
325325
code,
326326
state: state,
327327
},
@@ -386,7 +386,7 @@ describe("<MatrixChat />", () => {
386386

387387
it("should fail when query params do not include valid code and state", async () => {
388388
const urlParams = {
389-
oidc: {
389+
oidc_query: {
390390
code: "",
391391
state: "abc",
392392
},

apps/web/test/unit-tests/utils/oidc/authorize-test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe("OIDC authorization", () => {
7575

7676
const authUrl = new URL(window.location.href);
7777

78-
expect(authUrl.searchParams.get("response_mode")).toEqual("fragment");
78+
expect(authUrl.searchParams.get("response_mode")).toEqual("query");
7979
expect(authUrl.searchParams.get("response_type")).toEqual("code");
8080
expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
8181
expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
@@ -90,6 +90,18 @@ describe("OIDC authorization", () => {
9090
expect(authUrl.searchParams.has("nonce")).toBeTruthy();
9191
expect(authUrl.searchParams.has("code_challenge")).toBeTruthy();
9292
});
93+
94+
it("should prefer response_mode fragment if supported", async () => {
95+
await startOidcLogin(
96+
{ ...delegatedAuthConfig, response_modes_supported: ["query", "fragment"] },
97+
clientId,
98+
homeserverUrl,
99+
);
100+
101+
const authUrl = new URL(window.location.href);
102+
103+
expect(authUrl.searchParams.get("response_mode")).toEqual("fragment");
104+
});
93105
});
94106

95107
describe("completeOidcLogin()", () => {
@@ -131,19 +143,19 @@ describe("OIDC authorization", () => {
131143
});
132144

133145
it("should throw when query params do not include state and code", async () => {
134-
await expect(async () => await completeOidcLogin({})).rejects.toThrow(
146+
await expect(async () => await completeOidcLogin({}, "query")).rejects.toThrow(
135147
OidcClientError.InvalidQueryParameters,
136148
);
137149
});
138150

139151
it("should make request complete authorization code grant", async () => {
140-
await completeOidcLogin(params);
152+
await completeOidcLogin(params, "fragment");
141153

142154
expect(completeAuthorizationCodeGrant).toHaveBeenCalledWith(code, state, "fragment");
143155
});
144156

145157
it("should return accessToken, configured homeserver and identityServer", async () => {
146-
const result = await completeOidcLogin(params);
158+
const result = await completeOidcLogin(params, "query");
147159

148160
expect(result).toEqual({
149161
accessToken: tokenResponse.access_token,

apps/web/test/unit-tests/vector/app-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe("sso_redirect_options", () => {
7676
});
7777

7878
it("should redirect for native OIDC", async () => {
79-
const authConfig = makeDelegatedAuthConfig(issuer);
79+
const authConfig = { ...makeDelegatedAuthConfig(issuer), response_modes_supported: ["query", "fragment"] };
8080
fetchMock.get("https://synapse/_matrix/client/v1/auth_metadata", authConfig);
8181
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
8282
fetchMock.get(authConfig.jwks_uri!, { keys: [] });

apps/web/test/unit-tests/vector/url_utils-test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,18 @@ describe("parseUrlParameters", () => {
4343
expect(parsed.params.legacy_sso?.loginToken).toEqual("foobar");
4444
});
4545

46-
it("should parse oidc parameters from oauth-fragment", () => {
46+
it("should parse oidc parameters from fragment", () => {
4747
const u = new URL("https://app.element.io/#code=foobar&state=barfoo");
4848
const parsed = parseAppUrl(u);
49-
expect(parsed.params.oidc?.code).toEqual("foobar");
50-
expect(parsed.params.oidc?.state).toEqual("barfoo");
49+
expect(parsed.params.oidc_fragment?.code).toEqual("foobar");
50+
expect(parsed.params.oidc_fragment?.state).toEqual("barfoo");
51+
});
52+
53+
it("should parse oidc parameters from query", () => {
54+
const u = new URL("https://app.element.io/?code=foobar&state=barfoo");
55+
const parsed = parseAppUrl(u);
56+
expect(parsed.params.oidc_query?.code).toEqual("foobar");
57+
expect(parsed.params.oidc_query?.state).toEqual("barfoo");
5158
});
5259

5360
it("should parse guest parameters", () => {

0 commit comments

Comments
 (0)