Skip to content

Commit 630479e

Browse files
authored
Merge pull request #20750 from mozilla/FXA-13947
fix(mobile): Redirect passwordless users to set_password in OAuthNative flows when Sync is not decoupled
2 parents 79a575f + baf1823 commit 630479e

40 files changed

Lines changed: 805 additions & 70 deletions

File tree

packages/functional-tests/lib/query-params.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
// This file contains query params that don't reflect states that can be reached from 123done.
66
import uaStrings from './ua-strings';
77
import { FF_OAUTH_CLIENT_ID } from './channels';
8-
import { SYNC_SESSION_TOKEN_SCOPE, OLDSYNC_SCOPE, VPN_SCOPE } from './scopes';
8+
import {
9+
SYNC_SESSION_TOKEN_SCOPE,
10+
OLDSYNC_SCOPE,
11+
VPN_SCOPE,
12+
PROFILE_SCOPE,
13+
} from './scopes';
914

1015
export const oauthWebchannelV1 = new URLSearchParams({
1116
context: 'oauth_webchannel_v1',
@@ -87,6 +92,22 @@ export const vpnMobileOAuthQueryParams = new URLSearchParams({
8792
service: 'vpn',
8893
});
8994

95+
// When Sync is not decoupled on Android, signing up for VPN on Android means
96+
// signing up for Sync as well; all scopes are included in the request
97+
export const vpnSyncMobileOAuthFenixQueryParams = new URLSearchParams({
98+
...Object.fromEntries(oauthWebchannelV1.entries()),
99+
client_id: 'a2270f727f45f648', // Fenix (Android)
100+
code_challenge_method: 'S256',
101+
code_challenge: '2oc_C4v1qHeefWAGu5LI5oDG1oX4FV_Itc148D8_oQI',
102+
keys_jwk:
103+
'eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImdUejVIWFJfa2pxSFRtMG43ZjhxcDMybVZFaHZ1cGo1dXNUV1h5TWZsb1kiLCJ5IjoiVER5TlhkalhibHZld1pWLVc5MXNDZU9fRWd0NU9WYXhpblBzOEFTQ3owZyJ9',
104+
scope: `${PROFILE_SCOPE} ${OLDSYNC_SCOPE} ${VPN_SCOPE}`,
105+
state: 'fakestate',
106+
automatedBrowser: 'true',
107+
service: 'vpn',
108+
entrypoint: 'protection_panel',
109+
});
110+
90111
export const syncDesktopOAuthQueryParamsNoScope = new URLSearchParams({
91112
...Object.fromEntries(oauthWebchannelV1.entries()),
92113
client_id: FF_OAUTH_CLIENT_ID, // Firefox Desktop

packages/functional-tests/tests/misc/vpnIntegration.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { expect, test } from '../../lib/fixtures/standard';
77
import {
88
syncMobileOAuthFenixQueryParams,
99
vpnMobileOAuthQueryParams,
10+
vpnSyncMobileOAuthFenixQueryParams,
1011
} from '../../lib/query-params';
1112
import { PROFILE_SCOPE, VPN_SCOPE } from '../../lib/scopes';
1213

@@ -53,4 +54,30 @@ test.describe('vpn integration', () => {
5354
});
5455
}
5556
);
57+
58+
test('passwordless VPN + Sync signin on Android routes to set password when Sync is not decoupled', async ({
59+
target,
60+
syncOAuthBrowserPages: { page, signin, signinPasswordlessCode },
61+
testAccountTracker,
62+
}) => {
63+
const { email, password } = await testAccountTracker.signUpPasswordless();
64+
await signin.goto('/authorization', vpnSyncMobileOAuthFenixQueryParams);
65+
await signin.fillOutEmailFirstForm(email);
66+
67+
await page.waitForURL(/signin_passwordless_code/);
68+
const code = await target.emailClient.getPasswordlessSigninCode(email);
69+
await signinPasswordlessCode.fillOutCodeForm(code);
70+
71+
await page.waitForURL(/set_password/);
72+
await expect(
73+
page.getByRole('heading', { name: 'Create password to sync' })
74+
).toBeVisible();
75+
76+
await page.getByLabel('Password', { exact: true }).fill(password);
77+
await page.getByLabel('Repeat password').fill(password);
78+
await page.getByRole('button', { name: 'Start syncing' }).click();
79+
80+
await signin.checkWebChannelMessage(FirefoxCommand.OAuthLogin);
81+
await signin.checkWebChannelMessage(FirefoxCommand.Login);
82+
});
5683
});

