Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit cd8679c

Browse files
authored
Improve client metadata used for OIDC dynamic registration (#12257)
1 parent e8ce9cb commit cd8679c

File tree

7 files changed

+80
-41
lines changed

7 files changed

+80
-41
lines changed

src/@types/common.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
import { JSXElementConstructor } from "react";
1818

19+
export type { NonEmptyArray } from "matrix-js-sdk/src/matrix";
20+
1921
// Based on https://stackoverflow.com/a/53229857/3532235
2022
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
2123
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
@@ -38,8 +40,6 @@ export type KeysStartingWith<Input extends object, Str extends string> = {
3840
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X
3941
}[keyof Input];
4042

41-
export type NonEmptyArray<T> = [T, ...T[]];
42-
4343
export type Defaultize<P, D> = P extends any
4444
? string extends keyof P
4545
? P

src/BasePlatform.ts

+37-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ See the License for the specific language governing permissions and
1717
limitations under the License.
1818
*/
1919

20-
import { MatrixClient, MatrixEvent, Room, SSOAction, encodeUnpaddedBase64 } from "matrix-js-sdk/src/matrix";
20+
import {
21+
MatrixClient,
22+
MatrixEvent,
23+
Room,
24+
SSOAction,
25+
encodeUnpaddedBase64,
26+
OidcRegistrationClientMetadata,
27+
} from "matrix-js-sdk/src/matrix";
2128
import { logger } from "matrix-js-sdk/src/logger";
2229

2330
import dis from "./dispatcher/dispatcher";
@@ -30,6 +37,7 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
3037
import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager";
3138
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
3239
import { IConfigOptions } from "./IConfigOptions";
40+
import SdkConfig from "./SdkConfig";
3341

3442
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
3543
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
@@ -426,7 +434,7 @@ export default abstract class BasePlatform {
426434
/**
427435
* Delete a previously stored pickle key from storage.
428436
* @param {string} userId the user ID for the user that the pickle key is for.
429-
* @param {string} userId the device ID that the pickle key is for.
437+
* @param {string} deviceId the device ID that the pickle key is for.
430438
*/
431439
public async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
432440
try {
@@ -443,4 +451,31 @@ export default abstract class BasePlatform {
443451
window.sessionStorage.clear();
444452
window.localStorage.clear();
445453
}
454+
455+
/**
456+
* Base URL to use when generating external links for this client, for platforms e.g. Desktop this will be a different instance
457+
*/
458+
public get baseUrl(): string {
459+
return window.location.origin + window.location.pathname;
460+
}
461+
462+
/**
463+
* Metadata to use for dynamic OIDC client registrations
464+
*/
465+
public async getOidcClientMetadata(): Promise<OidcRegistrationClientMetadata> {
466+
const config = SdkConfig.get();
467+
return {
468+
clientName: config.brand,
469+
clientUri: this.baseUrl,
470+
redirectUris: [this.getSSOCallbackUrl().href],
471+
logoUri: new URL("vector-icons/1024.png", this.baseUrl).href,
472+
applicationType: "web",
473+
// XXX: We break the spec by not consistently supplying these required fields
474+
// contacts: [],
475+
// @ts-ignore
476+
tosUri: config.terms_and_conditions_links?.[0]?.url,
477+
// @ts-ignore
478+
policyUri: config.privacy_policy_url,
479+
};
480+
}
446481
}

src/Login.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ export default class Login {
120120
try {
121121
const oidcFlow = await tryInitOidcNativeFlow(
122122
this.delegatedAuthentication,
123-
SdkConfig.get().brand,
124123
SdkConfig.get().oidc_static_clients,
125124
isRegistration,
126125
);
@@ -223,23 +222,21 @@ export interface OidcNativeFlow extends ILoginFlow {
223222
* results.
224223
*
225224
* @param delegatedAuthConfig Auth config from ValidatedServerConfig
226-
* @param clientName Client name to register with the OP, eg 'Element', used during client registration with OP
227225
* @param staticOidcClientIds static client config from config.json, used during client registration with OP
228226
* @param isRegistration true when we are attempting registration
229227
* @returns Promise<OidcNativeFlow> when oidc native authentication flow is supported and correctly configured
230228
* @throws when client can't register with OP, or any unexpected error
231229
*/
232230
const tryInitOidcNativeFlow = async (
233231
delegatedAuthConfig: OidcClientConfig,
234-
brand: string,
235-
oidcStaticClients?: IConfigOptions["oidc_static_clients"],
232+
staticOidcClientIds?: IConfigOptions["oidc_static_clients"],
236233
isRegistration?: boolean,
237234
): Promise<OidcNativeFlow> => {
238235
// if registration is not supported, bail before attempting to get the clientId
239236
if (isRegistration && !isUserRegistrationSupported(delegatedAuthConfig)) {
240237
throw new Error("Registration is not supported by OP");
241238
}
242-
const clientId = await getOidcClientId(delegatedAuthConfig, brand, window.location.origin, oidcStaticClients);
239+
const clientId = await getOidcClientId(delegatedAuthConfig, staticOidcClientIds);
243240

244241
const flow = {
245242
type: "oidcNativeFlow",

src/utils/oidc/registerClient.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { registerOidcClient } from "matrix-js-sdk/src/oidc/register";
1919

2020
import { IConfigOptions } from "../../IConfigOptions";
2121
import { ValidatedDelegatedAuthConfig } from "../ValidatedServerConfig";
22+
import PlatformPeg from "../../PlatformPeg";
2223

2324
/**
2425
* Get the statically configured clientId for the issuer
@@ -40,22 +41,18 @@ const getStaticOidcClientId = (
4041
* Checks statically configured clientIds first
4142
* Then attempts dynamic registration with the OP
4243
* @param delegatedAuthConfig Auth config from ValidatedServerConfig
43-
* @param clientName Client name to register with the OP, eg 'Element'
44-
* @param baseUrl URL of the home page of the Client, eg 'https://app.element.io/'
4544
* @param staticOidcClients static client config from config.json
4645
* @returns Promise<string> resolves with clientId
4746
* @throws if no clientId is found
4847
*/
4948
export const getOidcClientId = async (
5049
delegatedAuthConfig: ValidatedDelegatedAuthConfig,
51-
clientName: string,
52-
baseUrl: string,
5350
staticOidcClients?: IConfigOptions["oidc_static_clients"],
5451
): Promise<string> => {
5552
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients);
5653
if (staticClientId) {
5754
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`);
5855
return staticClientId;
5956
}
60-
return await registerOidcClient(delegatedAuthConfig, clientName, baseUrl);
57+
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());
6158
};

test/components/structures/auth/Login-test.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -415,12 +415,7 @@ describe("Login", function () {
415415
// tried to register
416416
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
417417
// called with values from config
418-
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(
419-
delegatedAuth,
420-
"test-brand",
421-
"http://localhost",
422-
oidcStaticClientsConfig,
423-
);
418+
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
424419
});
425420

426421
it("should fallback to normal login when client registration fails", async () => {

test/test-utils/platform.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import PlatformPeg from "../../src/PlatformPeg";
2222
// doesn't implement abstract
2323
// @ts-ignore
2424
class MockPlatform extends BasePlatform {
25-
constructor(platformMocks: Partial<Record<MethodLikeKeys<BasePlatform>, unknown>>) {
25+
constructor(platformMocks: Partial<Record<keyof BasePlatform, unknown>>) {
2626
super();
2727
Object.assign(this, platformMocks);
2828
}

test/utils/oidc/registerClient-test.ts

+35-20
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { OidcError } from "matrix-js-sdk/src/oidc/error";
1919

2020
import { getOidcClientId } from "../../../src/utils/oidc/registerClient";
2121
import { ValidatedDelegatedAuthConfig } from "../../../src/utils/ValidatedServerConfig";
22+
import { mockPlatformPeg } from "../../test-utils";
23+
import PlatformPeg from "../../../src/PlatformPeg";
2224

2325
describe("getOidcClientId()", () => {
2426
const issuer = "https://auth.com/";
@@ -41,10 +43,21 @@ describe("getOidcClientId()", () => {
4143
beforeEach(() => {
4244
fetchMockJest.mockClear();
4345
fetchMockJest.resetBehavior();
46+
mockPlatformPeg();
47+
Object.defineProperty(PlatformPeg.get(), "baseUrl", {
48+
get(): string {
49+
return baseUrl;
50+
},
51+
});
52+
Object.defineProperty(PlatformPeg.get(), "getSSOCallbackUrl", {
53+
value: () => ({
54+
href: baseUrl,
55+
}),
56+
});
4457
});
4558

4659
it("should return static clientId when configured", async () => {
47-
expect(await getOidcClientId(delegatedAuthConfig, clientName, baseUrl, staticOidcClients)).toEqual("abc123");
60+
expect(await getOidcClientId(delegatedAuthConfig, staticOidcClients)).toEqual("abc123");
4861
// didn't try to register
4962
expect(fetchMockJest).toHaveFetchedTimes(0);
5063
});
@@ -55,9 +68,9 @@ describe("getOidcClientId()", () => {
5568
issuer: "https://issuerWithoutStaticClientId.org/",
5669
registrationEndpoint: undefined,
5770
};
58-
await expect(
59-
getOidcClientId(authConfigWithoutRegistration, clientName, baseUrl, staticOidcClients),
60-
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
71+
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
72+
OidcError.DynamicRegistrationNotSupported,
73+
);
6174
// didn't try to register
6275
expect(fetchMockJest).toHaveFetchedTimes(0);
6376
});
@@ -67,7 +80,7 @@ describe("getOidcClientId()", () => {
6780
...delegatedAuthConfig,
6881
registrationEndpoint: undefined,
6982
};
70-
await expect(getOidcClientId(authConfigWithoutRegistration, clientName, baseUrl)).rejects.toThrow(
83+
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
7184
OidcError.DynamicRegistrationNotSupported,
7285
);
7386
// didn't try to register
@@ -79,15 +92,20 @@ describe("getOidcClientId()", () => {
7992
status: 200,
8093
body: JSON.stringify({ client_id: dynamicClientId }),
8194
});
82-
expect(await getOidcClientId(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId);
95+
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
8396
// didn't try to register
84-
expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, {
85-
headers: {
86-
"Accept": "application/json",
87-
"Content-Type": "application/json",
88-
},
89-
method: "POST",
90-
body: JSON.stringify({
97+
expect(fetchMockJest).toHaveBeenCalledWith(
98+
registrationEndpoint,
99+
expect.objectContaining({
100+
headers: {
101+
"Accept": "application/json",
102+
"Content-Type": "application/json",
103+
},
104+
method: "POST",
105+
}),
106+
);
107+
expect(JSON.parse(fetchMockJest.mock.calls[0][1]!.body as string)).toEqual(
108+
expect.objectContaining({
91109
client_name: clientName,
92110
client_uri: baseUrl,
93111
response_types: ["code"],
@@ -96,17 +114,16 @@ describe("getOidcClientId()", () => {
96114
id_token_signed_response_alg: "RS256",
97115
token_endpoint_auth_method: "none",
98116
application_type: "web",
117+
logo_uri: `${baseUrl}/vector-icons/1024.png`,
99118
}),
100-
});
119+
);
101120
});
102121

103122
it("should throw when registration request fails", async () => {
104123
fetchMockJest.post(registrationEndpoint, {
105124
status: 500,
106125
});
107-
await expect(getOidcClientId(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
108-
OidcError.DynamicRegistrationFailed,
109-
);
126+
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
110127
});
111128

112129
it("should throw when registration response is invalid", async () => {
@@ -115,8 +132,6 @@ describe("getOidcClientId()", () => {
115132
// no clientId in response
116133
body: "{}",
117134
});
118-
await expect(getOidcClientId(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
119-
OidcError.DynamicRegistrationInvalid,
120-
);
135+
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationInvalid);
121136
});
122137
});

0 commit comments

Comments
 (0)