feat(react): Convert third party auth callback page to React

This commit is contained in:
Vijay Budhram 2024-11-20 11:46:49 -05:00
Родитель 13777a2ab2
Коммит 264c8ab528
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 9778545895B2532B
9 изменённых файлов: 154 добавлений и 98 удалений

Просмотреть файл

@ -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,

Просмотреть файл

@ -128,7 +128,7 @@ const getReactRouteGroups = (showReactApp, reactRoute) => {
'post_verify/third_party_auth/callback',
'post_verify/third_party_auth/set_password',
]),
fullProdRollout: false,
fullProdRollout: true,
},
webChannelExampleRoutes: {

Просмотреть файл

@ -65,6 +65,7 @@ export const Constants = {
// will be stripped.
OAUTH_UNTRUSTED_ALLOWED_PERMISSIONS: [
'openid',
'profile',
'profile:display_name',
'profile:email',
'profile:uid',

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -348,6 +348,7 @@ export class OAuthWebIntegration extends BaseIntegration {
}
// Ported from content server, search for _normalizeScopesAndPermissions
console.log('this.data.scope', this.data.scope);
let permissions = Array.from(scopeStrToArray(this.data.scope || ''));
if (this.isTrusted()) {
// We have to normalize `profile` into is expanded sub-scopes
@ -369,6 +370,7 @@ export class OAuthWebIntegration extends BaseIntegration {
});
}
console.log('permissions', permissions);
return permissions;
}

Просмотреть файл

@ -2,20 +2,38 @@
* 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 } 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 {
BaseIntegration,
IntegrationType,
useAccount,
useClientInfoState,
useProductInfoState,
} from '../../../models';
import {
StoredAccountData,
storeAccountData,
} from '../../../lib/storage-utils';
import { QueryParams } from '../../..';
import { queryParamsToMetricsContext } from '../../../lib/metrics';
import { handleNavigation } from '../../Signin/utils';
import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
import { useAuthClient } from '../../../models';
import {
DefaultIntegrationFlags,
IntegrationFactory,
} from '../../../lib/integrations';
import {
GenericData,
StorageData,
UrlQueryData,
} from '../../../lib/model-data';
import { ReachRouterWindow } from '../../../lib/window';
type LinkedAccountData = {
uid: hexstring;
@ -25,48 +43,73 @@ 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.
const initializeIntegration = (
originalParams: URLSearchParams,
clientInfoState: any,
productInfoState: any
) => {
const windowWrapper = new ReachRouterWindow();
const urlQueryData = new UrlQueryData(windowWrapper);
let integration = new BaseIntegration(IntegrationType.Web, urlQueryData);
// All use of params should be reworked to use `useValidatedQueryParams` hook in FXA-8834
try {
console.log('Initializing integration');
// The `state` param returned by third party auth provider contains the
// original FxA OAuth request params. We need to extract them and build
// a new integration based on that.
const state = decodeURIComponent(originalParams.get('state') || '');
const stateParams = new URLSearchParams(new URL(state).search);
// Update the URL query data with FxA OAuth request params
urlQueryData.setParams(stateParams);
const paramsObject = Object.fromEntries(stateParams.entries());
const data = new GenericData(paramsObject);
const flags = new DefaultIntegrationFlags(
urlQueryData,
new StorageData(windowWrapper)
);
const integrationFactory = new IntegrationFactory({
window: windowWrapper,
data,
flags,
clientInfo: clientInfoState.data?.clientInfo,
productInfo: productInfoState.data?.productInfo,
});
integration = integrationFactory.getIntegration();
console.log('Integration initialized', integration);
} catch (error) {
console.error('Error initializing integration', error);
}
return integration;
};
const ThirdPartyAuthCallback = ({
flowQueryParams,
}: { flowQueryParams?: QueryParams } & RouteComponentProps) => {
const navigate = useNavigate();
const account = useAccount();
const params = new URLSearchParams(window.location.search);
const authClient = useAuthClient();
const clientInfoState = useClientInfoState();
const productInfoState = useProductInfoState();
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 originalParams = new URLSearchParams(window.location.search);
const integration = initializeIntegration(
originalParams,
clientInfoState,
productInfoState
);
const { finishOAuthFlowHandler } = useFinishOAuthFlowHandler(
authClient,
integration
);
// 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(),
@ -74,51 +117,12 @@ const ThirdPartyAuthCallback = ({
verified: true,
metricsEnabled: true,
};
storeAccountData(accountData);
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)
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');
const code = originalParams.get('code');
const providerFromParams = originalParams.get('provider');
let provider: AUTH_PROVIDER | undefined;
if (providerFromParams === 'apple') {
provider = AUTH_PROVIDER.APPLE;
@ -129,28 +133,47 @@ const ThirdPartyAuthCallback = ({
return { code, provider };
};
async function verifyOAuthResponseAndSignIn() {
const { code, provider } = getAuthParams();
const navigateNext = async (linkedAccount: LinkedAccountData) => {
const navigationOptions = {
email: linkedAccount.email,
signinData: {
uid: linkedAccount.uid,
sessionToken: linkedAccount.sessionToken,
verified: true,
},
integration,
finishOAuthFlowHandler,
queryParams: location.search,
};
if (code && provider) {
const { error: navError } = await handleNavigation(navigationOptions, {
handleFxaLogin: false,
handleFxaOAuthLogin: true,
});
if (navError) {
console.log('navError', navError);
}
};
async function verifyOAuthResponseAndSignIn() {
const { code: thirdPartyOAuthCode, provider } = getAuthParams();
if (thirdPartyOAuthCode && 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,
thirdPartyOAuthCode,
provider,
undefined,
queryParamsToMetricsContext(
flowQueryParams as unknown as Record<string, string>
)
);
completeSignIn(linkedAccount);
await storeLinkedAccountData(linkedAccount);
await navigateNext(linkedAccount);
} catch (error) {
// TODO add error handling
console.log('error', error);
}
} else {
// TODO validate what should happen if we hit this page
@ -159,8 +182,12 @@ const ThirdPartyAuthCallback = ({
}
}
const isSigningIn = useRef(false);
useEffect(() => {
verifyOAuthResponseAndSignIn();
if (!isSigningIn.current) {
isSigningIn.current = true;
verifyOAuthResponseAndSignIn();
}
});
return (

Просмотреть файл

@ -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';
@ -320,6 +319,19 @@ const Signin = ({
]
);
// const isSigningIn = useRef(false);
// useEffect(() => {
// if (isSigningIn.current) {
// console.log("Skipping sign in");
// } else {
// isSigningIn.current = true;
// if (window.location.search.includes('thirdPartyAuthComplete') && sessionToken) {
// console.log("AUto sign in with cached account");
// signInWithCachedAccount(sessionToken);
// }
// }
// }, [signInWithCachedAccount, sessionToken]);
return (
<AppLayout>
{(localizedSuccessBannerHeading || localizedSuccessBannerDescription) && (