Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

OIDC: revoke tokens on logout #11718

Merged
merged 97 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
2607997
test persistCredentials without a pickle key
Jul 11, 2023
3506c06
Merge branch 'develop' into kerry/25708/test-persist-credentials
Jul 12, 2023
609f790
test setLoggedIn with pickle key
Jul 12, 2023
f3092c7
lint
Jul 12, 2023
fad7f33
type error
Jul 12, 2023
32d5fb0
extract token persisting code into function, persist refresh token
Jul 12, 2023
e6529f1
store has_refresh_token too
Jul 12, 2023
66d57e5
pass refreshToken from oidcAuthGrant into credentials
Jul 12, 2023
b33e347
rest restore session with pickle key
Jul 13, 2023
823ba2e
Merge branch 'kerry/25708/test-persist-credentials' into kerry/25708/…
Jul 13, 2023
e91bbf4
retreive stored refresh token and add to credentials
Jul 13, 2023
b7e0603
Merge branch 'develop' into kerry/25708/test-persist-credentials
Jul 13, 2023
b8b0c86
Merge branch 'kerry/25708/test-persist-credentials' into kerry/25708/…
Jul 13, 2023
3ed9cc1
Merge branch 'develop' into kerry/25708/restore-refresh-token
Jul 13, 2023
221d306
extract token decryption into function
Jul 13, 2023
64dbc94
remove TODO
Jul 13, 2023
f059642
Merge branch 'develop' into kerry/25708/test-persist-credentials
Jul 16, 2023
9272110
Merge branch 'kerry/25708/test-persist-credentials' into kerry/25708/…
Jul 17, 2023
0f5fc31
Merge branch 'develop' into kerry/25708/restore-refresh-token
Jul 17, 2023
1708bef
very messy poc
Jul 17, 2023
1b76c18
Merge branch 'develop' into kerry/token-refresh-poc
Jul 18, 2023
d24fbd0
Merge branch 'develop' into kerry/25708/save-refresh-token
Jul 19, 2023
880c258
Merge branch 'kerry/25708/save-refresh-token' of https://github.com/m…
Jul 19, 2023
65c0734
Merge branch 'develop' into kerry/25708/save-refresh-token
Jul 19, 2023
2bab36b
utils to persist clientId and issuer after oidc authentication
Jul 19, 2023
66dc9fb
add dep oidc-client-ts
Jul 19, 2023
a5c0a51
persist issuer and clientId after successful oidc auth
Jul 19, 2023
50f3fe4
add OidcClientStore
Jul 20, 2023
3681b2e
comments and tidy
Jul 20, 2023
978109a
Merge branch 'develop' into kerry/25710/oidc-client-store
Jul 20, 2023
05d6252
Merge branch 'kerry/25708/restore-refresh-token' into kerry/25709/rev…
Jul 20, 2023
880671c
expose getters for stored refresh and access tokens in Lifecycle
Jul 20, 2023
97a9c89
revoke tokens with oidc provider
Jul 20, 2023
b75ad17
test logout action in MatrixChat
Jul 20, 2023
70ddb4a
comments
Jul 20, 2023
22329b9
Merge branch 'kerry/25708/save-refresh-token' into kerry/25708/restor…
Jul 20, 2023
56441dc
Merge branch 'develop' into kerry/25708/save-refresh-token
Jul 20, 2023
af481b2
prettier
Jul 20, 2023
976ec8c
Merge branch 'kerry/25708/save-refresh-token' into kerry/25708/restor…
Jul 20, 2023
70b7ca2
Merge branch 'kerry/25708/restore-refresh-token' into kerry/25709/rev…
Jul 20, 2023
07195c0
test OidcClientStore.revokeTokens
Jul 21, 2023
31822a5
Merge branch 'kerry/25710/test-logout' into kerry/25709/revoke-tokens
Jul 21, 2023
4f1bb4e
put pickle key destruction back
Jul 21, 2023
ae80087
Merge branch 'kerry/25708/restore-refresh-token' into kerry/token-ref…
Jul 21, 2023
ddd8ed7
Merge branch 'develop' into kerry/25708/restore-refresh-token
Sep 25, 2023
8cd5823
comment pedantry
Sep 25, 2023
ca24f0a
Merge branch 'kerry/25708/restore-refresh-token' into kerry/token-ref…
Sep 25, 2023
1fa7809
Merge branch 'develop' into kerry/token-refresh-poc
Oct 1, 2023
97fad4d
working refresh without persistence
Oct 1, 2023
e3673ee
extract token persistence functions to utils
Oct 2, 2023
41a4eb3
Merge branch 'kerry/25392/extract-token-functions' into kerry/token-r…
Oct 2, 2023
1c8e8cb
add sugar
Oct 2, 2023
0d558d7
Merge branch 'kerry/25392/extract-token-functions' into kerry/token-r…
Oct 2, 2023
5986b6c
implement TokenRefresher class with persistence
Oct 2, 2023
7db7291
tidying
Oct 2, 2023
dcd3026
persist idTokenClaims
Oct 2, 2023
4921e78
persist idTokenClaims
Oct 2, 2023
6ba08a2
tests
Oct 2, 2023
c962ca1
remove unused cde
Oct 2, 2023
0b4c4d8
Merge branch 'develop' into kerry/25392/persist-oidc-token-claims
Oct 2, 2023
3590f9c
Merge branch 'develop' into kerry/25392/persist-oidc-token-claims
Oct 2, 2023
87eb820
Merge branch 'kerry/25392/persist-oidc-token-claims' into kerry/token…
Oct 2, 2023
153ec78
create token refresher during doSetLoggedIn
Oct 2, 2023
ebdf0d5
tidying
Oct 2, 2023
2b1e73c
also tidying
Oct 2, 2023
b9b5411
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 3, 2023
e578107
Merge branch 'kerry/token-refresh-poc' into kerry/25709/revoke-tokens
Oct 4, 2023
f345a09
OidcClientStore.initClient use stored issuer when client well known u…
Oct 4, 2023
83de914
test Lifecycle.logout
Oct 4, 2023
7e081d4
update Lifecycle test replaceUsingCreds calls
Oct 4, 2023
7048c03
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 4, 2023
80dfd23
Merge branch 'kerry/25709/revoke-tokens' of https://github.com/matrix…
Oct 4, 2023
69b3cab
Merge branch 'kerry/token-refresh-poc' into kerry/25709/revoke-tokens
Oct 5, 2023
b2a3cd1
fix test
Oct 5, 2023
dabbee6
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 8, 2023
d302374
Merge branch 'develop' into kerry/token-refresh-poc
Oct 10, 2023
df70f53
tidy
Oct 10, 2023
0b0bb61
test tokenrefresher creation in login flow
Oct 10, 2023
8a47e6e
test token refresher
Oct 10, 2023
a32ca16
Update src/utils/oidc/TokenRefresher.ts
Oct 11, 2023
5370474
use literal value for m.authentication
Oct 11, 2023
7f40f86
improve comments
Oct 11, 2023
ead0aae
Merge branch 'kerry/token-refresh-poc' of https://github.com/matrix-o…
Oct 11, 2023
9c0fdf9
Merge branch 'develop' into kerry/token-refresh-poc
Oct 11, 2023
6953b45
Merge branch 'develop' into kerry/token-refresh-poc
Oct 11, 2023
541a6e6
Merge branch 'develop' into kerry/token-refresh-poc
Oct 11, 2023
ec3421a
Merge branch 'kerry/token-refresh-poc' into kerry/25709/revoke-tokens
Oct 11, 2023
866296d
fix test mock, comment
Oct 11, 2023
1a232b7
typo
Oct 12, 2023
fc10e64
add sdkContext to SoftLogout, pass oidcClientStore to logout
Oct 12, 2023
1f7dd1b
fullstops
Oct 12, 2023
f1fdd13
comments
Oct 12, 2023
09b8d13
fussy comment formatting
Oct 12, 2023
a3c50ad
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 12, 2023
ba2a561
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 12, 2023
a5a8d1b
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 12, 2023
21147d2
Merge branch 'develop' into kerry/25709/revoke-tokens
Oct 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPaylo
import { SdkContextClass } from "./contexts/SDKContext";
import { messageForLoginError } from "./utils/ErrorUtils";
import { completeOidcLogin } from "./utils/oidc/authorize";
import { OidcClientStore } from "./stores/oidc/OidcClientStore";
import {
getStoredOidcClientId,
getStoredOidcIdTokenClaims,
Expand Down Expand Up @@ -921,10 +922,29 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void

let _isLoggingOut = false;

/**
* Logs out the current session.
* When user has authenticated using OIDC native flow revoke tokens with OIDC provider.
* Otherwise, call /logout on the homeserver.
* @param client
* @param oidcClientStore
*/
async function doLogout(client: MatrixClient, oidcClientStore?: OidcClientStore): Promise<void> {
if (oidcClientStore?.isUserAuthenticatedWithOidc) {
const accessToken = client.getAccessToken() ?? undefined;
const refreshToken = client.getRefreshToken() ?? undefined;

await oidcClientStore.revokeTokens(accessToken, refreshToken);
} else {
await client.logout(true);
}
}

/**
* Logs the current session out and transitions to the logged-out state
* @param oidcClientStore store instance from SDKContext
*/
export function logout(): void {
export function logout(oidcClientStore?: OidcClientStore): void {
const client = MatrixClientPeg.get();
if (!client) return;

Expand All @@ -940,7 +960,8 @@ export function logout(): void {

_isLoggingOut = true;
PlatformPeg.get()?.destroyPickleKey(client.getSafeUserId(), client.getDeviceId() ?? "");
client.logout(true).then(onLoggedOut, (err) => {

doLogout(client, oidcClientStore).then(onLoggedOut, (err) => {
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
// you want to log into a different server, so just forget the
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Promise.all([
...[...CallStore.instance.activeCalls].map((call) => call.disconnect()),
cleanUpBroadcasts(this.stores),
]).finally(() => Lifecycle.logout());
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
break;
case "require_registration":
startAnyRegistrationFlow(payload as any);
Expand Down
12 changes: 9 additions & 3 deletions src/components/structures/auth/SoftLogout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
import Spinner from "../../views/elements/Spinner";
import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody";
import { SDKContext } from "../../../contexts/SDKContext";

enum LoginView {
Loading,
Expand Down Expand Up @@ -70,8 +71,13 @@ interface IState {
}

export default class SoftLogout extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
public static contextType = SDKContext;
public context!: React.ContextType<typeof SDKContext>;

public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props, context);

this.context = context;

this.state = {
loginView: LoginView.Loading,
Expand All @@ -98,7 +104,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
if (!wipeData) return;

logger.log("Clearing data from soft-logged-out session");
Lifecycle.logout();
Lifecycle.logout(this.context.oidcClientStore);
},
});
};
Expand Down
67 changes: 62 additions & 5 deletions src/stores/oidc/OidcClientStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand Down
88 changes: 69 additions & 19 deletions test/Lifecycle-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ import { logger } from "matrix-js-sdk/src/logger";
import * as MatrixJs from "matrix-js-sdk/src/matrix";
import { setCrypto } from "matrix-js-sdk/src/crypto/crypto";
import * as MatrixCryptoAes from "matrix-js-sdk/src/crypto/aes";
import { MockedObject } from "jest-mock";
import fetchMock from "fetch-mock-jest";

import StorageEvictedDialog from "../src/components/views/dialogs/StorageEvictedDialog";
import { restoreFromLocalStorage, setLoggedIn } from "../src/Lifecycle";
import { logout, restoreFromLocalStorage, setLoggedIn } from "../src/Lifecycle";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import Modal from "../src/Modal";
import * as StorageManager from "../src/utils/StorageManager";
import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser, mockPlatformPeg } from "./test-utils";
import ToastStore from "../src/stores/ToastStore";
import { OidcClientStore } from "../src/stores/oidc/OidcClientStore";
import { makeDelegatedAuthConfig } from "./test-utils/oidc";
import { persistOidcAuthenticatedSettings } from "../src/utils/oidc/persistOidcSettings";

Expand All @@ -40,24 +42,29 @@ describe("Lifecycle", () => {

const realLocalStorage = global.localStorage;

const mockClient = getMockClientWithEventEmitter({
stopClient: jest.fn(),
removeAllListeners: jest.fn(),
clearStores: jest.fn(),
getAccountData: jest.fn(),
getUserId: jest.fn(),
getDeviceId: jest.fn(),
isVersionSupported: jest.fn().mockResolvedValue(true),
getCrypto: jest.fn(),
getClientWellKnown: jest.fn(),
getThirdpartyProtocols: jest.fn(),
store: {
destroy: jest.fn(),
},
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
});
let mockClient!: MockedObject<MatrixJs.MatrixClient>;

beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
stopClient: jest.fn(),
removeAllListeners: jest.fn(),
clearStores: jest.fn(),
getAccountData: jest.fn(),
getDeviceId: jest.fn(),
isVersionSupported: jest.fn().mockResolvedValue(true),
getCrypto: jest.fn(),
getClientWellKnown: jest.fn(),
waitForClientWellKnown: jest.fn(),
getThirdpartyProtocols: jest.fn(),
store: {
destroy: jest.fn(),
},
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
logout: jest.fn().mockResolvedValue(undefined),
getAccessToken: jest.fn(),
getRefreshToken: jest.fn(),
});
// stub this
jest.spyOn(MatrixClientPeg, "replaceUsingCreds").mockImplementation(() => {});
jest.spyOn(MatrixClientPeg, "start").mockResolvedValue(undefined);
Expand Down Expand Up @@ -692,7 +699,7 @@ describe("Lifecycle", () => {

beforeEach(() => {
// mock oidc config for oidc client initialisation
mockClient.getClientWellKnown.mockReturnValue({
mockClient.waitForClientWellKnown.mockResolvedValue({
"m.authentication": {
issuer: issuer,
},
Expand Down Expand Up @@ -776,4 +783,47 @@ describe("Lifecycle", () => {
});
});
});

describe("logout()", () => {
let oidcClientStore!: OidcClientStore;
const accessToken = "test-access-token";
const refreshToken = "test-refresh-token";

beforeEach(() => {
oidcClientStore = new OidcClientStore(mockClient);
// stub
jest.spyOn(oidcClientStore, "revokeTokens").mockResolvedValue(undefined);

mockClient.getAccessToken.mockReturnValue(accessToken);
mockClient.getRefreshToken.mockReturnValue(refreshToken);
});

it("should call logout on the client when oidcClientStore is falsy", async () => {
logout();

await flushPromises();

expect(mockClient.logout).toHaveBeenCalledWith(true);
});

it("should call logout on the client when oidcClientStore.isUserAuthenticatedWithOidc is falsy", async () => {
jest.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(false);
logout(oidcClientStore);

await flushPromises();

expect(mockClient.logout).toHaveBeenCalledWith(true);
expect(oidcClientStore.revokeTokens).not.toHaveBeenCalled();
});

it("should revoke tokens when user is authenticated with oidc", async () => {
jest.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(true);
logout(oidcClientStore);

await flushPromises();

expect(mockClient.logout).not.toHaveBeenCalled();
expect(oidcClientStore.revokeTokens).toHaveBeenCalledWith(accessToken, refreshToken);
});
});
});
Loading
Loading