Skip to content

Commit 292dde3

Browse files
committed
fix: stores were not cleared properly when switching sso accounts
1 parent 2797dba commit 292dde3

File tree

4 files changed

+139
-104
lines changed

4 files changed

+139
-104
lines changed

src/hooks/useSso.ts

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Unomed AG
2+
* Copyright 2024-2025 Unomed AG
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,65 +14,78 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { createClient } from 'matrix-js-sdk';
1817
import { useEffect, useState } from 'react';
1918
import { clearCredentials, getCredentials, storeCredentials } from '../auth/credentials';
19+
import defaultConfigClient from '../utils/client';
2020

21-
const useSso = (baseUrl: string) => {
21+
const useSso = (baseUrl: string, loggedInUserId: string | undefined) => {
2222
const [accessToken, setAccessToken] = useState<string>();
2323
const [userId, setUserId] = useState<string>();
2424
const [deviceId, setDeviceId] = useState<string>();
2525

2626
useEffect(() => {
27-
2827
const ssoLogin = async () => {
2928
const queryParams = new URLSearchParams(window.location.search);
3029
const loginToken = queryParams.get('loginToken');
30+
const baseDomain = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
31+
32+
if (loginToken) {
33+
const mx = defaultConfigClient(baseUrl);
34+
// clear any existing stores
35+
await mx.clearStores();
36+
clearCredentials();
3137

32-
const [accessToken, userId, deviceId] = getCredentials();
38+
try {
39+
const payload = await mx.loginWithToken(loginToken);
40+
if (payload.access_token) {
41+
storeCredentials(payload);
42+
}
43+
} finally {
44+
window.history.replaceState({}, document.title, baseDomain);
45+
}
46+
}
47+
48+
const [existingAccessToken, existingUserId, existingDeviceId] = getCredentials();
3349

34-
let isLoggedIn = false;
3550
// Check if the existing credentials are valid
36-
if (accessToken && userId && deviceId) {
37-
const mx = createClient({ baseUrl, accessToken, userId, deviceId });
51+
let isLoggedin = false;
52+
if (loggedInUserId === existingUserId
53+
&& existingAccessToken
54+
&& existingUserId
55+
&& existingDeviceId
56+
) {
57+
const mx = defaultConfigClient(
58+
baseUrl,
59+
existingAccessToken,
60+
existingUserId,
61+
existingDeviceId,
62+
);
63+
3864
try {
3965
await mx.whoami();
40-
isLoggedIn = true;
41-
setAccessToken(accessToken);
42-
setUserId(userId);
43-
setDeviceId(deviceId);
66+
setAccessToken(existingAccessToken);
67+
setUserId(existingUserId);
68+
setDeviceId(existingDeviceId);
69+
isLoggedin = true;
4470
} catch {
4571
// credentials are not valid
4672
}
4773
}
4874

49-
if (!isLoggedIn) {
50-
const mx = createClient({ baseUrl });
51-
const baseDomain = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
52-
if (loginToken) {
53-
// Clear any existing stores
54-
await mx.clearStores();
55-
56-
const payload = await mx.loginWithToken(loginToken);
57-
if (payload.access_token) {
58-
storeCredentials(payload);
59-
setAccessToken(payload.access_token);
60-
setUserId(payload.user_id);
61-
setDeviceId(payload.device_id);
62-
window.history.replaceState({}, document.title, baseDomain);
63-
} else {
64-
clearCredentials();
65-
}
66-
} else {
67-
clearCredentials();
68-
const url = mx.getSsoLoginUrl(baseDomain, 'sso');
69-
window.location.replace(url);
70-
}
75+
if (!isLoggedin) {
76+
const mx = defaultConfigClient(baseUrl);
77+
// clear any existing stores
78+
await mx.clearStores();
79+
clearCredentials();
80+
const url = mx.getSsoLoginUrl(baseDomain, 'sso');
81+
window.location.replace(url);
7182
}
7283
};
7384

74-
ssoLogin();
75-
}, [baseUrl]);
85+
if (loggedInUserId) {
86+
ssoLogin();
87+
}
88+
}, [baseUrl, loggedInUserId]);
7689

7790
return {
7891
accessToken,

src/providers/MatrixClientProvider.tsx

Lines changed: 54 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { ClientEvent, createClient, IndexedDBCryptoStore, IndexedDBStore, MatrixClient, SyncState } from 'matrix-js-sdk';
17+
import { ClientEvent, MatrixClient, SyncState } from 'matrix-js-sdk';
1818
import React, { useEffect, useState } from 'react';
1919
import MatrixClientContext from '../context/MatrixClientContext';
20-
import { cacheSecretStorageKey, cryptoCallbacks } from '../utils/secretStorageKeys';
20+
import { cacheSecretStorageKey } from '../utils/secretStorageKeys';
2121
import { CryptoApi, decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api';
22+
import defaultConfigClient from '../utils/client';
2223

2324

2425
interface Props {
@@ -55,28 +56,10 @@ const MatrixClientProvider = ({
5556
// Initialize the client
5657
if (!accessToken || !userId || !deviceId) return;
5758

58-
const indexedDBStore = new IndexedDBStore({
59-
indexedDB: global.indexedDB,
60-
localStorage: global.localStorage,
61-
dbName: 'web-sync-store',
62-
});
63-
64-
const client = createClient({
65-
baseUrl,
66-
accessToken,
67-
userId,
68-
store: indexedDBStore,
69-
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
70-
deviceId,
71-
timelineSupport: true,
72-
cryptoCallbacks,
73-
verificationMethods: [
74-
'm.sas.v1',
75-
],
76-
});
59+
const client = defaultConfigClient(baseUrl, accessToken, userId, deviceId);
7760

7861
(async () => {
79-
await indexedDBStore.startup();
62+
await client.store.startup();
8063

8164
if (enableCrypto) {
8265
// Setup e2ee
@@ -94,39 +77,21 @@ const MatrixClientProvider = ({
9477

9578
// Start syncing
9679
await client.startClient({ lazyLoadMembers: true });
97-
})();
98-
99-
const handleStateChange = (state: SyncState) => {
100-
// Make the client available after the first sync has completed
101-
if (state === SyncState.Prepared) {
102-
setMx(client);
103-
}
104-
};
10580

106-
client.on(ClientEvent.Sync, handleStateChange);
107-
return () => {
108-
client.removeListener(ClientEvent.Sync, handleStateChange);
109-
// Clean up matrix client on unmount
110-
client.stopClient();
111-
};
112-
}, [baseUrl, enableCrypto, rustCryptoStoreKeyFn, accessToken, userId, deviceId]);
113-
114-
useEffect(() => {
115-
// Add recovery key and enable
116-
if (enableKeyBackup && recoveryKeyFn && mx) {
117-
(async () => {
81+
// Activate Key Backup
82+
if (enableCrypto && enableKeyBackup && recoveryKeyFn) {
11883
const recoveryKey = await recoveryKeyFn?.();
11984

12085
// Validate the recovery key
12186
let recoveryKeyValid = false;
12287

12388
if (recoveryKey) {
12489
const decodedRecoveryKey = decodeRecoveryKey(recoveryKey);
125-
const keyId = await mx?.secretStorage.getDefaultKeyId();
126-
const secretStorageKeyTuple = await mx?.secretStorage.getKey(keyId);
90+
const keyId = await client.secretStorage.getDefaultKeyId();
91+
const secretStorageKeyTuple = await client.secretStorage.getKey(keyId);
12792
if (keyId && secretStorageKeyTuple) {
12893
const [, keyInfo] = secretStorageKeyTuple;
129-
recoveryKeyValid = await mx.secretStorage.checkKey(decodedRecoveryKey, keyInfo);
94+
recoveryKeyValid = await client.secretStorage.checkKey(decodedRecoveryKey, keyInfo);
13095

13196
if (recoveryKeyValid) {
13297
// cache the recovery key if it's valid
@@ -136,41 +101,63 @@ const MatrixClientProvider = ({
136101
}
137102

138103
if (recoveryKeyValid) {
139-
const hasKeyBackup = (await cryptoApi?.checkKeyBackupAndEnable()) !== null;
104+
const crypto = client.getCrypto();
105+
const hasKeyBackup = (await crypto?.checkKeyBackupAndEnable()) !== null;
140106
if (hasKeyBackup) {
141-
await cryptoApi?.loadSessionBackupPrivateKeyFromSecretStorage();
107+
await crypto?.loadSessionBackupPrivateKeyFromSecretStorage();
142108
}
143109
}
144-
})();
145-
}
146-
}, [enableKeyBackup, mx, cryptoApi, recoveryKeyFn]);
110+
}
147111

148-
useEffect(() => {
149-
if (enableCrossSigning) {
150-
(async () => {
112+
// Activate cross-signing
113+
if (enableCrypto && enableCrossSigning) {
151114
// Cache missing cross-signing keys locally, and setup cross-signing
152-
await cryptoApi?.userHasCrossSigningKeys(mx?.getUserId() || undefined, true);
153-
await cryptoApi?.bootstrapCrossSigning({});
154-
})();
155-
}
156-
}, [enableCrossSigning, mx, cryptoApi]);
115+
const crypto = client.getCrypto();
116+
await crypto?.userHasCrossSigningKeys(client.getUserId() || undefined, true);
117+
await crypto?.bootstrapCrossSigning({});
118+
}
157119

158-
useEffect(() => {
159-
if (enableDeviceDehydration) {
160-
(async () => {
120+
// Activate device dehydration
121+
if (enableCrypto && enableKeyBackup && enableCrossSigning && enableDeviceDehydration) {
161122
// If supported, rehydrate from existing device (if exists) and start regular device dehydration
162-
const dehydrationSupported = await cryptoApi?.isDehydrationSupported();
123+
const crypto = client.getCrypto();
124+
const dehydrationSupported = await crypto?.isDehydrationSupported();
163125
if (dehydrationSupported) {
164126
try {
165-
await cryptoApi?.startDehydration();
127+
await crypto?.startDehydration();
166128
} catch {
167129
// create new dehydration key if dehydration fails to start
168-
await cryptoApi?.startDehydration(true);
130+
await crypto?.startDehydration(true);
169131
}
170132
}
171-
})();
172-
}
173-
}, [enableDeviceDehydration, cryptoApi]);
133+
}
134+
})();
135+
136+
const handleStateChange = (state: SyncState) => {
137+
// Make the client available after the first sync has completed
138+
if (state === SyncState.Prepared) {
139+
setMx(client);
140+
}
141+
};
142+
143+
client.on(ClientEvent.Sync, handleStateChange);
144+
return () => {
145+
client.removeListener(ClientEvent.Sync, handleStateChange);
146+
// Clean up matrix client on unmount
147+
client.stopClient();
148+
};
149+
}, [
150+
baseUrl,
151+
enableCrypto,
152+
enableKeyBackup,
153+
enableCrossSigning,
154+
enableDeviceDehydration,
155+
recoveryKeyFn,
156+
rustCryptoStoreKeyFn,
157+
accessToken,
158+
userId,
159+
deviceId
160+
]);
174161

175162
return (
176163
<MatrixClientContext.Provider value={{

src/providers/SSOAuthMatrixClientProvider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import MatrixClientProvider from './MatrixClientProvider';
2020
interface Props {
2121
children: React.ReactNode;
2222
baseUrl: string;
23+
loggedInUserId?: string;
2324
enableCrypto?: boolean;
2425
enableKeyBackup?: boolean;
2526
enableCrossSigning?: boolean;
@@ -31,14 +32,15 @@ interface Props {
3132
const SSOAuthMatrixClientProvider = ({
3233
children,
3334
baseUrl,
35+
loggedInUserId,
3436
enableCrypto = false,
3537
enableKeyBackup = false,
3638
enableCrossSigning = false,
3739
enableDeviceDehydration = false,
3840
rustCryptoStoreKeyFn,
3941
recoveryKeyFn,
4042
}: Props) => {
41-
const { accessToken, userId, deviceId } = useSso(baseUrl);
43+
const { accessToken, userId, deviceId } = useSso(baseUrl, loggedInUserId);
4244

4345
return (
4446
<MatrixClientProvider

src/utils/client.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createClient, IndexedDBCryptoStore, IndexedDBStore } from 'matrix-js-sdk';
2+
import { cryptoCallbacks } from './secretStorageKeys';
3+
4+
5+
const defaultConfigClient = (
6+
baseUrl: string,
7+
accessToken?: string,
8+
userId?: string,
9+
deviceId?: string,
10+
) => {
11+
const indexedDBStore = new IndexedDBStore({
12+
indexedDB: global.indexedDB,
13+
localStorage: global.localStorage,
14+
dbName: 'web-sync-store',
15+
});
16+
17+
return createClient({
18+
baseUrl,
19+
accessToken,
20+
userId,
21+
store: indexedDBStore,
22+
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
23+
deviceId,
24+
timelineSupport: true,
25+
cryptoCallbacks,
26+
verificationMethods: [
27+
'm.sas.v1',
28+
],
29+
});
30+
};
31+
32+
33+
export default defaultConfigClient;

0 commit comments

Comments
 (0)