OIDC dynamic client registration

This commit is contained in:
Quentin Gliech 2022-04-25 09:31:00 +02:00
parent fdf11cc791
commit f1cc22a920
No known key found for this signature in database
GPG Key ID: 22D62B84552719FC
6 changed files with 74 additions and 17 deletions

View File

@ -50,18 +50,19 @@ export class CompleteOIDCLoginViewModel extends ViewModel {
} }
const code = this._code; const code = this._code;
// TODO: cleanup settings storage // TODO: cleanup settings storage
const [startedAt, nonce, codeVerifier, redirectUri, homeserver, issuer] = await Promise.all([ const [startedAt, nonce, codeVerifier, redirectUri, homeserver, issuer, clientId] = await Promise.all([
this.platform.settingsStorage.getInt(`oidc_${this._state}_started_at`), this.platform.settingsStorage.getInt(`oidc_${this._state}_started_at`),
this.platform.settingsStorage.getString(`oidc_${this._state}_nonce`), this.platform.settingsStorage.getString(`oidc_${this._state}_nonce`),
this.platform.settingsStorage.getString(`oidc_${this._state}_code_verifier`), this.platform.settingsStorage.getString(`oidc_${this._state}_code_verifier`),
this.platform.settingsStorage.getString(`oidc_${this._state}_redirect_uri`), this.platform.settingsStorage.getString(`oidc_${this._state}_redirect_uri`),
this.platform.settingsStorage.getString(`oidc_${this._state}_homeserver`), this.platform.settingsStorage.getString(`oidc_${this._state}_homeserver`),
this.platform.settingsStorage.getString(`oidc_${this._state}_issuer`), this.platform.settingsStorage.getString(`oidc_${this._state}_issuer`),
this.platform.settingsStorage.getString(`oidc_${this._state}_client_id`),
]); ]);
const oidcApi = new OidcApi({ const oidcApi = new OidcApi({
issuer, issuer,
clientId: "hydrogen-web", clientId,
request: this._request, request: this._request,
encoding: this._encoding, encoding: this._encoding,
crypto: this._crypto, crypto: this._crypto,

View File

@ -24,11 +24,11 @@ export class StartOIDCLoginViewModel extends ViewModel {
this._issuer = options.loginOptions.oidc.issuer; this._issuer = options.loginOptions.oidc.issuer;
this._homeserver = options.loginOptions.homeserver; this._homeserver = options.loginOptions.homeserver;
this._api = new OidcApi({ this._api = new OidcApi({
clientId: "hydrogen-web",
issuer: this._issuer, issuer: this._issuer,
request: this.platform.request, request: this.platform.request,
encoding: this.platform.encoding, encoding: this.platform.encoding,
crypto: this.platform.crypto, crypto: this.platform.crypto,
urlCreator: this.urlCreator,
}); });
} }
@ -42,6 +42,7 @@ export class StartOIDCLoginViewModel extends ViewModel {
async discover() { async discover() {
// Ask for the metadata once so it gets discovered and cached // Ask for the metadata once so it gets discovered and cached
await this._api.metadata() await this._api.metadata()
await this._api.ensureRegistered();
} }
async startOIDCLogin() { async startOIDCLogin() {
@ -49,6 +50,7 @@ export class StartOIDCLoginViewModel extends ViewModel {
scope: "openid", scope: "openid",
redirectUri: this.urlCreator.createOIDCRedirectURL(), redirectUri: this.urlCreator.createOIDCRedirectURL(),
}); });
const clientId = await this._api.clientId();
await Promise.all([ await Promise.all([
this.platform.settingsStorage.setInt(`oidc_${p.state}_started_at`, Date.now()), this.platform.settingsStorage.setInt(`oidc_${p.state}_started_at`, Date.now()),
this.platform.settingsStorage.setString(`oidc_${p.state}_nonce`, p.nonce), this.platform.settingsStorage.setString(`oidc_${p.state}_nonce`, p.nonce),
@ -56,6 +58,7 @@ export class StartOIDCLoginViewModel extends ViewModel {
this.platform.settingsStorage.setString(`oidc_${p.state}_redirect_uri`, p.redirectUri), this.platform.settingsStorage.setString(`oidc_${p.state}_redirect_uri`, p.redirectUri),
this.platform.settingsStorage.setString(`oidc_${p.state}_homeserver`, this._homeserver), this.platform.settingsStorage.setString(`oidc_${p.state}_homeserver`, this._homeserver),
this.platform.settingsStorage.setString(`oidc_${p.state}_issuer`, this._issuer), this.platform.settingsStorage.setString(`oidc_${p.state}_issuer`, this._issuer),
this.platform.settingsStorage.setString(`oidc_${p.state}_client_id`, clientId),
]); ]);
const link = await this._api.authorizationEndpoint(p); const link = await this._api.authorizationEndpoint(p);

View File

@ -129,6 +129,10 @@ export class URLRouter {
return window.location.origin; return window.location.origin;
} }
absoluteUrlForAsset(asset) {
return (new URL('/assets/' + asset, window.location.origin)).toString();
}
normalizeUrl() { normalizeUrl() {
// Remove any queryParameters from the URL // Remove any queryParameters from the URL
// Gets rid of the loginToken after SSO // Gets rid of the loginToken after SSO

View File

@ -132,7 +132,6 @@ export class Client {
try { try {
const oidcApi = new OidcApi({ const oidcApi = new OidcApi({
issuer, issuer,
clientId: "hydrogen-web",
request: this._platform.request, request: this._platform.request,
encoding: this._platform.encoding, encoding: this._platform.encoding,
crypto: this._platform.crypto, crypto: this._platform.crypto,
@ -202,6 +201,7 @@ export class Client {
if (loginData.oidc_issuer) { if (loginData.oidc_issuer) {
sessionInfo.oidcIssuer = loginData.oidc_issuer; sessionInfo.oidcIssuer = loginData.oidc_issuer;
sessionInfo.oidcClientId = loginData.oidc_client_id;
} }
log.set("id", sessionId); log.set("id", sessionId);
@ -263,7 +263,7 @@ export class Client {
if (sessionInfo.oidcIssuer) { if (sessionInfo.oidcIssuer) {
const oidcApi = new OidcApi({ const oidcApi = new OidcApi({
issuer: sessionInfo.oidcIssuer, issuer: sessionInfo.oidcIssuer,
clientId: "hydrogen-web", clientId: sessionInfo.oidcClientId,
request: this._platform.request, request: this._platform.request,
encoding: this._platform.encoding, encoding: this._platform.encoding,
crypto: this._platform.crypto, crypto: this._platform.crypto,

View File

@ -66,7 +66,8 @@ export class OIDCLoginMethod implements ILoginMethod {
}).response(); }).response();
const oidc_issuer = this._oidcApi.issuer; const oidc_issuer = this._oidcApi.issuer;
const oidc_client_id = await this._oidcApi.clientId();
return { oidc_issuer, access_token, refresh_token, expires_in, user_id, device_id }; return { oidc_issuer, oidc_client_id, access_token, refresh_token, expires_in, user_id, device_id };
} }
} }

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import type {RequestFunction} from "../../platform/types/types"; import type {RequestFunction} from "../../platform/types/types";
import type {URLRouter} from "../../domain/navigation/URLRouter.js";
const WELL_KNOWN = ".well-known/openid-configuration"; const WELL_KNOWN = ".well-known/openid-configuration";
@ -54,18 +55,35 @@ function assert(condition: any, message: string): asserts condition {
export class OidcApi { export class OidcApi {
_issuer: string; _issuer: string;
_clientId: string;
_requestFn: RequestFunction; _requestFn: RequestFunction;
_encoding: any; _encoding: any;
_crypto: any; _crypto: any;
_urlCreator: URLRouter;
_metadataPromise: Promise<any>; _metadataPromise: Promise<any>;
_registrationPromise: Promise<any>;
constructor({ issuer, clientId, request, encoding, crypto }) { constructor({ issuer, request, encoding, crypto, urlCreator, clientId }) {
this._issuer = issuer; this._issuer = issuer;
this._clientId = clientId;
this._requestFn = request; this._requestFn = request;
this._encoding = encoding; this._encoding = encoding;
this._crypto = crypto; this._crypto = crypto;
this._urlCreator = urlCreator;
if (clientId) {
this._registrationPromise = Promise.resolve({ client_id: clientId });
}
}
get clientMetadata() {
return {
client_name: "Hydrogen Web",
logo_uri: this._urlCreator.absoluteUrlForAsset("icon.png"),
response_types: ["code"],
grant_types: ["authorization_code", "refresh_token"],
redirect_uris: [this._urlCreator.createOIDCRedirectURL()],
id_token_signed_response_alg: "RS256",
token_endpoint_auth_method: "none",
};
} }
get metadataUrl() { get metadataUrl() {
@ -76,11 +94,35 @@ export class OidcApi {
return this._issuer; return this._issuer;
} }
get redirectUri() { async clientId(): Promise<string> {
return window.location.origin; return (await this.registration())["client_id"];
} }
metadata() { registration(): Promise<any> {
if (!this._registrationPromise) {
this._registrationPromise = (async () => {
const headers = new Map();
headers.set("Accept", "application/json");
headers.set("Content-Type", "application/json");
const req = this._requestFn(await this.registrationEndpoint(), {
method: "POST",
headers,
format: "json",
body: JSON.stringify(this.clientMetadata),
});
const res = await req.response();
if (res.status >= 400) {
throw new Error("failed to register client");
}
return res.body;
})();
}
return this._registrationPromise;
}
metadata(): Promise<any> {
if (!this._metadataPromise) { if (!this._metadataPromise) {
this._metadataPromise = (async () => { this._metadataPromise = (async () => {
const headers = new Map(); const headers = new Map();
@ -105,6 +147,7 @@ export class OidcApi {
const m = await this.metadata(); const m = await this.metadata();
assert(typeof m.authorization_endpoint === "string", "Has an authorization endpoint"); assert(typeof m.authorization_endpoint === "string", "Has an authorization endpoint");
assert(typeof m.token_endpoint === "string", "Has a token endpoint"); assert(typeof m.token_endpoint === "string", "Has a token endpoint");
assert(typeof m.registration_endpoint === "string", "Has a registration endpoint");
assert(Array.isArray(m.response_types_supported) && m.response_types_supported.includes("code"), "Supports the code response type"); assert(Array.isArray(m.response_types_supported) && m.response_types_supported.includes("code"), "Supports the code response type");
assert(Array.isArray(m.response_modes_supported) && m.response_modes_supported.includes("fragment"), "Supports the fragment response mode"); assert(Array.isArray(m.response_modes_supported) && m.response_modes_supported.includes("fragment"), "Supports the fragment response mode");
assert(Array.isArray(m.grant_types_supported) && m.grant_types_supported.includes("authorization_code"), "Supports the authorization_code grant type"); assert(Array.isArray(m.grant_types_supported) && m.grant_types_supported.includes("authorization_code"), "Supports the authorization_code grant type");
@ -126,13 +169,13 @@ export class OidcApi {
scope, scope,
nonce, nonce,
codeVerifier, codeVerifier,
}: AuthorizationParams) { }: AuthorizationParams): Promise<string> {
const metadata = await this.metadata(); const metadata = await this.metadata();
const url = new URL(metadata["authorization_endpoint"]); const url = new URL(metadata["authorization_endpoint"]);
url.searchParams.append("response_mode", "fragment"); url.searchParams.append("response_mode", "fragment");
url.searchParams.append("response_type", "code"); url.searchParams.append("response_type", "code");
url.searchParams.append("redirect_uri", redirectUri); url.searchParams.append("redirect_uri", redirectUri);
url.searchParams.append("client_id", this._clientId); url.searchParams.append("client_id", await this.clientId());
url.searchParams.append("state", state); url.searchParams.append("state", state);
url.searchParams.append("scope", scope); url.searchParams.append("scope", scope);
if (nonce) { if (nonce) {
@ -147,11 +190,16 @@ export class OidcApi {
return url.toString(); return url.toString();
} }
async tokenEndpoint() { async tokenEndpoint(): Promise<string> {
const metadata = await this.metadata(); const metadata = await this.metadata();
return metadata["token_endpoint"]; return metadata["token_endpoint"];
} }
async registrationEndpoint(): Promise<string> {
const metadata = await this.metadata();
return metadata["registration_endpoint"];
}
generateParams({ scope, redirectUri }: { scope: string, redirectUri: string }): AuthorizationParams { generateParams({ scope, redirectUri }: { scope: string, redirectUri: string }): AuthorizationParams {
return { return {
scope, scope,
@ -169,7 +217,7 @@ export class OidcApi {
}: { codeVerifier: string, code: string, redirectUri: string }): Promise<BearerToken> { }: { codeVerifier: string, code: string, redirectUri: string }): Promise<BearerToken> {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("grant_type", "authorization_code"); params.append("grant_type", "authorization_code");
params.append("client_id", this._clientId); params.append("client_id", await this.clientId());
params.append("code_verifier", codeVerifier); params.append("code_verifier", codeVerifier);
params.append("redirect_uri", redirectUri); params.append("redirect_uri", redirectUri);
params.append("code", code); params.append("code", code);
@ -201,7 +249,7 @@ export class OidcApi {
}: { refreshToken: string }): Promise<BearerToken> { }: { refreshToken: string }): Promise<BearerToken> {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("grant_type", "refresh_token"); params.append("grant_type", "refresh_token");
params.append("client_id", this._clientId); params.append("client_id", await this.clientId());
params.append("refresh_token", refreshToken); params.append("refresh_token", refreshToken);
const body = params.toString(); const body = params.toString();