зеркало из https://github.com/mozilla/fxa.git
feat(react): Convert third party auth callback page to React
This commit is contained in:
Родитель
13777a2ab2
Коммит
921186405a
|
@ -32,6 +32,7 @@ import SignInTotpCodeView from '../views/sign_in_totp_code';
|
|||
import SignInUnblockView from '../views/sign_in_unblock';
|
||||
import SignUpPasswordView from '../views/sign_up_password';
|
||||
import ThirdPartyAuthSetPasswordView from '../views/post_verify/third_party_auth/set_password';
|
||||
import ThirdPartyAuthCallbackView from '../views/post_verify/third_party_auth/callback';
|
||||
import Storage from './storage';
|
||||
import SubscriptionsProductRedirectView from '../views/subscriptions_product_redirect';
|
||||
import SubscriptionsManagementRedirectView from '../views/subscriptions_management_redirect';
|
||||
|
@ -376,9 +377,12 @@ Router = Router.extend({
|
|||
type: VerificationReasons.SECONDARY_EMAIL_VERIFIED,
|
||||
}
|
||||
),
|
||||
'post_verify/third_party_auth/callback(/)': createViewHandler(
|
||||
'post_verify/third_party_auth/callback'
|
||||
),
|
||||
'post_verify/third_party_auth/callback(/)': function () {
|
||||
this.createReactOrBackboneViewHandler(
|
||||
'post_verify/third_party_auth/callback',
|
||||
ThirdPartyAuthCallbackView
|
||||
);
|
||||
},
|
||||
'post_verify/third_party_auth/set_password(/)': function () {
|
||||
this.createReactOrBackboneViewHandler(
|
||||
'post_verify/third_party_auth/set_password',
|
||||
|
|
|
@ -62,7 +62,8 @@
|
|||
"resetPasswordRoutes": true,
|
||||
"signUpRoutes": true,
|
||||
"signInRoutes": true,
|
||||
"emailFirstRoutes": false
|
||||
"emailFirstRoutes": false,
|
||||
"postVerifyThirdPartyAuthRoutes": true
|
||||
},
|
||||
"featureFlags": {
|
||||
"sendFxAStatusOnSettings": true,
|
||||
|
|
|
@ -142,4 +142,30 @@ export class DefaultIntegrationFlags implements IntegrationFlags {
|
|||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
isThirdPartyAuthCallback() {
|
||||
if (!/third_party_auth\/callback/.test(this.pathname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = this.searchParam('state');
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedState = decodeURIComponent(state);
|
||||
// Maybe check for values in url?
|
||||
new URL(decodedState);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const code = this.searchParam('code');
|
||||
if (!code) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
RelierClientInfo,
|
||||
RelierSubscriptionInfo,
|
||||
OAuthIntegration,
|
||||
ThirdPartyAuthCallbackIntegration,
|
||||
} from '../../models/integrations';
|
||||
import {
|
||||
ModelDataStore,
|
||||
|
@ -103,7 +104,9 @@ export class IntegrationFactory {
|
|||
const flags = this.flags;
|
||||
|
||||
// The order of checks matters
|
||||
if (flags.isDevicePairingAsAuthority()) {
|
||||
if (flags.isThirdPartyAuthCallback()) {
|
||||
return this.createThirdPartyAuthCallbackIntegration(data);
|
||||
} else if (flags.isDevicePairingAsAuthority()) {
|
||||
return this.createPairingAuthorityIntegration(channelData, storageData);
|
||||
} else if (flags.isDevicePairingAsSupplicant()) {
|
||||
return this.createPairingSupplicationIntegration(data, storageData);
|
||||
|
@ -123,6 +126,12 @@ export class IntegrationFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private createThirdPartyAuthCallbackIntegration(data: ModelDataStore) {
|
||||
const integration = new ThirdPartyAuthCallbackIntegration(data);
|
||||
this.initIntegration(integration);
|
||||
return integration;
|
||||
}
|
||||
|
||||
private createPairingAuthorityIntegration(
|
||||
data: ModelDataStore,
|
||||
storageData: ModelDataStore
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface IntegrationFlags {
|
|||
isV3DesktopContext(): boolean;
|
||||
isOAuthSuccessFlow(): { status: boolean; clientId: string };
|
||||
isOAuthVerificationFlow(): boolean;
|
||||
isThirdPartyAuthCallback(): boolean;
|
||||
|
||||
isServiceOAuth(): boolean;
|
||||
isServiceSync(): boolean;
|
||||
|
|
|
@ -30,7 +30,7 @@ export class UrlQueryData extends UrlData {
|
|||
* Sets a new internal state from a set of URL search params
|
||||
* @param params
|
||||
*/
|
||||
protected setParams(params: URLSearchParams) {
|
||||
public setParams(params: URLSearchParams) {
|
||||
// Immediately update the internal state
|
||||
this.internalState = params;
|
||||
|
||||
|
|
|
@ -933,6 +933,15 @@ export class Account implements AccountData {
|
|||
metricsContext
|
||||
)
|
||||
);
|
||||
|
||||
currentAccount(getStoredAccountData(linkedAccount));
|
||||
sessionToken(linkedAccount.sessionToken);
|
||||
|
||||
this.apolloClient.cache.writeQuery({
|
||||
query: GET_LOCAL_SIGNED_IN_STATUS,
|
||||
data: { isSignedIn: true },
|
||||
});
|
||||
|
||||
return linkedAccount;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ export enum IntegrationType {
|
|||
SyncBasic = 'SyncBasic',
|
||||
SyncDesktopV3 = 'SyncDesktopV3',
|
||||
Web = 'Web', // default
|
||||
ThirdPartyAuthCallback = 'ThirdPartyAuthCallback', // For third party auth callbacks
|
||||
}
|
||||
|
||||
/* TODO, do we care about this feature (capability in content-server)?
|
||||
|
@ -183,6 +184,10 @@ export abstract class Integration<
|
|||
isTrusted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
thirdPartyAuthParams() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseIntegration<
|
||||
|
|
|
@ -14,3 +14,4 @@ export * from './supplicant-info';
|
|||
export * from './sync-basic-integration';
|
||||
export * from './sync-desktop-v3-integration';
|
||||
export * from './web-integration';
|
||||
export * from './third-party-auth-callback-integration';
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { ModelDataStore, GenericData } from '../../lib/model-data';
|
||||
import { ThirdPartyAuthCallbackIntegration } from './third-party-auth-callback-integration';
|
||||
import { AUTH_PROVIDER } from 'fxa-auth-client/browser';
|
||||
|
||||
describe('models/integrations/third-party-auth-callback-integration', function () {
|
||||
let data: ModelDataStore;
|
||||
let model: ThirdPartyAuthCallbackIntegration;
|
||||
|
||||
beforeEach(function () {
|
||||
data = new GenericData({});
|
||||
data.set('code', 'test-code');
|
||||
data.set('provider', 'apple');
|
||||
const state = encodeURIComponent('https://example.com?param=value');
|
||||
data.set('state', state);
|
||||
model = new ThirdPartyAuthCallbackIntegration(data);
|
||||
});
|
||||
|
||||
it('exists', () => {
|
||||
expect(model).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return third party auth params', () => {
|
||||
const params = model.thirdPartyAuthParams();
|
||||
expect(params).toEqual({
|
||||
code: 'test-code',
|
||||
provider: AUTH_PROVIDER.APPLE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return FxA params from state', () => {
|
||||
const fxaParams = model.getFxAParams();
|
||||
expect(fxaParams).toBe('?param=value');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import {
|
||||
BaseIntegration,
|
||||
Integration,
|
||||
IntegrationFeatures,
|
||||
IntegrationType,
|
||||
} from './base-integration';
|
||||
import { bind, ModelDataStore } from '../../lib/model-data';
|
||||
import { AUTH_PROVIDER } from 'fxa-auth-client/browser';
|
||||
|
||||
import { BaseIntegrationData } from './web-integration';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export function isThirdPartyAuthCallbackIntegration(
|
||||
integration: null | Integration<IntegrationFeatures>
|
||||
): integration is ThirdPartyAuthCallbackIntegration {
|
||||
if (!integration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return integration.type === IntegrationType.ThirdPartyAuthCallback;
|
||||
}
|
||||
|
||||
export class ThirdPartyAuthCallbackIntegrationData extends BaseIntegrationData {
|
||||
@IsString()
|
||||
@bind()
|
||||
state: string | undefined;
|
||||
|
||||
@IsString()
|
||||
@bind()
|
||||
code: string | undefined;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@bind()
|
||||
provider: string | undefined;
|
||||
}
|
||||
|
||||
export interface ThirdPartyAuthCallbackIntegrationFeatures
|
||||
extends IntegrationFeatures {}
|
||||
|
||||
export class ThirdPartyAuthCallbackIntegration extends BaseIntegration<ThirdPartyAuthCallbackIntegrationFeatures> {
|
||||
constructor(data: ModelDataStore) {
|
||||
super(
|
||||
IntegrationType.ThirdPartyAuthCallback,
|
||||
new ThirdPartyAuthCallbackIntegrationData(data)
|
||||
);
|
||||
}
|
||||
|
||||
thirdPartyAuthParams() {
|
||||
const code = this.data.code;
|
||||
const providerFromParams = this.data.provider;
|
||||
let provider: AUTH_PROVIDER | undefined;
|
||||
if (providerFromParams === 'apple') {
|
||||
provider = AUTH_PROVIDER.APPLE;
|
||||
} else {
|
||||
provider = AUTH_PROVIDER.GOOGLE;
|
||||
}
|
||||
|
||||
return { code, provider };
|
||||
}
|
||||
|
||||
getFxAParams() {
|
||||
const state = this.data.state;
|
||||
if (state) {
|
||||
const decodedState = decodeURIComponent(state);
|
||||
const url = new URL(decodedState);
|
||||
return url.search;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -3,29 +3,126 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
|
||||
import ThirdPartyAuthCallback from '.';
|
||||
import { AppContext } from '../../../models';
|
||||
import { mockAppContext } from '../../../models/mocks';
|
||||
import { LocationProvider } from '@reach/router';
|
||||
import { ThirdPartyAuthProps } from '../../../components/ThirdPartyAuth';
|
||||
import { createAppContext, mockAppContext } from '../../../models/mocks';
|
||||
import { useAccount, useIntegration } from '../../../models';
|
||||
import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
|
||||
import { handleNavigation } from '../../Signin/utils';
|
||||
import { isThirdPartyAuthCallbackIntegration } from '../../../models/integrations/third-party-auth-callback-integration';
|
||||
import { QueryParams } from '../../../index';
|
||||
|
||||
function renderWith(props?: ThirdPartyAuthProps) {
|
||||
jest.mock('../../../models', () => ({
|
||||
...jest.requireActual('../../../models'),
|
||||
useClientInfoState: jest.fn(),
|
||||
useProductInfoState: jest.fn(),
|
||||
useIntegration: jest.fn(),
|
||||
useAccount: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@reach/router', () => ({
|
||||
...jest.requireActual('@reach/router'),
|
||||
useLocation: () => {
|
||||
return {
|
||||
search: '?',
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/oauth/hooks', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
useFinishOAuthFlowHandler: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../Signin/utils', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
handleNavigation: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../../models/integrations/third-party-auth-callback-integration',
|
||||
() => {
|
||||
return {
|
||||
__esModule: true,
|
||||
isThirdPartyAuthCallbackIntegration: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
function renderWith(props?: { flowQueryParams?: QueryParams }) {
|
||||
return renderWithLocalizationProvider(
|
||||
<AppContext.Provider value={mockAppContext()}>
|
||||
<LocationProvider>
|
||||
<ThirdPartyAuthCallback {...props} />;
|
||||
</LocationProvider>
|
||||
<AppContext.Provider value={{ ...mockAppContext(), ...createAppContext() }}>
|
||||
<ThirdPartyAuthCallback {...props} />;
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('ThirdPartyAuth component', () => {
|
||||
it('renders as expected', () => {
|
||||
renderWith();
|
||||
describe('ThirdPartyAuthCallback component', () => {
|
||||
beforeEach(() => {
|
||||
(useFinishOAuthFlowHandler as jest.Mock).mockImplementation(() => ({
|
||||
finishOAuthFlowHandler: jest.fn(),
|
||||
oAuthDataError: null,
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders as expected', async () => {
|
||||
renderWith({});
|
||||
screen.getByText(
|
||||
'Please wait, you are being redirected to the authorized application.'
|
||||
);
|
||||
});
|
||||
|
||||
it('verifies third-party auth response and navigates', async () => {
|
||||
const mockVerifyAccountThirdParty = jest.fn().mockResolvedValue({
|
||||
uid: 'uid',
|
||||
sessionToken: 'sessionToken',
|
||||
providerUid: 'providerUid',
|
||||
email: 'email@example.com',
|
||||
});
|
||||
const mockAccount = {
|
||||
verifyAccountThirdParty: mockVerifyAccountThirdParty,
|
||||
};
|
||||
(useAccount as jest.Mock).mockReturnValue(mockAccount);
|
||||
|
||||
const mockIntegration = {
|
||||
thirdPartyAuthParams: () => ({ code: 'code', provider: 'provider' }),
|
||||
getFxAParams: () => 'param=value',
|
||||
};
|
||||
(useIntegration as jest.Mock).mockReturnValue(mockIntegration);
|
||||
|
||||
(
|
||||
isThirdPartyAuthCallbackIntegration as unknown as jest.Mock
|
||||
).mockReturnValue(true);
|
||||
|
||||
const mockFinishOAuthFlowHandler = jest.fn();
|
||||
(useFinishOAuthFlowHandler as jest.Mock).mockReturnValue({
|
||||
finishOAuthFlowHandler: mockFinishOAuthFlowHandler,
|
||||
});
|
||||
|
||||
const mockHandleNavigation = jest.fn().mockResolvedValue({ error: null });
|
||||
(handleNavigation as jest.Mock).mockReturnValue(mockHandleNavigation);
|
||||
|
||||
renderWith({
|
||||
flowQueryParams: {},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifyAccountThirdParty).toHaveBeenCalledWith(
|
||||
'code',
|
||||
'provider',
|
||||
undefined,
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,20 +2,31 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
|
||||
import { RouteComponentProps, useLocation } from '@reach/router';
|
||||
import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
|
||||
import AppLayout from '../../../components/AppLayout';
|
||||
import { AUTH_PROVIDER } from 'fxa-auth-client/browser';
|
||||
import { useAccount } from '../../../models';
|
||||
import {
|
||||
useAccount,
|
||||
useIntegration,
|
||||
useAuthClient,
|
||||
Integration,
|
||||
} from '../../../models';
|
||||
import { handleNavigation } from '../../Signin/utils';
|
||||
import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
|
||||
import {
|
||||
StoredAccountData,
|
||||
storeAccountData,
|
||||
} from '../../../lib/storage-utils';
|
||||
import { QueryParams } from '../../..';
|
||||
import { queryParamsToMetricsContext } from '../../../lib/metrics';
|
||||
import {
|
||||
isThirdPartyAuthCallbackIntegration,
|
||||
ThirdPartyAuthCallbackIntegration,
|
||||
} from '../../../models/integrations/third-party-auth-callback-integration';
|
||||
import { ReachRouterWindow } from '../../../lib/window';
|
||||
import { UrlQueryData } from '../../../lib/model-data';
|
||||
|
||||
type LinkedAccountData = {
|
||||
uid: hexstring;
|
||||
|
@ -25,143 +36,134 @@ type LinkedAccountData = {
|
|||
verificationMethod?: string;
|
||||
};
|
||||
|
||||
// TODO this page to be completed/activated in FXA-8834
|
||||
// User reaches this page when redirected back from third party auth provider.
|
||||
// It requires /post_verify/third_party_auth/callback route to be turned on for react
|
||||
// otherwise, users authenticating with the react version of signin/signup are directed
|
||||
// to the backbone version of the callback to complete their third party authentication.
|
||||
|
||||
// All use of params should be reworked to use `useValidatedQueryParams` hook in FXA-8834
|
||||
|
||||
const ThirdPartyAuthCallback = ({
|
||||
flowQueryParams,
|
||||
}: { flowQueryParams?: QueryParams } & RouteComponentProps) => {
|
||||
const navigate = useNavigate();
|
||||
const account = useAccount();
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const integration = useIntegration();
|
||||
const authClient = useAuthClient();
|
||||
const location = useLocation();
|
||||
|
||||
const getRedirectUrl = () => {
|
||||
// get the stashed state with origin information
|
||||
// use it to reconstruct redirect for oauth
|
||||
// the state is the entire URL of the origin and includes the redirect_uri in a param
|
||||
// if authenticating from a RP
|
||||
const state = params.get('state');
|
||||
if (state) {
|
||||
// we may need to deconstruct the state to access/modify the redirect URL
|
||||
const stateParams = new URL(decodeURIComponent(state)).searchParams;
|
||||
const redirect = stateParams.get('redirect_uri');
|
||||
// if the state contains a redirect_uri, we need to redirect to RP
|
||||
// otherwise we redirect internally
|
||||
if (redirect) {
|
||||
const url = new URL(redirect);
|
||||
// TODO append other params from state to the redirect URL
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const linkedAccountData = useRef({} as LinkedAccountData);
|
||||
|
||||
// Persist account data to local storage to match parity with content-server
|
||||
// this allows the recent account to be used for /signin
|
||||
const storeLinkedAccountData = async (linkedAccount: LinkedAccountData) => {
|
||||
const accountData: StoredAccountData = {
|
||||
// We are using the email that was returned from the Third Party Auth
|
||||
// Not the email entered in the email-first form as they might be different
|
||||
email: linkedAccount.email,
|
||||
uid: linkedAccount.uid,
|
||||
lastLogin: Date.now(),
|
||||
sessionToken: linkedAccount.sessionToken,
|
||||
verified: true,
|
||||
metricsEnabled: true,
|
||||
};
|
||||
const { finishOAuthFlowHandler } = useFinishOAuthFlowHandler(
|
||||
authClient,
|
||||
integration || ({} as Integration)
|
||||
);
|
||||
|
||||
storeAccountData(accountData);
|
||||
};
|
||||
const storeLinkedAccountData = useCallback(
|
||||
async (linkedAccount: LinkedAccountData) => {
|
||||
const accountData: StoredAccountData = {
|
||||
email: linkedAccount.email,
|
||||
uid: linkedAccount.uid,
|
||||
lastLogin: Date.now(),
|
||||
sessionToken: linkedAccount.sessionToken,
|
||||
verified: true,
|
||||
metricsEnabled: true,
|
||||
};
|
||||
return storeAccountData(accountData);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const completeSignIn = async (linkedAccount: LinkedAccountData) => {
|
||||
// TODO in FXA-8834, use SignIn method that should be ported in FXA-6488
|
||||
// to complete sign in with the sessionToken obtained when verifying the third party auth
|
||||
// this should also update graphQL cache (isSignedIn:true)
|
||||
// await account.signIn(linkedAccount)
|
||||
const verifyThirdPartyAuthResponse = useCallback(async () => {
|
||||
const { code: thirdPartyOAuthCode, provider } = (
|
||||
integration as ThirdPartyAuthCallbackIntegration
|
||||
).thirdPartyAuthParams();
|
||||
|
||||
await storeLinkedAccountData(linkedAccount);
|
||||
|
||||
// TODO ensure correct redirects for all integrations (OAuth, Desktop, Mobile)
|
||||
// redirect is constructed from state param in the URL params
|
||||
const redirectURL = getRedirectUrl();
|
||||
if (redirectURL) {
|
||||
// get the stashed state with origin information
|
||||
// use it to reconstruct redirect for oauth
|
||||
// the state is the entire URL of the origin and includes the redirect_uri in a param
|
||||
// if authenticating from a RP
|
||||
const state = params.get('state');
|
||||
if (state) {
|
||||
// we may need to deconstruct the state to access/modify the redirect URL
|
||||
const stateParams = new URL(decodeURIComponent(state)).searchParams;
|
||||
const redirect = stateParams.get('redirect_uri');
|
||||
// if the state contains a redirect_uri, we need to redirect to RP
|
||||
// otherwise we redirect internally
|
||||
if (redirect) {
|
||||
hardNavigate(redirect);
|
||||
} else {
|
||||
// general redirect to settings for non-RP
|
||||
// currently, redirect to /settings fails with an "unauthenticated" error from GQL
|
||||
// and redirects to /signin (on backbone) where ThirdPArty Auth successfully
|
||||
// navigates to /settings
|
||||
navigate('/settings');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// auth params are received from the third party auth provider
|
||||
// and are required to verify the account
|
||||
const getAuthParams = () => {
|
||||
const code = params.get('code');
|
||||
const providerFromParams = params.get('provider');
|
||||
let provider: AUTH_PROVIDER | undefined;
|
||||
if (providerFromParams === 'apple') {
|
||||
provider = AUTH_PROVIDER.APPLE;
|
||||
} else {
|
||||
provider = AUTH_PROVIDER.GOOGLE;
|
||||
if (!thirdPartyOAuthCode) {
|
||||
return hardNavigate('/');
|
||||
}
|
||||
|
||||
return { code, provider };
|
||||
};
|
||||
try {
|
||||
const linkedAccount: LinkedAccountData =
|
||||
await account.verifyAccountThirdParty(
|
||||
thirdPartyOAuthCode,
|
||||
provider,
|
||||
undefined,
|
||||
queryParamsToMetricsContext(
|
||||
flowQueryParams as unknown as Record<string, string>
|
||||
)
|
||||
);
|
||||
await storeLinkedAccountData(linkedAccount);
|
||||
|
||||
async function verifyOAuthResponseAndSignIn() {
|
||||
const { code, provider } = getAuthParams();
|
||||
linkedAccountData.current = linkedAccount;
|
||||
|
||||
if (code && provider) {
|
||||
try {
|
||||
// Verify and link the third party account to FxA. Note, this
|
||||
// will create a new FxA account if one does not exist.
|
||||
// The response contains a session token that can be used
|
||||
// to sign the user in to FxA or to complete an Oauth flow.
|
||||
const linkedAccount: LinkedAccountData =
|
||||
await account.verifyAccountThirdParty(
|
||||
code,
|
||||
provider,
|
||||
undefined,
|
||||
queryParamsToMetricsContext(
|
||||
flowQueryParams as unknown as Record<string, string>
|
||||
)
|
||||
);
|
||||
const fxaParams = (
|
||||
integration as ThirdPartyAuthCallbackIntegration
|
||||
).getFxAParams();
|
||||
|
||||
completeSignIn(linkedAccount);
|
||||
} catch (error) {
|
||||
// TODO add error handling
|
||||
}
|
||||
} else {
|
||||
// TODO validate what should happen if we hit this page
|
||||
// without the required auth params to verify the account
|
||||
// HACK: Force the query params to be set in the URL, which then loads
|
||||
// the integration stored in ThirdPartyAuthCallbackIntegration `state` value.
|
||||
const urlQueryData = new UrlQueryData(new ReachRouterWindow());
|
||||
urlQueryData.setParams(new URLSearchParams(fxaParams));
|
||||
} catch (error) {
|
||||
// TODO validate what should happen here
|
||||
hardNavigate('/');
|
||||
}
|
||||
}
|
||||
}, [account, flowQueryParams, integration, storeLinkedAccountData]);
|
||||
|
||||
/**
|
||||
* Navigate to the next page
|
||||
if Sync based integration -> navigate to set password or sign-in
|
||||
if OAuth based integration -> verify OAuth and navigate to RP
|
||||
if neither -> navigate to settings
|
||||
*/
|
||||
const performNavigation = useCallback(
|
||||
async (linkedAccount: LinkedAccountData) => {
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navigationOptions = {
|
||||
email: linkedAccount.email,
|
||||
signinData: {
|
||||
uid: linkedAccount.uid,
|
||||
sessionToken: linkedAccount.sessionToken,
|
||||
verified: true,
|
||||
},
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
queryParams: location.search,
|
||||
};
|
||||
|
||||
const { error: navError } = await handleNavigation(navigationOptions, {
|
||||
handleFxaLogin: false,
|
||||
handleFxaOAuthLogin: false,
|
||||
});
|
||||
|
||||
if (navError) {
|
||||
// TODO validate what should happen here
|
||||
hardNavigate('/');
|
||||
}
|
||||
},
|
||||
[finishOAuthFlowHandler, integration, location.search]
|
||||
);
|
||||
|
||||
const navigateNext = useCallback(
|
||||
async (linkedAccount: LinkedAccountData) => {
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
performNavigation(linkedAccount);
|
||||
},
|
||||
[integration, performNavigation]
|
||||
);
|
||||
|
||||
// Ensure we only attempt to verify third party auth creds once
|
||||
useEffect(() => {
|
||||
verifyOAuthResponseAndSignIn();
|
||||
});
|
||||
if (isThirdPartyAuthCallbackIntegration(integration)) {
|
||||
verifyThirdPartyAuthResponse();
|
||||
}
|
||||
}, [integration, verifyThirdPartyAuthResponse]);
|
||||
|
||||
// Once we have verified the third party auth, navigate to the next page
|
||||
useEffect(() => {
|
||||
if (integration && linkedAccountData.current.sessionToken) {
|
||||
navigateNext(linkedAccountData.current);
|
||||
}
|
||||
}, [integration, navigateNext]);
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
|
|
|
@ -2,13 +2,12 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { Link, RouteComponentProps, useLocation } from '@reach/router';
|
||||
import { useNavigateWithQuery as useNavigate } from '../../lib/hooks/useNavigateWithQuery';
|
||||
import classNames from 'classnames';
|
||||
import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
|
||||
import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import AppLayout from '../../components/AppLayout';
|
||||
import CardHeader from '../../components/CardHeader';
|
||||
|
|
Загрузка…
Ссылка в новой задаче