Skip to content

Commit a940e3f

Browse files
committed
fix(sdk): re-introduce loginWithToken wrap with stricter guards
Re-add the auto-relogin auth-error handler that fixes e2ee-key-reset and device-management force-logout flows, with two extra guards to avoid the SAML regression that the previous version caused: - readStoredLoginToken() === token: nothing rotated the stored token mid-flight (a concurrent SAML/password/OAuth login already wrote a fresh one) - sdk.account.uid === triedWithUid: the SDK account didn't get refreshed by a successful adopt while we were awaiting (a parallel Meteor-routed login completed and updated the in-memory state) If both guards hold, the only plausible explanation is a true server force-logout (Users.unsetLoginTokens), so we drop local credentials and let the router fall back to Login. Verified locally: login.spec, account-login, e2ee-key-reset and omnichannel-rooms-forward all pass.
1 parent 2e721a2 commit a940e3f

1 file changed

Lines changed: 44 additions & 0 deletions

File tree

apps/meteor/client/lib/sdk/ddpSdk.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,50 @@ if (typeof window !== 'undefined') {
212212
const sdk = getDdpSdk();
213213
window.__rocketChatSdk = sdk;
214214

215+
// DDPSDK auto-fires loginWithToken on every `connected` event using the
216+
// in-memory account.user.token (DDPSDK.create line 115-122). When the
217+
// server force-logs the user out (resetUserE2EKey →
218+
// Users.unsetLoginTokens → meteor.service force_logout listener closes
219+
// the user's WebSocket sessions), the SDK reconnects and immediately
220+
// retries the now-dead token. DDPSDK calls this with `void` so the
221+
// rejection is swallowed; account.user stays populated, Meteor.userId()
222+
// stays set, and the navbar continues to render Home with stale creds.
223+
//
224+
// Wrap account.loginWithToken so we can observe rejections from the
225+
// auto-retry. To avoid breaking the SAML/password login flows where a
226+
// fresh login is concurrently in flight, only act when:
227+
// - the error is auth-shaped (`isAuthError`) AND
228+
// - the token in localStorage still matches the one we tried with
229+
// (nothing rotated it mid-flight) AND
230+
// - the SDK account didn't get refreshed by a successful adopt while
231+
// we were awaiting (sdk.account.uid still maps to this token's user)
232+
const account = sdk.account as unknown as { loginWithToken: (token: string) => Promise<unknown> };
233+
const originalLogin = account.loginWithToken.bind(sdk.account);
234+
account.loginWithToken = async (token: string) => {
235+
const triedWithUid = sdk.account.uid;
236+
try {
237+
return await originalLogin(token);
238+
} catch (error) {
239+
const stillSameStored = readStoredLoginToken() === token;
240+
const accountStillReflectsThisToken = sdk.account.uid === triedWithUid;
241+
if (isAuthError(error) && stillSameStored && accountStillReflectsThisToken) {
242+
try {
243+
Accounts._unstoreLoginToken();
244+
} catch {
245+
// ignore
246+
}
247+
try {
248+
(Meteor.connection as unknown as { setUserId: (uid: string | null) => void }).setUserId(null);
249+
} catch {
250+
// ignore
251+
}
252+
sdk.account.uid = undefined;
253+
sdk.account.user = undefined;
254+
}
255+
throw error;
256+
}
257+
};
258+
215259
// Boot-time auth is now driven by Meteor's login resume routed through
216260
// stubMeteorStream, which calls adoptAccountFromMeteorLoginResult on
217261
// success. Calling ensureConnectedAndAuthenticated here as well would

0 commit comments

Comments
 (0)