Skip to content

Commit 261c266

Browse files
ggazzoclaude
andcommitted
fix(sdk): retry login through Meteor instead of clearing creds on auth-error
The 500ms-deferred cleanup wasn't enough to handle e2ee-passphrase-management: the test's loginByUserState fires _pollStoredLoginToken with the same token already in localStorage, so Meteor's poller bails (cached token == current). By the time the wrap's setTimeout fires, the test has already injected the SAME token (mongo $addToSet re-added it server-side after unsetLoginTokens), but the wrap was clearing creds anyway, leaving the page stuck on /login with no follow-up login firing. Replace the unconditional clear with a Meteor.loginWithToken retry against whatever's in localStorage right now. If the token was rotated (or re-added to mongo concurrently), the retry succeeds; if it's truly stale (real force-logout, no concurrent recovery), Meteor's callback invokes forceClientLoggedOut to drive the user to /login as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 76c79d1 commit 261c266

1 file changed

Lines changed: 45 additions & 18 deletions

File tree

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

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ export const ensureConnectedAndAuthenticated = async (): Promise<void> => {
135135
}
136136
};
137137

138+
const forceClientLoggedOut = (sdk: DDPSDK): void => {
139+
try {
140+
Accounts._unstoreLoginToken();
141+
} catch {
142+
// ignore
143+
}
144+
try {
145+
(Meteor.connection as unknown as { setUserId: (uid: string | null) => void }).setUserId(null);
146+
} catch {
147+
// ignore
148+
}
149+
sdk.account.uid = undefined;
150+
sdk.account.user = undefined;
151+
};
152+
138153
const isAuthError = (error: unknown): boolean => {
139154
if (!error || typeof error !== 'object') return false;
140155
const e = error as { error?: unknown; reason?: unknown };
@@ -229,31 +244,43 @@ if (typeof window !== 'undefined') {
229244
return await originalLogin(token);
230245
} catch (error) {
231246
if (!isAuthError(error)) throw error;
232-
// Defer the cleanup so a concurrent fresh login (e.g. SAML's
233-
// post-redirect resume, e2ee-passphrase-management's
234-
// loginByUserState/_pollStoredLoginToken, password login from
235-
// the form) has a chance to rotate the stored token and SDK
236-
// account state. After the delay, only clear creds if the
237-
// state is still stuck on the same token+uid we tried with —
238-
// meaning no concurrent flow rescued it. For real
239-
// force-logout (no concurrent login), nothing else will
240-
// touch localStorage and we end up clearing on schedule.
247+
// Defer the recovery so a concurrent fresh login (e.g. SAML's
248+
// post-redirect resume, password login from the form) has a chance
249+
// to rotate the stored token and SDK account state. After the
250+
// delay, if state is still stuck on the same token+uid we tried
251+
// with, retry the login through Meteor with whatever's currently
252+
// in localStorage. Two outcomes:
253+
// - if localStorage was rotated by a concurrent flow (or a test's
254+
// loginByUserState re-added the same token to mongo via
255+
// $addToSet AFTER the server's unsetLoginTokens), the retry
256+
// hits Meteor's normal success path and setUserId fires;
257+
// - if the token is still genuinely stale (real force-logout, no
258+
// concurrent recovery), Meteor's login callback receives the
259+
// auth error and we call makeClientLoggedOut to drive the user
260+
// to /login.
261+
// This avoids the race where _pollStoredLoginToken short-circuits
262+
// because the stored token didn't visibly change, but the
263+
// underlying mongo token was wiped+re-added.
241264
setTimeout(() => {
242265
const stillSameStored = readStoredLoginToken() === token;
243266
const accountStillReflectsThisToken = sdk.account.uid === triedWithUid;
244267
if (!stillSameStored || !accountStillReflectsThisToken) return;
245-
try {
246-
Accounts._unstoreLoginToken();
247-
} catch {
248-
// ignore
268+
const currentToken = readStoredLoginToken();
269+
if (!currentToken) {
270+
forceClientLoggedOut(sdk);
271+
return;
249272
}
250273
try {
251-
(Meteor.connection as unknown as { setUserId: (uid: string | null) => void }).setUserId(null);
252-
} catch {
253-
// ignore
274+
(Meteor as unknown as { loginWithToken: (token: string, cb: (err: unknown) => void) => void }).loginWithToken(
275+
currentToken,
276+
(err) => {
277+
if (err) forceClientLoggedOut(sdk);
278+
},
279+
);
280+
} catch (e) {
281+
console.warn('[ddpSdk] retry Meteor.loginWithToken failed', e);
282+
forceClientLoggedOut(sdk);
254283
}
255-
sdk.account.uid = undefined;
256-
sdk.account.user = undefined;
257284
}, 500);
258285
throw error;
259286
}

0 commit comments

Comments
 (0)