OIDC: revoke tokens on logout (#11718)

* test persistCredentials without a pickle key

* test setLoggedIn with pickle key

* lint

* type error

* extract token persisting code into function, persist refresh token

* store has_refresh_token too

* pass refreshToken from oidcAuthGrant into credentials

* rest restore session with pickle key

* retreive stored refresh token and add to credentials

* extract token decryption into function

* remove TODO

* very messy poc

* utils to persist clientId and issuer after oidc authentication

* add dep oidc-client-ts

* persist issuer and clientId after successful oidc auth

* add OidcClientStore

* comments and tidy

* expose getters for stored refresh and access tokens in Lifecycle

* revoke tokens with oidc provider

* test logout action in MatrixChat

* comments

* prettier

* test OidcClientStore.revokeTokens

* put pickle key destruction back

* comment pedantry

* working refresh without persistence

* extract token persistence functions to utils

* add sugar

* implement TokenRefresher class with persistence

* tidying

* persist idTokenClaims

* persist idTokenClaims

* tests

* remove unused cde

* create token refresher during doSetLoggedIn

* tidying

* also tidying

* OidcClientStore.initClient use stored issuer when client well known unavailable

* test Lifecycle.logout

* update Lifecycle test replaceUsingCreds calls

* fix test

* tidy

* test tokenrefresher creation in login flow

* test token refresher

* Update src/utils/oidc/TokenRefresher.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* use literal value for m.authentication

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* improve comments

* fix test mock, comment

* typo

* add sdkContext to SoftLogout, pass oidcClientStore to logout

* fullstops

* comments

* fussy comment formatting

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Kerry
2023-10-15 21:35:25 +00:00
committed by GitHub
co-authored by Richard van der Hoff
parent fe3ad78a02
commit 84ca519b3f
6 changed files with 258 additions and 44 deletions
+62 -5
View File
@@ -50,6 +50,54 @@ export class OidcClientStore {
return this._accountManagementEndpoint;
}
/**
* Revokes provided access and refresh tokens with the configured OIDC provider
* @param accessToken
* @param refreshToken
* @returns Promise that resolves when tokens have been revoked
* @throws when OidcClient cannot be initialised, or revoking either token fails
*/
public async revokeTokens(accessToken?: string, refreshToken?: string): Promise<void> {
const client = await this.getOidcClient();
if (!client) {
throw new Error("No OIDC client");
}
const results = await Promise.all([
this.tryRevokeToken(client, accessToken, "access_token"),
this.tryRevokeToken(client, refreshToken, "refresh_token"),
]);
if (results.some((success) => !success)) {
throw new Error("Failed to revoke tokens");
}
}
/**
* Try to revoke a given token
* @param oidcClient
* @param token
* @param tokenType passed to revocation endpoint as token type hint
* @returns Promise that resolved with boolean whether the token revocation succeeded or not
*/
private async tryRevokeToken(
oidcClient: OidcClient,
token: string | undefined,
tokenType: "access_token" | "refresh_token",
): Promise<boolean> {
try {
if (!token) {
return false;
}
await oidcClient.revokeToken(token, tokenType);
return true;
} catch (error) {
logger.error(`Failed to revoke ${tokenType}`, error);
return false;
}
}
private async getOidcClient(): Promise<OidcClient | undefined> {
if (!this.oidcClient && !this.initialisingOidcClientPromise) {
this.initialisingOidcClientPromise = this.initOidcClient();
@@ -59,18 +107,27 @@ export class OidcClientStore {
return this.oidcClient;
}
/**
* Tries to initialise an OidcClient using stored clientId and OIDC discovery.
* Assigns this.oidcClient and accountManagement endpoint.
* Logs errors and does not throw when oidc client cannot be initialised.
* @returns promise that resolves when initialising OidcClient succeeds or fails
*/
private async initOidcClient(): Promise<void> {
const wellKnown = this.matrixClient.getClientWellKnown();
if (!wellKnown) {
logger.error("Cannot initialise OidcClientStore: client well known required.");
const wellKnown = await this.matrixClient.waitForClientWellKnown();
if (!wellKnown && !this.authenticatedIssuer) {
logger.error("Cannot initialise OIDC client without issuer.");
return;
}
const delegatedAuthConfig =
(wellKnown && M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown)) ?? undefined;
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) ?? undefined;
try {
const clientId = getStoredOidcClientId();
const { account, metadata, signingKeys } = await discoverAndValidateAuthenticationConfig(
delegatedAuthConfig,
// if HS has valid delegated auth config in .well-known, use it
// otherwise fallback to the known issuer
delegatedAuthConfig ?? { issuer: this.authenticatedIssuer! },
);
// if no account endpoint is configured default to the issuer
this._accountManagementEndpoint = account ?? metadata.issuer;