From 64fe60ea9996a501ffb0639fb68bc31c3ac1f07c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 16 Feb 2023 18:25:21 +0000 Subject: [PATCH] Store id_token and fix up logout implementation --- src/domain/LogoutViewModel.ts | 2 +- src/domain/navigation/URLRouter.ts | 5 ++ src/matrix/Client.js | 60 +++++++++++++------ src/matrix/login/OIDCLoginMethod.ts | 4 +- src/matrix/net/OidcApi.ts | 49 ++++++++++++++- .../localstorage/SessionInfoStorage.ts | 1 + 6 files changed, 98 insertions(+), 23 deletions(-) diff --git a/src/domain/LogoutViewModel.ts b/src/domain/LogoutViewModel.ts index 49933f2130..f3b6d1f2b2 100644 --- a/src/domain/LogoutViewModel.ts +++ b/src/domain/LogoutViewModel.ts @@ -52,7 +52,7 @@ export class LogoutViewModel extends ViewModel { this.emitChange("busy"); try { const client = new Client(this.platform); - await client.startLogout(this._sessionId); + await client.startLogout(this._sessionId, this.urlRouter); this.navigation.push("session", true); } catch (err) { this._error = err; diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index 872c9f7584..ae3e004190 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -33,6 +33,7 @@ export interface IURLRouter { openRoomActionUrl(roomId: string): string; createSSOCallbackURL(): string; createOIDCRedirectURL(): string; + createOIDCPostLogoutRedirectURL(): string; absoluteAppUrl(): string; absoluteUrlForAsset(asset: string): string; normalizeUrl(): void; @@ -159,6 +160,10 @@ export class URLRouter implements IURLRou return window.location.origin; } + createOIDCPostLogoutRedirectURL(): string { + return window.location.origin; + } + absoluteAppUrl(): string { return window.location.origin; } diff --git a/src/matrix/Client.js b/src/matrix/Client.js index bcbb374cf8..db6fca2b58 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -209,6 +209,10 @@ export class Client { sessionInfo.expiresIn = loginData.expires_in; } + if (loginData.id_token) { + sessionInfo.idToken = loginData.id_token; + } + if (loginData.oidc_issuer) { sessionInfo.oidcIssuer = loginData.oidc_issuer; sessionInfo.oidcClientId = loginData.oidc_client_id; @@ -236,7 +240,7 @@ export class Client { }); } - async _createSessionAfterAuth({deviceId, userId, accessToken, refreshToken, homeserver, expiresIn, oidcIssuer, oidcClientId, accountManagementUrl}, inspectAccountSetup, log) { + async _createSessionAfterAuth({deviceId, userId, accessToken, refreshToken, homeserver, expiresIn, idToken, oidcIssuer, oidcClientId, accountManagementUrl}, inspectAccountSetup, log) { const id = this.createNewSessionId(); const lastUsed = this._platform.clock.now(); const sessionInfo = { @@ -251,6 +255,7 @@ export class Client { oidcIssuer, oidcClientId, accountManagementUrl, + idToken, }; if (expiresIn) { sessionInfo.accessTokenExpiresAt = lastUsed + expiresIn * 1000; @@ -497,7 +502,7 @@ export class Client { return !this._reconnector; } - startLogout(sessionId) { + startLogout(sessionId, urlRouter) { return this._platform.logger.run("logout", async log => { this._sessionId = sessionId; log.set("id", this._sessionId); @@ -505,26 +510,43 @@ export class Client { if (!sessionInfo) { throw new Error(`Could not find session for id ${this._sessionId}`); } + let endSessionRedirectEndpoint; try { - const hsApi = new HomeServerApi({ - homeserver: sessionInfo.homeServer, - accessToken: sessionInfo.accessToken, - request: this._platform.request - }); - await hsApi.logout({log}).response(); - const oidcApi = new OidcApi({ - issuer: sessionInfo.oidcIssuer, - clientId: sessionInfo.oidcClientId, - request: this._platform.request, - encoding: this._platform.encoding, - crypto: this._platform.crypto, - }); - await oidcApi.revokeToken({ token: sessionInfo.accessToken, type: "access" }); - if (sessionInfo.refreshToken) { - await oidcApi.revokeToken({ token: sessionInfo.refreshToken, type: "refresh" }); + if (sessionInfo.oidcClientId) { + // OIDC logout + const oidcApi = new OidcApi({ + issuer: sessionInfo.oidcIssuer, + clientId: sessionInfo.oidcClientId, + request: this._platform.request, + encoding: this._platform.encoding, + crypto: this._platform.crypto, + urlRouter, + }); + await oidcApi.revokeToken({ token: sessionInfo.accessToken, type: "access" }); + if (sessionInfo.refreshToken) { + await oidcApi.revokeToken({ token: sessionInfo.refreshToken, type: "refresh" }); + } + endSessionRedirectEndpoint = await oidcApi.endSessionEndpoint({ + idTokenHint: sessionInfo.idToken, + logoutHint: sessionInfo.userId, + }) + } else { + // regular logout + const hsApi = new HomeServerApi({ + homeserver: sessionInfo.homeServer, + accessToken: sessionInfo.accessToken, + request: this._platform.request + }); + await hsApi.logout({log}).response(); } - } catch (err) {} + } catch (err) { + console.error(err); + } await this.deleteSession(log); + // OIDC might have given us a redirect URI to go to do tell the OP we are signing out + if (endSessionRedirectEndpoint) { + this._platform.openUrl(endSessionRedirectEndpoint); + } }); } diff --git a/src/matrix/login/OIDCLoginMethod.ts b/src/matrix/login/OIDCLoginMethod.ts index e0e3f58ff7..2533737bf3 100644 --- a/src/matrix/login/OIDCLoginMethod.ts +++ b/src/matrix/login/OIDCLoginMethod.ts @@ -55,7 +55,7 @@ export class OIDCLoginMethod implements ILoginMethod { } async login(hsApi: HomeServerApi, _deviceName: string, log: ILogItem): Promise> { - const { access_token, refresh_token, expires_in } = await this._oidcApi.completeAuthorizationCodeGrant({ + const { access_token, refresh_token, expires_in, id_token } = await this._oidcApi.completeAuthorizationCodeGrant({ code: this._code, codeVerifier: this._codeVerifier, redirectUri: this._redirectUri, @@ -72,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, oidc_account_management_url: this._accountManagementUrl }; + return { oidc_issuer, oidc_client_id, access_token, refresh_token, expires_in, id_token, user_id, device_id, oidc_account_management_url: this._accountManagementUrl }; } } diff --git a/src/matrix/net/OidcApi.ts b/src/matrix/net/OidcApi.ts index 08219a2236..f066f91830 100644 --- a/src/matrix/net/OidcApi.ts +++ b/src/matrix/net/OidcApi.ts @@ -30,6 +30,7 @@ type BearerToken = { access_token: string, refresh_token?: string, expires_in?: number, + id_token?: string, } const isValidBearerToken = (t: any): t is BearerToken => @@ -48,6 +49,28 @@ type AuthorizationParams = { codeVerifier?: string, }; +/** + * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html + */ +type LogoutParams = { + /** + * Maps to the `id_token_hint` parameter. + */ + idTokenHint?: string, + /** + * Maps to the `state` parameter. + */ + state?: string, + /** + * Maps to the `post_logout_redirect_uri` parameter. + */ + redirectUri?: string, + /** + * Maps to the `logout_hint` parameter. + */ + logoutHint?: string, +}; + function assert(condition: any, message: string): asserts condition { if (!condition) { throw new Error(`Assertion failed: ${message}`); @@ -97,6 +120,7 @@ export class OidcApi { redirect_uris: [this._urlRouter.createOIDCRedirectURL()], id_token_signed_response_alg: "RS256", token_endpoint_auth_method: "none", + post_logout_redirect_uris: [this._urlRouter.createOIDCPostLogoutRedirectURL()], }; } @@ -226,6 +250,30 @@ export class OidcApi { return metadata["revocation_endpoint"]; } + async endSessionEndpoint({idTokenHint, logoutHint, redirectUri, state}: LogoutParams): Promise { + const metadata = await this.metadata(); + const endpoint = metadata["end_session_endpoint"]; + if (!endpoint) { + return undefined; + } + if (!redirectUri) { + redirectUri = this._urlRouter.createOIDCPostLogoutRedirectURL(); + } + const url = new URL(endpoint); + url.searchParams.append("client_id", await this.clientId()); + url.searchParams.append("post_logout_redirect_uri", redirectUri); + if (idTokenHint) { + url.searchParams.append("id_token_hint", idTokenHint); + } + if (logoutHint) { + url.searchParams.append("logout_hint", logoutHint); + } + if (state) { + url.searchParams.append("state", state); + } + return url.href; + } + async isGuestAvailable(): Promise { const metadata = await this.metadata(); return metadata["scopes_supported"]?.includes("urn:matrix:org.matrix.msc2967.client:api:guest"); @@ -331,7 +379,6 @@ export class OidcApi { const req = this._requestFn(revocationEndpoint, { method: "POST", headers, - format: "json", body, }); diff --git a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts index 000879e8be..2a51383f04 100644 --- a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts +++ b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.ts @@ -26,6 +26,7 @@ interface ISessionInfo { oidcIssuer?: string; accountManagementUrl?: string; lastUsed: number; + idToken?: string; } // todo: this should probably be in platform/types?