diff --git a/src/domain/login/CompleteOIDCLoginViewModel.js b/src/domain/login/CompleteOIDCLoginViewModel.js index 5d0da980..a544939a 100644 --- a/src/domain/login/CompleteOIDCLoginViewModel.js +++ b/src/domain/login/CompleteOIDCLoginViewModel.js @@ -50,7 +50,7 @@ export class CompleteOIDCLoginViewModel extends ViewModel { } const code = this._code; // TODO: cleanup settings storage - const [startedAt, nonce, codeVerifier, redirectUri, homeserver, issuer, clientId] = await Promise.all([ + const [startedAt, nonce, codeVerifier, redirectUri, homeserver, issuer, clientId, accountManagementUrl] = await Promise.all([ this.platform.settingsStorage.getInt(`oidc_${this._state}_started_at`), this.platform.settingsStorage.getString(`oidc_${this._state}_nonce`), this.platform.settingsStorage.getString(`oidc_${this._state}_code_verifier`), @@ -58,6 +58,7 @@ export class CompleteOIDCLoginViewModel extends ViewModel { this.platform.settingsStorage.getString(`oidc_${this._state}_homeserver`), this.platform.settingsStorage.getString(`oidc_${this._state}_issuer`), this.platform.settingsStorage.getString(`oidc_${this._state}_client_id`), + this.platform.settingsStorage.getString(`oidc_${this._state}_account_management_url`), ]); const oidcApi = new OidcApi({ @@ -67,7 +68,7 @@ export class CompleteOIDCLoginViewModel extends ViewModel { encoding: this._encoding, crypto: this._crypto, }); - const method = new OIDCLoginMethod({oidcApi, nonce, codeVerifier, code, homeserver, startedAt, redirectUri}); + const method = new OIDCLoginMethod({oidcApi, nonce, codeVerifier, code, homeserver, startedAt, redirectUri, accountManagementUrl}); const status = await this._attemptLogin(method); let error = ""; switch (status) { diff --git a/src/domain/login/StartOIDCLoginViewModel.js b/src/domain/login/StartOIDCLoginViewModel.js index 70980e32..07cae075 100644 --- a/src/domain/login/StartOIDCLoginViewModel.js +++ b/src/domain/login/StartOIDCLoginViewModel.js @@ -22,6 +22,7 @@ export class StartOIDCLoginViewModel extends ViewModel { super(options); this._isBusy = true; this._issuer = options.loginOptions.oidc.issuer; + this._accountManagementUrl = options.loginOptions.oidc.account; this._homeserver = options.loginOptions.homeserver; this._api = new OidcApi({ issuer: this._issuer, @@ -70,6 +71,7 @@ export class StartOIDCLoginViewModel extends ViewModel { 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}_client_id`, clientId), + this.platform.settingsStorage.setString(`oidc_${p.state}_account_management_url`, this._accountManagementUrl), ]); const link = await this._api.authorizationEndpoint(p); diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 4dcdb111..147d7402 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -53,6 +53,7 @@ export class SettingsViewModel extends ViewModel { this.pushNotifications = new PushNotificationStatus(); this._activeTheme = undefined; this._logsFeedbackMessage = undefined; + this._accountManagementUrl = null; } get _session() { @@ -82,9 +83,16 @@ export class SettingsViewModel extends ViewModel { if (!import.meta.env.DEV) { this._activeTheme = await this.platform.themeLoader.getActiveTheme(); } + const {accountManagementUrl} = await this.platform.sessionInfoStorage.get(this._client._sessionId); + this._accountManagementUrl = accountManagementUrl; this.emitChange(""); } + + get accountManagementUrl() { + return this._accountManagementUrl; + } + get closeUrl() { return this._closeUrl; } diff --git a/src/matrix/Client.js b/src/matrix/Client.js index d51f23a3..4597b511 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -125,7 +125,7 @@ export class Client { queryLogin(initialHomeserver) { return new AbortableOperation(async setAbortable => { - const { homeserver, issuer } = await lookupHomeserver(initialHomeserver, (url, options) => { + const { homeserver, issuer, account } = await lookupHomeserver(initialHomeserver, (url, options) => { return setAbortable(this._platform.request(url, options)); }); if (issuer) { @@ -140,7 +140,7 @@ export class Client { return { homeserver, - oidc: { issuer }, + oidc: { issuer, account }, }; } catch (e) { console.log(e); @@ -202,6 +202,7 @@ export class Client { if (loginData.oidc_issuer) { sessionInfo.oidcIssuer = loginData.oidc_issuer; sessionInfo.oidcClientId = loginData.oidc_client_id; + sessionInfo.accountManagementUrl = loginData.oidc_account_management_url; } log.set("id", sessionId); diff --git a/src/matrix/login/OIDCLoginMethod.ts b/src/matrix/login/OIDCLoginMethod.ts index b25689aa..e0e3f58f 100644 --- a/src/matrix/login/OIDCLoginMethod.ts +++ b/src/matrix/login/OIDCLoginMethod.ts @@ -25,6 +25,7 @@ export class OIDCLoginMethod implements ILoginMethod { private readonly _nonce: string; private readonly _redirectUri: string; private readonly _oidcApi: OidcApi; + private readonly _accountManagementUrl?: string; public readonly homeserver: string; constructor({ @@ -34,6 +35,7 @@ export class OIDCLoginMethod implements ILoginMethod { homeserver, redirectUri, oidcApi, + accountManagementUrl, }: { nonce: string, code: string, @@ -41,6 +43,7 @@ export class OIDCLoginMethod implements ILoginMethod { homeserver: string, redirectUri: string, oidcApi: OidcApi, + accountManagementUrl?: string, }) { this._oidcApi = oidcApi; this._code = code; @@ -48,6 +51,7 @@ export class OIDCLoginMethod implements ILoginMethod { this._nonce = nonce; this._redirectUri = redirectUri; this.homeserver = homeserver; + this._accountManagementUrl = accountManagementUrl; } async login(hsApi: HomeServerApi, _deviceName: string, log: ILogItem): Promise> { @@ -68,6 +72,6 @@ export class OIDCLoginMethod implements ILoginMethod { const oidc_issuer = this._oidcApi.issuer; const oidc_client_id = await this._oidcApi.clientId(); - return { oidc_issuer, oidc_client_id, 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, oidc_account_management_url: this._accountManagementUrl }; } } diff --git a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts index 80443e83..000879e8 100644 --- a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts +++ b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts @@ -24,6 +24,7 @@ interface ISessionInfo { accessTokenExpiresAt?: number; refreshToken?: string; oidcIssuer?: string; + accountManagementUrl?: string; lastUsed: number; } diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index 10e78f2c..9a858f2b 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -42,6 +42,7 @@ async function getWellKnownResponse(homeserver, request) { export async function lookupHomeserver(homeserver, request) { homeserver = normalizeHomeserver(homeserver); let issuer = null; + let account = null; const wellKnownResponse = await getWellKnownResponse(homeserver, request); if (wellKnownResponse && wellKnownResponse.status === 200) { const {body} = wellKnownResponse; @@ -54,6 +55,11 @@ export async function lookupHomeserver(homeserver, request) { if (typeof wellKnownIssuer === "string") { issuer = wellKnownIssuer; } + + const wellKnownAccount = body["org.matrix.msc2965.authentication"]?.["account"]; + if (typeof wellKnownAccount === "string") { + account = wellKnownAccount; + } } - return {homeserver, issuer}; + return {homeserver, issuer, account}; } diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index c4405e82..66290357 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -47,6 +47,13 @@ export class SettingsView extends TemplateView { disabled: vm => vm.isLoggingOut }, vm.i18n`Log out`)), ); + + settingNodes.push( + t.if(vm => vm.accountManagementUrl, t => { + return t.p([vm.i18n`You can manage your account `, t.a({href: vm.accountManagementUrl, target: "_blank"}, vm.i18n`here`), "."]); + }), + ); + settingNodes.push( t.h3("Key backup"), t.view(new KeyBackupSettingsView(vm.keyBackupViewModel))