Skip to content

Commit de8c561

Browse files
committed
OIDC dynamic client registration
1 parent 9f7e508 commit de8c561

File tree

6 files changed

+75
-17
lines changed

6 files changed

+75
-17
lines changed

src/domain/login/CompleteOIDCLoginViewModel.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,19 @@ export class CompleteOIDCLoginViewModel extends ViewModel {
5050
}
5151
const code = this._code;
5252
// TODO: cleanup settings storage
53-
const [startedAt, nonce, codeVerifier, redirectUri, homeserver, issuer] = await Promise.all([
53+
const [startedAt, nonce, codeVerifier, redirectUri, homeserver, issuer, clientId] = await Promise.all([
5454
this.platform.settingsStorage.getInt(`oidc_${this._state}_started_at`),
5555
this.platform.settingsStorage.getString(`oidc_${this._state}_nonce`),
5656
this.platform.settingsStorage.getString(`oidc_${this._state}_code_verifier`),
5757
this.platform.settingsStorage.getString(`oidc_${this._state}_redirect_uri`),
5858
this.platform.settingsStorage.getString(`oidc_${this._state}_homeserver`),
5959
this.platform.settingsStorage.getString(`oidc_${this._state}_issuer`),
60+
this.platform.settingsStorage.getString(`oidc_${this._state}_client_id`),
6061
]);
6162

6263
const oidcApi = new OidcApi({
6364
issuer,
64-
clientId: "hydrogen-web",
65+
clientId,
6566
request: this._request,
6667
encoding: this._encoding,
6768
crypto: this._crypto,

src/domain/login/StartOIDCLoginViewModel.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ export class StartOIDCLoginViewModel extends ViewModel {
2424
this._issuer = options.loginOptions.oidc.issuer;
2525
this._homeserver = options.loginOptions.homeserver;
2626
this._api = new OidcApi({
27-
clientId: "hydrogen-web",
2827
issuer: this._issuer,
2928
request: this.platform.request,
3029
encoding: this.platform.encoding,
3130
crypto: this.platform.crypto,
31+
urlCreator: this.urlCreator,
3232
});
3333
}
3434

@@ -42,20 +42,23 @@ export class StartOIDCLoginViewModel extends ViewModel {
4242
async discover() {
4343
// Ask for the metadata once so it gets discovered and cached
4444
await this._api.metadata()
45+
await this._api.ensureRegistered();
4546
}
4647

4748
async startOIDCLogin() {
4849
const p = this._api.generateParams({
4950
scope: "openid",
5051
redirectUri: this.urlCreator.createOIDCRedirectURL(),
5152
});
53+
const clientId = await this._api.clientId();
5254
await Promise.all([
5355
this.platform.settingsStorage.setInt(`oidc_${p.state}_started_at`, Date.now()),
5456
this.platform.settingsStorage.setString(`oidc_${p.state}_nonce`, p.nonce),
5557
this.platform.settingsStorage.setString(`oidc_${p.state}_code_verifier`, p.codeVerifier),
5658
this.platform.settingsStorage.setString(`oidc_${p.state}_redirect_uri`, p.redirectUri),
5759
this.platform.settingsStorage.setString(`oidc_${p.state}_homeserver`, this._homeserver),
5860
this.platform.settingsStorage.setString(`oidc_${p.state}_issuer`, this._issuer),
61+
this.platform.settingsStorage.setString(`oidc_${p.state}_client_id`, clientId),
5962
]);
6063

6164
const link = await this._api.authorizationEndpoint(p);

src/domain/navigation/URLRouter.js

+4
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ export class URLRouter {
129129
return window.location.origin;
130130
}
131131

132+
absoluteUrlForAsset(asset) {
133+
return (new URL('/assets/' + asset, window.location.origin)).toString();
134+
}
135+
132136
normalizeUrl() {
133137
// Remove any queryParameters from the URL
134138
// Gets rid of the loginToken after SSO

src/matrix/Client.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ export class Client {
132132
try {
133133
const oidcApi = new OidcApi({
134134
issuer,
135-
clientId: "hydrogen-web",
136135
request: this._platform.request,
137136
encoding: this._platform.encoding,
138137
crypto: this._platform.crypto,
@@ -201,6 +200,7 @@ export class Client {
201200

202201
if (loginData.oidc_issuer) {
203202
sessionInfo.oidcIssuer = loginData.oidc_issuer;
203+
sessionInfo.oidcClientId = loginData.oidc_client_id;
204204
}
205205

206206
log.set("id", sessionId);
@@ -262,7 +262,7 @@ export class Client {
262262
if (sessionInfo.oidcIssuer) {
263263
const oidcApi = new OidcApi({
264264
issuer: sessionInfo.oidcIssuer,
265-
clientId: "hydrogen-web",
265+
clientId: sessionInfo.oidcClientId,
266266
request: this._platform.request,
267267
encoding: this._platform.encoding,
268268
crypto: this._platform.crypto,

src/matrix/login/OIDCLoginMethod.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export class OIDCLoginMethod implements ILoginMethod {
6666
}).response();
6767

6868
const oidc_issuer = this._oidcApi.issuer;
69+
const oidc_client_id = await this._oidcApi.clientId();
6970

70-
return { oidc_issuer, access_token, refresh_token, expires_in, user_id, device_id };
71+
return { oidc_issuer, oidc_client_id, access_token, refresh_token, expires_in, user_id, device_id };
7172
}
7273
}

src/matrix/net/OidcApi.ts

+60-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import type {RequestFunction} from "../../platform/types/types";
18+
import type {URLRouter} from "../../domain/navigation/URLRouter.js";
1819

1920
const WELL_KNOWN = ".well-known/openid-configuration";
2021

@@ -41,6 +42,7 @@ const isValidBearerToken = (t: any): t is BearerToken =>
4142
type AuthorizationParams = {
4243
state: string,
4344
scope: string,
45+
clientId: string,
4446
redirectUri: string,
4547
nonce?: string,
4648
codeVerifier?: string,
@@ -54,18 +56,35 @@ function assert(condition: any, message: string): asserts condition {
5456

5557
export class OidcApi {
5658
_issuer: string;
57-
_clientId: string;
5859
_requestFn: RequestFunction;
5960
_encoding: any;
6061
_crypto: any;
62+
_urlCreator: URLRouter;
6163
_metadataPromise: Promise<any>;
64+
_registrationPromise: Promise<any>;
6265

63-
constructor({ issuer, clientId, request, encoding, crypto }) {
66+
constructor({ issuer, request, encoding, crypto, urlCreator, clientId }) {
6467
this._issuer = issuer;
65-
this._clientId = clientId;
6668
this._requestFn = request;
6769
this._encoding = encoding;
6870
this._crypto = crypto;
71+
this._urlCreator = urlCreator;
72+
73+
if (clientId) {
74+
this._registrationPromise = Promise.resolve({ client_id: clientId });
75+
}
76+
}
77+
78+
get clientMetadata() {
79+
return {
80+
client_name: "Hydrogen Web",
81+
logo_uri: this._urlCreator.absoluteUrlForAsset("icon.png"),
82+
response_types: ["code"],
83+
grant_types: ["authorization_code", "refresh_token"],
84+
redirect_uris: [this._urlCreator.createOIDCRedirectURL()],
85+
id_token_signed_response_alg: "RS256",
86+
token_endpoint_auth_method: "none",
87+
};
6988
}
7089

7190
get metadataUrl() {
@@ -76,11 +95,35 @@ export class OidcApi {
7695
return this._issuer;
7796
}
7897

79-
get redirectUri() {
80-
return window.location.origin;
98+
async clientId(): Promise<string> {
99+
return (await this.registration())["client_id"];
100+
}
101+
102+
registration(): Promise<any> {
103+
if (!this._registrationPromise) {
104+
this._registrationPromise = (async () => {
105+
const headers = new Map();
106+
headers.set("Accept", "application/json");
107+
headers.set("Content-Type", "application/json");
108+
const req = this._requestFn(await this.registrationEndpoint(), {
109+
method: "POST",
110+
headers,
111+
format: "json",
112+
body: JSON.stringify(this.clientMetadata),
113+
});
114+
const res = await req.response();
115+
if (res.status >= 400) {
116+
throw new Error("failed to register client");
117+
}
118+
119+
return res.body;
120+
})();
121+
}
122+
123+
return this._registrationPromise;
81124
}
82125

83-
metadata() {
126+
metadata(): Promise<any> {
84127
if (!this._metadataPromise) {
85128
this._metadataPromise = (async () => {
86129
const headers = new Map();
@@ -105,6 +148,7 @@ export class OidcApi {
105148
const m = await this.metadata();
106149
assert(typeof m.authorization_endpoint === "string", "Has an authorization endpoint");
107150
assert(typeof m.token_endpoint === "string", "Has a token endpoint");
151+
assert(typeof m.registration_endpoint === "string", "Has a registration endpoint");
108152
assert(Array.isArray(m.response_types_supported) && m.response_types_supported.includes("code"), "Supports the code response type");
109153
assert(Array.isArray(m.response_modes_supported) && m.response_modes_supported.includes("fragment"), "Supports the fragment response mode");
110154
assert(Array.isArray(m.grant_types_supported) && m.grant_types_supported.includes("authorization_code"), "Supports the authorization_code grant type");
@@ -126,13 +170,13 @@ export class OidcApi {
126170
scope,
127171
nonce,
128172
codeVerifier,
129-
}: AuthorizationParams) {
173+
}: AuthorizationParams): Promise<string> {
130174
const metadata = await this.metadata();
131175
const url = new URL(metadata["authorization_endpoint"]);
132176
url.searchParams.append("response_mode", "fragment");
133177
url.searchParams.append("response_type", "code");
134178
url.searchParams.append("redirect_uri", redirectUri);
135-
url.searchParams.append("client_id", this._clientId);
179+
url.searchParams.append("client_id", await this.clientId());
136180
url.searchParams.append("state", state);
137181
url.searchParams.append("scope", scope);
138182
if (nonce) {
@@ -147,11 +191,16 @@ export class OidcApi {
147191
return url.toString();
148192
}
149193

150-
async tokenEndpoint() {
194+
async tokenEndpoint(): Promise<string> {
151195
const metadata = await this.metadata();
152196
return metadata["token_endpoint"];
153197
}
154198

199+
async registrationEndpoint(): Promise<string> {
200+
const metadata = await this.metadata();
201+
return metadata["registration_endpoint"];
202+
}
203+
155204
generateParams({ scope, redirectUri }: { scope: string, redirectUri: string }): AuthorizationParams {
156205
return {
157206
scope,
@@ -169,7 +218,7 @@ export class OidcApi {
169218
}: { codeVerifier: string, code: string, redirectUri: string }): Promise<BearerToken> {
170219
const params = new URLSearchParams();
171220
params.append("grant_type", "authorization_code");
172-
params.append("client_id", this._clientId);
221+
params.append("client_id", await this.clientId());
173222
params.append("code_verifier", codeVerifier);
174223
params.append("redirect_uri", redirectUri);
175224
params.append("code", code);
@@ -201,7 +250,7 @@ export class OidcApi {
201250
}: { refreshToken: string }): Promise<BearerToken> {
202251
const params = new URLSearchParams();
203252
params.append("grant_type", "refresh_token");
204-
params.append("client_id", this._clientId);
253+
params.append("client_id", await this.clientId());
205254
params.append("refresh_token", refreshToken);
206255
const body = params.toString();
207256

0 commit comments

Comments
 (0)