packages/functional-tests/tests/passwordless/signinPasswordless.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,7 @@ test.describe('severity-2', () => {
13641364
},
13651365
testAccountTracker,
13661366
}) => {
1367+
test.fixme(true, 'TODO in FXA-13647');
13671368
// Create passwordless account and set up TOTP via API
13681369
const { email, sessionToken } =
13691370
await testAccountTracker.signUpPasswordless();

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,11 @@ const AuthAndAccountSetupRoutes = ({
585585
{/* Post verify */}
586586
<ThirdPartyAuthCallback
587587
path="/post_verify/third_party_auth/callback/*"
588-
{...{ flowQueryParams, integration }}
588+
{...{
589+
flowQueryParams,
590+
integration,
591+
useFxAStatusResult,
592+
}}
589593
/>
590594
<SetPasswordContainer
591595
path="/post_verify/set_password/*"
@@ -678,6 +682,7 @@ const AuthAndAccountSetupRoutes = ({
678682
flowQueryParams,
679683
isSignedIntoFirefox,
680684
setCurrentSplitLayout,
685+
useFxAStatusResult,
681686
}}
682687
/>
683688
<SigninPasswordlessCodeContainer
@@ -688,6 +693,7 @@ const AuthAndAccountSetupRoutes = ({
688693
flowQueryParams,
689694
isSignedIntoFirefox,
690695
setCurrentSplitLayout,
696+
useFxAStatusResult,
691697
}}
692698
/>
693699
<SigninContainer
@@ -741,7 +747,12 @@ const AuthAndAccountSetupRoutes = ({
741747
/>
742748
<SigninTotpCodeContainer
743749
path="/signin_totp_code/*"
744-
{...{ integration, serviceName, setCurrentSplitLayout }}
750+
{...{
751+
integration,
752+
serviceName,
753+
setCurrentSplitLayout,
754+
useFxAStatusResult,
755+
}}
745756
/>
746757
<SigninConfirmed
747758
path="/signin_verified/*"

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const FormSetupAccount = ({
1616
onSubmit,
1717
loading,
1818
isSync,
19+
requirePasswordConfirmation,
1920
offeredSyncEngineConfigs,
2021
submitButtonGleanId,
2122
passwordFormType = 'signup',
@@ -45,8 +46,8 @@ export const FormSetupAccount = ({
4546
submitButtonGleanId,
4647
passwordFormType,
4748
cmsButton,
49+
requirePasswordConfirmation,
4850
}}
49-
requirePasswordConfirmation={isSync}
5051
/>
5152
);
5253
};

packages/fxa-settings/src/components/FormSetupAccount/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type FormSetupAccountProps = {
2222
onSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>;
2323
loading: boolean;
2424
isSync: boolean;
25+
requirePasswordConfirmation?: boolean;
2526
offeredSyncEngineConfigs?: typeof syncEngineConfigs;
2627
submitButtonGleanId?: string;
2728
passwordFormType?: 'signup' | 'post-verify-set-password';

packages/fxa-settings/src/lib/hooks/useFxAStatus/index.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ describe('useFxAStatus', () => {
154154
const integration = {
155155
type: IntegrationType.Web,
156156
isSync: () => false,
157+
isFirefoxNonSync: () => false,
157158
};
158159

159160
renderHook(() => useFxAStatus(integration));

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
import firefox from '../../channels/firefox';
2020
import { Constants } from '../../constants';
2121

22-
type FxAStatusIntegration = Pick<Integration, 'type' | 'isSync'>;
22+
type FxAStatusIntegration = Pick<
23+
Integration,
24+
'type' | 'isSync' | 'isFirefoxNonSync'
25+
>;
2326

2427
/**
2528
* If integration.isSync or integration is OAuthNative, sends firefox.fxaStatus to retrieve

packages/fxa-settings/src/lib/hooks/useFxAStatus/mocks.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import { getSyncEngineIds, syncEngineConfigs } from '../../sync-engines';
6+
import type { UseFxAStatusResult } from '.';
67

78
export function mockUseFxAStatus({
89
offeredSyncEnginesOverride,
@@ -36,7 +37,7 @@ export function mockUseFxAStatus({
3637
selectedEnginesForGlean,
3738
supportsKeysOptionalLogin,
3839
supportsCanLinkAccountUid,
39-
};
40+
} satisfies UseFxAStatusResult;
4041
}
4142

4243
export default mockUseFxAStatus;

packages/fxa-settings/src/models/integrations/integration.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,38 @@ describe('Integration Model', function () {
4141
});
4242
});
4343

44+
describe('requiresPasswordForLogin', function () {
45+
it('is true when keys are required (Sync), regardless of keys-optional support', () => {
46+
jest.spyOn(model, 'requiresKeys').mockReturnValue(true);
47+
jest.spyOn(model, 'wantsKeysIfPasswordEntered').mockReturnValue(false);
48+
expect(model.requiresPasswordForLogin(true)).toBe(true);
49+
});
50+
51+
it('is true for a non-Sync client that wants keys when keys are not optional', () => {
52+
jest.spyOn(model, 'requiresKeys').mockReturnValue(false);
53+
jest.spyOn(model, 'wantsKeysIfPasswordEntered').mockReturnValue(true);
54+
expect(model.requiresPasswordForLogin(false)).toBe(true);
55+
});
56+
57+
it('is false for a non-Sync client that wants keys when the browser supports keys-optional login', () => {
58+
jest.spyOn(model, 'requiresKeys').mockReturnValue(false);
59+
jest.spyOn(model, 'wantsKeysIfPasswordEntered').mockReturnValue(true);
60+
expect(model.requiresPasswordForLogin(true)).toBe(false);
61+
});
62+
63+
it('is false when the client does not want keys', () => {
64+
jest.spyOn(model, 'requiresKeys').mockReturnValue(false);
65+
jest.spyOn(model, 'wantsKeysIfPasswordEntered').mockReturnValue(false);
66+
expect(model.requiresPasswordForLogin(false)).toBe(false);
67+
});
68+
69+
it('treats an omitted keys-optional flag as "not supported"', () => {
70+
jest.spyOn(model, 'requiresKeys').mockReturnValue(false);
71+
jest.spyOn(model, 'wantsKeysIfPasswordEntered').mockReturnValue(true);
72+
expect(model.requiresPasswordForLogin()).toBe(true);
73+
});
74+
});
75+
4476
describe('isTrusted', function () {
4577
it('returns `true`', () => {
4678
expect(model.isTrusted()).toBeTruthy();

0 commit comments

Comments
 (0)