Skip to content

Commit 2b04bd0

Browse files
authored
Merge pull request #20098 from mozilla/worktree-FXA-13160
bug(settings): Abandoning sync sign flow can cause down stream problems
2 parents 84f34db + 62a7ce2 commit 2b04bd0

11 files changed

Lines changed: 102 additions & 11 deletions

File tree

packages/functional-tests/lib/targets/base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type Credentials = Awaited<ReturnType<AuthClient['signUp']>> & {
1313
password: string;
1414
secret?: string;
1515
sessionToken?: string;
16+
verified?: boolean;
1617
};
1718

1819
export abstract class BaseTarget {

packages/functional-tests/lib/targets/local.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class LocalTarget extends BaseTarget {
4242
return {
4343
email,
4444
password,
45+
verified: options.preVerified === 'true',
4546
...result,
4647
} as Credentials;
4748
}

packages/functional-tests/lib/targets/remote.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export abstract class RemoteTarget extends BaseTarget {
3333
return {
3434
email,
3535
password,
36+
verified: preVerified === 'true',
3637
...creds,
3738
};
3839
}

packages/functional-tests/tests/oauth/syncSignIn.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,43 @@ test.describe('severity-1 #smoke', () => {
5959
await expect(signin.cachedSigninHeading).toBeVisible();
6060
await expect(page.getByText(email)).toBeVisible();
6161
});
62+
63+
test('can sign in to OAuth after abandoning sync confirmation code', async ({
64+
target,
65+
syncBrowserPages: { page, signin, signinTokenCode, relier },
66+
testAccountTracker,
67+
}) => {
68+
const syncCredentials = await testAccountTracker.signUpSync();
69+
70+
// Start sign-into-sync flow
71+
await page.goto(
72+
`${target.contentServerUrl}?context=fx_desktop_v3&service=sync&action=email&`
73+
);
74+
await signin.fillOutEmailFirstForm(syncCredentials.email);
75+
await signin.fillOutPasswordForm(syncCredentials.password);
76+
77+
// Confirm we reached the token code page, but intentionally skip entering the code
78+
await expect(page).toHaveURL(/signin_token_code/);
79+
80+
// Navigate to 123done without completing sync verification
81+
await relier.goto();
82+
await relier.clickEmailFirst();
83+
84+
// FxA sees the existing session and shows cached account
85+
await expect(signin.cachedSigninHeading).toBeVisible();
86+
await signin.signInButton.click();
87+
88+
// We get a signin code, because we are using a restmail address, and forces
89+
// verification. ie Must verify will always be set on this client.
90+
await expect(page).toHaveURL(/signin_token_code/);
91+
const signinCode = await target.emailClient.getVerifyLoginCode(
92+
syncCredentials.email
93+
);
94+
await signinTokenCode.fillOutCodeForm(signinCode);
95+
96+
// OAuth sign-in should succeed even though the sync session was not verified
97+
expect(await relier.isLoggedIn()).toBe(true);
98+
});
6299
});
63100

64101
test.describe('signin to Sync after OAuth', () => {

packages/fxa-dev-launcher/profile.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const CONFIGS = {
4646
},
4747
};
4848

49+
const isNightly = /Nightly/gi.test(process.env.FIREFOX_BIN || '');
4950
const env = process.env.FXA_ENV || 'local';
5051
const FXA_DESKTOP_CONTEXT =
5152
process.env.FXA_DESKTOP_CONTEXT || 'oauth_webchannel_v1';
@@ -65,7 +66,14 @@ if (!fxaEnv) {
6566
};
6667
}
6768

69+
const nightlyProfile = isNightly
70+
? {
71+
'browser.smartwindow.enabled': true,
72+
}
73+
: {};
74+
6875
const fxaProfile = {
76+
...nightlyProfile,
6977
// enable debugger and toolbox
7078
'devtools.chrome.enabled': true,
7179
'devtools.debugger.remote-enabled': true,

packages/fxa-settings/src/components/App/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,13 @@ export const App = ({
221221
integration.data?.service || ''
222222
);
223223

224-
if (userFromBrowser?.sessionToken) {
224+
// Don't intialize session state from partially succesful firefox logins (ie sync, relay, smartwindow).
225+
// This reprensents an abandonded login. Basically the user hasn't actually confirmed the session yet, so
226+
// don't assume it's valid.
227+
if (
228+
userFromBrowser?.sessionToken &&
229+
userFromBrowser.verified === true
230+
) {
225231
const isValidSession = await session.isValid(
226232
userFromBrowser.sessionToken
227233
);

packages/fxa-settings/src/lib/cache.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,15 @@ export function discardSessionToken() {
134134
const account = currentAccount();
135135
if (account) {
136136
account.sessionToken = undefined;
137+
account.verified = undefined;
138+
account.sessionVerified = undefined;
139+
account.hasPassword = undefined;
140+
137141
currentAccount(account);
142+
143+
// If there was a state change, dispatch an event
144+
dispatchStorageEvent('accounts');
145+
dispatchStorageEvent('currentAccountUid');
138146
}
139147
} catch (e) {
140148
// noop

packages/fxa-settings/src/lib/hooks/useOAuthFlowRecovery/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../../../models';
1111
import firefox from '../../channels/firefox';
1212
import { hardNavigate } from 'fxa-react/lib/utils';
13+
import { discardSessionToken } from '../../cache';
1314

1415
export type OAuthFlowRecoveryResult = {
1516
isRecovering: boolean;
@@ -67,9 +68,16 @@ export function useOAuthFlowRecovery(
6768
currentParams.set('code_challenge', params.code_challenge);
6869
}
6970
if (params.code_challenge_method) {
70-
currentParams.set('code_challenge_method', params.code_challenge_method);
71+
currentParams.set(
72+
'code_challenge_method',
73+
params.code_challenge_method
74+
);
7175
}
7276

77+
// Discard the current session token. We need to do this, otherwise the cached session will be used
78+
// and the UI will show an option to enter a password. This can lead to an infinite sign in loop.
79+
discardSessionToken();
80+
7381
// Redirect to /signin - user will re-enter password
7482
hardNavigate(`/signin?${currentParams.toString()}`);
7583

packages/fxa-settings/src/lib/oauth/hooks.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ export function useFinishOAuthFlowHandler(
231231
} catch (error) {
232232
// We only care about these errors, else just tell the user to try again.
233233
if (
234+
// Signals that session requires verification, ie a mustVerify state
235+
error.errno === AuthUiErrors.UNVERIFIED_SESSION.errno ||
236+
// Signals that session requires totp verification
234237
error.errno === AuthUiErrors.TOTP_REQUIRED.errno ||
238+
// Signals that we are dealing with an RP that requires a second factor
235239
error.errno === AuthUiErrors.INSUFFICIENT_ACR_VALUES.errno
236240
) {
237241
return { error };

packages/fxa-settings/src/models/Session.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,10 @@ export class Session implements SessionData {
131131

132132
async isValid(token: string): Promise<boolean> {
133133
try {
134-
await this.authClient.sessionStatus(token);
135-
setSessionVerified(true);
136-
return true;
134+
const resp = await this.authClient.sessionStatus(token);
135+
const isVerified = resp.state === 'verified';
136+
setSessionVerified(isVerified);
137+
return isVerified;
137138
} catch (e) {
138139
setSessionVerified(false);
139140
return false;

0 commit comments

Comments
 (0)