Since we're using TypeScript, it's crucial to define appropriate data types to
open-auth-payload.ts
exportclassOAuthPayload { email:string; firstName:string; lastName:string; picture?:string; accessToken:string; refreshToken?:string;}// Using outer layer type to access user data without using <any>exportclassPassportResponse { user:OAuthPayload;// body: oAuth code// url: called url// statusCode// statusMassage}
Strategy Class
FacebookStrategy extends PassportStrategy from Passport.js and implements the Facebook OAuth login flow.
Constructor:
Initializes the Facebook strategy with required options such as clientID, clientSecret, and callbackURL.
Logs an error if Facebook credentials (FACEBOOK_APP_ID and FACEBOOK_APP_SECRET) are not provided in constants.
Options:
clientID: Facebook App ID.
clientSecret: Facebook App Secret.
callbackURL: URL that Facebook redirects to after successful authentication.
scope: Scope of permissions; here, it requests access to the user's email.
profileFields: Specifies the fields retrieved from the Facebook profile (e.g., emails, name, picture).
validate Method
This method processes the Facebook profile data to create an OAuthPayload object and pass it to the done callback.
Parameters:
accessToken & refreshToken: Tokens returned by Facebook for accessing user information.
profile: Facebook profile object containing user information.
done: Callback to complete the validation process.
Data Mapping:
Maps profile fields to the OAuthPayload fields:
email: Primary email of the user.
firstName: User's first name.
lastName: User's last name.
picture: Profile picture URL.
Error Handling:
If an error occurs, handleError() is invoked, returning a BAD_REQUEST HTTP error with a user-friendly message.
import { HttpException, HttpStatus, Injectable } from'@nestjs/common';import { PassportStrategy } from'@nestjs/passport';import { Profile, Strategy } from'passport-facebook';import constants from'../../constants';import { OAuthPayload, PassportResponse } from'../dto/open-auth-payload';consthandleError= () => {thrownewHttpException( { message:'Authentication Error', description:"Sorry, we couldn't sign you in at the moment. Please try again later.", },HttpStatus.BAD_REQUEST );};@Injectable()exportclassFacebookStrategyextendsPassportStrategy(Strategy,'facebook') {constructor() {constclientID=constants.FACEBOOK_APP_ID;constclientSecret=constants.FACEBOOK_APP_SECRET;if (!clientID ||!clientSecret) {console.error('[ERROR] Please set up FACEBOOK_APP_ID and FACEBOOK_APP_SECRET'); }super({ clientID: clientID, clientSecret: clientSecret, callbackURL:`${constants.BASE_URL}/auth/callback`, scope:'email', profileFields: ['emails','name','picture'], }); }asyncvalidate( accessToken:string, refreshToken:string, profile:Profile,done: (err:Error, user:OAuthPayload) =>void ):Promise<PassportResponse> {try {const { name,emails,photos } = profile;constuser:OAuthPayload= { email: emails[0].value, firstName:name.givenName, lastName:name.familyName, picture: photos[0].value, accessToken, refreshToken, };// Passing OAuthPayload into user field of a responsedone(null, user); } catch (error) {done(handleError(),null);returnPromise.reject(error); } }}
After creating strategy we need to register it within a module
The component renders a Button styled for Facebook sign-in. When clicked, it begins the Facebook login process, handling responses and redirections based on authentication status.
State and Hooks:
useState for loading: Indicates when the login request is being processed.
useAuth, useAuthService, and useNavigate: Contexts and services for handling authentication state and navigation.
useEffect and useCallback manage the popup's event listeners and message handling.
URL Construction (getCode function):
Constructs the Facebook OAuth URL using the client ID and redirect URI.
Initiates the OAuth flow by calling openSignInWindow.
Popup Handling (openSignInWindow and receiveMessage functions):
openSignInWindow creates the popup for Facebook’s OAuth and sets up a message listener.
receiveMessage processes the OAuth response:
Checks the event origin for security.
Parses the code parameter returned by Facebook.
Calls the facebookAuth function with the code to retrieve the user’s tokens.
Authentication and Navigation Logic:
After receiving the tokens, the accessToken, refreshToken, and isTwoFactorEnable are evaluated:
If 2FA is enabled, redirects to the 2FA setup route.
Otherwise, stores tokens and navigates to the root path.
Error Handling:
Displays an error notification if there’s an issue during authentication.
Switches the active tab to the sign-up screen if necessary.
import { useCallback, useEffect, useState } from'react';import { useNavigate } from'react-router-dom';import { Button } from'antd';import { useAuthService } from'../../services/authService';import { useAuth } from'../../context/AuthContext';import { routesPath } from'../../common/constants/routesPath';import { notificationType } from'../../common/constants/notificationType';import { constants } from'../../common/constants/constants';import { textColor, spacing, textStandard } from'../../theming';import facebook from'../../assets/Facebook.png';constFacebookSignInButton= ({ authType, setActiveTabKey, openNotification, t }) => {let windowObjectReference =null;let previousUrl =null;let path =authType.toLowerCase();useEffect(() => {return () => {window.removeEventListener('message', receiveMessage); }; }, []);const [loading,setLoading] =useState(false);const { setAuth } =useAuth();constnavigate=useNavigate();const { facebookAuth } =useAuthService();functionencodeQueryData(data) {constret= [];for (let d in data) {ret.push(encodeURIComponent(d) +'='+encodeURIComponent(data[d])); }returnret.join('&'); }constgetCode= () => {constqueryData= { scope:'email', response_type:'code', redirect_uri:`${constants.BASE_URL}/auth/callback`, client_id:constants.FACEBOOK.CLIENT_ID, };constqueryString=encodeQueryData(queryData);consturl=`https://www.facebook.com/v19.0/dialog/oauth?${queryString}`;openSignInWindow(url,'facebook-redirect'); };constopenSignInWindow= (url, name) => {// Remove any existing event listenerswindow.removeEventListener('message', receiveMessage,false);// Window featuresconststrWindowFeatures='toolbar=no, menubar=no, width=600, height=700, top=100, left=100';if (windowObjectReference ===null||windowObjectReference.closed) {/* If the pointer to the window object in memory does not exist or if such pointer exists but the window was closed */ windowObjectReference =window.open(url, name, strWindowFeatures); } elseif (previousUrl !== url) {/* If the resource to load is different, then we load it in the already opened secondary window, and then we bring such window back on top/in front of its parent window. */ windowObjectReference =window.open(url, name, strWindowFeatures);windowObjectReference.focus(); } else {/* Else the window reference must exist and the window is not closed; therefore, we can bring it back on top of any other window with the focus() method. There would be no need to re-create the window or to reload the referenced resource. */windowObjectReference.focus(); }// Add the listener for receiving a message from the popupwindow.addEventListener('message', receiveMessage,false);// Assign the previous URL previousUrl = url; };constreceiveMessage=useCallback(async event => {// Check if we could trust the event originif (event.origin !==constants.BASE_URL) {return; }const { data } = event;// Check if the source is our popupif (event.source.name ==='facebook-redirect') {window.removeEventListener('message', receiveMessage,false);setLoading(true);let params =newURLSearchParams(data);constcode=params.get('code');try {constuserTokens=awaitfacebookAuth(path, code);if (userTokens?.data) {const { accessToken,refreshToken,isTwoFactorEnable } =userTokens.data;if (isTwoFactorEnable) {setAuth(accessToken,null);navigate(`${routesPath.AUTH.PATH}/${routesPath.AUTH.TWO_FACTOR_AUTH}`); } else {setAuth(accessToken, refreshToken);navigate(routesPath.ROOT.PATH); } }setLoading(false); } catch (error) {openNotification({ type:notificationType.ERROR, message:error.response?.data?.message ??error.response.status, description:error.response?.data?.description ??error.response.statusText, });setLoading(false);setActiveTabKey(error.response.data?.redirectToSignUp ?'1':'0');navigate('/auth/sign-in'); }// Remove any existing event listeners to prevent multiple requestswindow.removeEventListener('message', receiveMessage,false); } }, [authType] );return ( <Buttonloading={loading}onClick={getCode}style={{ background:'inherit', width:'100%', color:textColor.secondaryAlt,...textStandard, fontWeight:500, }}icon={<imgsrc={facebook} alt="google_icon"style={{ marginRight: spacing[1] }} />}size="large" > {t('auth:buttons.facebook')} </Button> );};exportdefault FacebookSignInButton;
OAuth Callback
This component should be set as the callbackURL route in your app’s router configuration. After Facebook redirects to this route, the component completes the OAuth handshake by communicating the response back to the original window and closing itself
useEffect Hook:
When the component mounts, it retrieves the URL parameters (window.location.search), which includes the OAuth code sent by Facebook after the user authorizes the app.
Message Posting:
It checks if the current window has an opener (the original window that opened the popup).
If an opener exists, it sends the URL parameters back to the opener using window.opener.postMessage(params). The original window then handles these parameters (e.g., by extracting the authorization code).
Popup Closure:
After posting the message, window.close() is called to close the popup, finishing the OAuth flow.
import { useEffect } from'react';functionCallback() {useEffect(() => {// Get the URL parameters which will include the codeconstparams=window.location.search;if (window.opener) {// Send them to the opening windowwindow.opener.postMessage(params);// Close the popupwindow.close(); } }, []);return <p>Please wait...</p>;}exportdefault Callback;
Sequence Diagram
This OAuth flow diagram helps provide a clear visual representation of how authentication works between the UI, API, and OAuth provider.