mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-25 12:35:01 +01:00
OIDC dynamic client registration
This commit is contained in:
parent
fdf11cc791
commit
f1cc22a920
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user