Skip to content

Commit 36c3b30

Browse files
authored
Merge pull request #20001 from mozilla/FXA-13009
fix(webchannel): Send 'can_link_account' during signin_unblock
2 parents 17341fb + ecab0df commit 36c3b30

4 files changed

Lines changed: 136 additions & 18 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,12 @@ const AuthAndAccountSetupRoutes = ({
638638
/>
639639
<SigninUnblockContainer
640640
path="/signin_unblock/*"
641-
{...{ integration, flowQueryParams, setCurrentSplitLayout }}
641+
{...{
642+
integration,
643+
flowQueryParams,
644+
setCurrentSplitLayout,
645+
useFxAStatusResult,
646+
}}
642647
/>
643648
<InlineRecoveryKeySetupContainer
644649
path="/inline_recovery_key_setup/*"

packages/fxa-settings/src/pages/Signin/SigninUnblock/container.test.tsx

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import {
2424
MOCK_UNBLOCK_CODE,
2525
MOCK_UNWRAP_BKEY,
2626
MOCK_UNWRAP_BKEY_V2,
27+
MOCK_CLIENT_ID,
28+
MOCK_REDIRECT_URI,
2729
mockLoadingSpinnerModule,
30+
MOCK_SERVICE,
2831
} from '../../mocks';
2932
import {
3033
mockGqlBeginSigninMutation,
@@ -45,6 +48,11 @@ import {
4548
} from './mocks';
4649
import { BeginSigninResult, SigninUnblockIntegration } from '../interfaces';
4750
import { tryFinalizeUpgrade } from '../../../lib/gql-key-stretch-upgrade';
51+
import { mockUseFxAStatus } from '../../../lib/hooks/useFxAStatus/mocks';
52+
import { ensureCanLinkAcountOrRedirect } from '../utils';
53+
import { IntegrationType, OAuthIntegrationData } from '../../../models';
54+
import { GenericData } from '../../../lib/model-data';
55+
import { Constants } from '../../../lib/constants';
4856

4957
let integration: SigninUnblockIntegration;
5058
function mockWebIntegration() {
@@ -55,6 +63,28 @@ function mockWebIntegration() {
5563
};
5664
}
5765

66+
function mockOAuthNativeIntegration() {
67+
integration = {
68+
...createMockSigninWebSyncIntegration(),
69+
type: IntegrationType.OAuthNative,
70+
data: new OAuthIntegrationData(
71+
new GenericData({
72+
context: Constants.OAUTH_WEBCHANNEL_CONTEXT,
73+
client_id: MOCK_CLIENT_ID,
74+
state: 'mock_state',
75+
})
76+
),
77+
clientInfo: {
78+
clientId: MOCK_CLIENT_ID,
79+
redirectUri: MOCK_REDIRECT_URI,
80+
imageUri: undefined,
81+
serviceName: MOCK_SERVICE,
82+
trusted: true,
83+
},
84+
isFirefoxMobileClient: () => false,
85+
} as SigninUnblockIntegration;
86+
}
87+
5888
let flowQueryParams: QueryParams;
5989
function mockQueryFlowParameters() {
6090
flowQueryParams = {
@@ -78,6 +108,11 @@ jest.mock('../../../lib/gql-key-stretch-upgrade', () => {
78108
};
79109
});
80110

111+
jest.mock('../utils', () => ({
112+
...jest.requireActual('../utils'),
113+
ensureCanLinkAcountOrRedirect: jest.fn(),
114+
}));
115+
81116
jest.mock('../../../models', () => {
82117
return {
83118
...jest.requireActual('../../../models'),
@@ -139,7 +174,13 @@ describe('signin unblock container', () => {
139174
});
140175

141176
/** Renders the container with a fake page component */
142-
async function render(mocks: Array<MockedResponse>) {
177+
async function render(
178+
mocks: Array<MockedResponse>,
179+
options?: { useFxAStatusResult?: ReturnType<typeof mockUseFxAStatus> }
180+
) {
181+
const useFxAStatusResult =
182+
options?.useFxAStatusResult || mockUseFxAStatus();
183+
143184
renderWithLocalizationProvider(
144185
<MockedProvider mocks={mocks} addTypename={false}>
145186
<LocationProvider>
@@ -148,6 +189,7 @@ describe('signin unblock container', () => {
148189
integration,
149190
serviceName: MozServices.Default,
150191
flowQueryParams,
192+
useFxAStatusResult,
151193
}}
152194
/>
153195
</LocationProvider>
@@ -185,6 +227,8 @@ describe('signin unblock container', () => {
185227
expect(result?.data?.signIn?.emailVerified).toBeDefined();
186228
expect(result?.data?.signIn?.sessionVerified).toBeDefined();
187229
expect(result?.data?.signIn?.metricsEnabled).toBeDefined();
230+
// Should NOT call ensureCanLinkAcountOrRedirect for non-OAuthNative integration
231+
expect(ensureCanLinkAcountOrRedirect).not.toHaveBeenCalled();
188232
});
189233

190234
it('handles signin with with key stretching upgrade', async () => {
@@ -295,4 +339,53 @@ describe('signin unblock container', () => {
295339
expect(result?.error).toBeDefined();
296340
expect(result?.error?.errno).toEqual(127);
297341
});
342+
343+
describe('with supportsCanLinkAccountUid capability and OAuthNative integration', () => {
344+
beforeEach(() => {
345+
mockOAuthNativeIntegration();
346+
(ensureCanLinkAcountOrRedirect as jest.Mock).mockResolvedValue(true);
347+
});
348+
349+
afterEach(() => {
350+
(ensureCanLinkAcountOrRedirect as jest.Mock).mockRestore();
351+
});
352+
353+
it('calls ensureCanLinkAcountOrRedirect with UID after successful signin', async () => {
354+
const useFxAStatusResult = mockUseFxAStatus({
355+
supportsCanLinkAccountUid: true,
356+
});
357+
358+
await render(
359+
[
360+
mockGqlCredentialStatusMutation(),
361+
mockGqlBeginSigninMutation(
362+
{
363+
unblockCode: MOCK_UNBLOCK_CODE,
364+
keys: true,
365+
},
366+
{
367+
authPW: MOCK_AUTH_PW_V2,
368+
}
369+
),
370+
],
371+
{ useFxAStatusResult }
372+
);
373+
374+
let result: BeginSigninResult | undefined;
375+
await act(async () => {
376+
result =
377+
await currentPageProps?.signinWithUnblockCode(MOCK_UNBLOCK_CODE);
378+
});
379+
380+
expect(result).toBeDefined();
381+
expect(result?.data).toBeDefined();
382+
expect(ensureCanLinkAcountOrRedirect).toHaveBeenCalledTimes(1);
383+
expect(ensureCanLinkAcountOrRedirect).toHaveBeenCalledWith(
384+
expect.objectContaining({
385+
email: MOCK_EMAIL,
386+
uid: expect.any(String),
387+
})
388+
);
389+
});
390+
});
298391
});

packages/fxa-settings/src/pages/Signin/SigninUnblock/container.tsx

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { RouteComponentProps, useLocation } from '@reach/router';
77

88
import VerificationMethods from '../../../constants/verification-methods';
99
import {
10+
isOAuthNativeIntegration,
1011
useAuthClient,
1112
useFtlMsgResolver,
1213
useSensitiveDataClient,
1314
} from '../../../models';
15+
import { UseFxAStatusResult } from '../../../lib/hooks/useFxAStatus';
1416

1517
// using default signin handlers
1618
import {
@@ -51,15 +53,18 @@ import { isFirefoxService } from '../../../models/integrations/utils';
5153
import { tryFinalizeUpgrade } from '../../../lib/gql-key-stretch-upgrade';
5254
import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery';
5355
import AppLayout from '../../../components/AppLayout';
56+
import { ensureCanLinkAcountOrRedirect } from '../utils';
5457

5558
export const SigninUnblockContainer = ({
5659
integration,
5760
flowQueryParams,
5861
setCurrentSplitLayout,
62+
useFxAStatusResult,
5963
}: {
6064
integration: SigninUnblockIntegration;
6165
flowQueryParams: QueryParams;
6266
setCurrentSplitLayout?: (value: boolean) => void;
67+
useFxAStatusResult: UseFxAStatusResult;
6368
} & RouteComponentProps) => {
6469
const authClient = useAuthClient();
6570
const ftlMsgResolver = useFtlMsgResolver();
@@ -165,22 +170,37 @@ export const SigninUnblockContainer = ({
165170
unwrapBKey: credentials.unwrapBKey,
166171
keyFetchToken: response.data.signIn.keyFetchToken,
167172
});
168-
}
169173

170-
const emailVerified = response.data?.signIn.emailVerified;
171-
const sessionVerified = response.data?.signIn.sessionVerified;
172-
const sessionToken = response.data?.signIn.sessionToken;
173-
// Attempt to finish key stretching upgrade now that session has been verified.
174-
if (emailVerified && sessionVerified && sessionToken) {
175-
await tryFinalizeUpgrade(
176-
sessionToken,
177-
sensitiveDataClient,
178-
'signin-unblock',
179-
credentialStatus,
180-
getWrappedKeys,
181-
passwordChangeStart,
182-
passwordChangeFinish
183-
);
174+
const emailVerified = response.data.signIn.emailVerified;
175+
const sessionVerified = response.data.signIn.sessionVerified;
176+
const sessionToken = response.data.signIn.sessionToken;
177+
// Attempt to finish key stretching upgrade now that session has been verified.
178+
if (emailVerified && sessionVerified && sessionToken) {
179+
await tryFinalizeUpgrade(
180+
sessionToken,
181+
sensitiveDataClient,
182+
'signin-unblock',
183+
credentialStatus,
184+
getWrappedKeys,
185+
passwordChangeStart,
186+
passwordChangeFinish
187+
);
188+
}
189+
190+
if (
191+
isOAuthNativeIntegration(integration) &&
192+
useFxAStatusResult.supportsCanLinkAccountUid
193+
) {
194+
const ok = await ensureCanLinkAcountOrRedirect({
195+
email,
196+
uid: response.data.signIn.uid,
197+
ftlMsgResolver,
198+
navigateWithQuery,
199+
});
200+
if (!ok) {
201+
return { data: undefined };
202+
}
203+
}
184204
}
185205

186206
return response;

packages/fxa-settings/src/pages/Signin/container.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ const SigninContainer = ({
312312
const beginSigninHandler: BeginSigninHandler = useCallback(
313313
async (email: string, password: string) => {
314314
// - If the user came from email-first WITHOUT a linked third‑party account, Index already showed
315-
// the merge prompt.
315+
// the merge prompt for old firefox versions (supportsCanLinkAccountUid=false).
316316
// - If the user HAS a linked third‑party account, Index deferred the prompt to avoid duplicates,
317317
// so we must prompt here instead.
318318
// Note: the browser will repond {ok} if the email matches stored data or the user accepts the merge.

0 commit comments

Comments
 (0)