Back to Blog
MSAL React Authentication: From Setup to Backend Integration

MSAL React Authentication: From Setup to Backend Integration

January 4, 2026
Stefan Mentovic
reactauthenticationazuremsalsecurity

Master Microsoft authentication in React SPAs with MSAL. Learn when to use it, how to configure it, and three proven patterns for backend API integration.

#MSAL React Authentication: From Setup to Backend Integration

Your React app needs enterprise authentication. Users expect single sign-on with their corporate Microsoft accounts. The security team demands proper token handling. And somehow, your backend API needs to trust that the person clicking buttons is actually who they claim to be.

This is where MSAL (Microsoft Authentication Library) becomes your best friend — or your worst nightmare, depending on how you approach it.

In this guide, we'll cut through the complexity. You'll learn when MSAL React is the right choice, how to set it up properly, and — most importantly — three battle-tested patterns for integrating authentication with your backend API.

#When Should You Use MSAL React?

MSAL React is the right choice when:

  • Your users authenticate with Microsoft Entra ID (formerly Azure AD)
  • You're building a Single-Page Application (SPA) without a backend-for-frontend
  • You need access to Microsoft Graph or other Azure-protected APIs
  • Enterprise SSO is a requirement — users sign in once across multiple apps
  • You need fine-grained control over token acquisition and caching

Consider alternatives when:

  • You have a dedicated backend that can handle OAuth flows (use server-side auth instead)
  • You're building a mobile app (use @azure/msal-react-native or native SDKs)
  • Your identity provider isn't Microsoft-based (use Auth0, Clerk, or generic OIDC libraries)
  • You need server-side rendering with authentication (AuthJS or similar)

Key insight: MSAL React uses the OAuth 2.0 Authorization Code Flow with PKCE, which is the most secure option for public clients like SPAs. No client secrets are exposed in the browser.

#Architecture Overview

Before diving into code, understand the authentication flow:

MSAL authentication flow diagram showing the complete sequence from user sign-in through token acquisition to backend API callsMSAL authentication flow diagram showing the complete sequence from user sign-in through token acquisition to backend API calls

  1. User clicks "Sign In" — MSAL redirects to Microsoft login page
  2. User authenticates (password, MFA, SSO)
  3. Microsoft redirects back with authorization code
  4. MSAL exchanges code for tokens (ID + Access)
  5. Tokens cached in browser (sessionStorage/localStorage)
  6. App calls backend API with access token
  7. Backend validates token and processes request

The critical question is step 7: How does your backend validate the token? We'll explore three patterns later.

#Setting Up MSAL React

#Step 1: Register Your Application

Before writing any code, register your app in the Microsoft Entra admin center:

  1. Navigate to Identity → Applications → App registrations

  2. Click New registration

  3. Configure:

    • Name: Your app name
    • Supported account types: Choose based on your needs
    • Redirect URI: Select "Single-page application" and add http://localhost:5173 (Vite default)
  4. Note your Application (client) ID and Directory (tenant) ID

  5. Under API permissions, add any Microsoft Graph permissions your app needs

Production tip: Register separate apps for development and production. Use different redirect URIs for each environment.

#Step 2: Install Dependencies

npm install @azure/msal-react @azure/msal-browser

The @azure/msal-react package is a wrapper around @azure/msal-browser, providing React-specific hooks and components.

#Step 3: Configure MSAL

Create a configuration file that defines your authentication settings:

// src/lib/auth/msal-config.ts
import { Configuration, LogLevel } from '@azure/msal-browser';

export const msalConfig: Configuration = {
	auth: {
		clientId: import.meta.env.VITE_AZURE_CLIENT_ID,
		authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AZURE_TENANT_ID}`,
		redirectUri: import.meta.env.VITE_REDIRECT_URI || window.location.origin,
		postLogoutRedirectUri: window.location.origin,
		navigateToLoginRequestUrl: true,
	},
	cache: {
		// sessionStorage is more secure (cleared on tab close)
		// localStorage persists across sessions
		cacheLocation: 'sessionStorage',
		storeAuthStateInCookie: false, // Set true for IE11 support
	},
	system: {
		loggerOptions: {
			logLevel: import.meta.env.DEV ? LogLevel.Verbose : LogLevel.Error,
			piiLoggingEnabled: false,
			loggerCallback: (level, message, containsPii) => {
				if (containsPii) return;
				switch (level) {
					case LogLevel.Error:
						console.error(message);
						break;
					case LogLevel.Warning:
						console.warn(message);
						break;
					case LogLevel.Info:
						console.info(message);
						break;
					case LogLevel.Verbose:
						console.debug(message);
						break;
				}
			},
		},
	},
};

// Scopes for ID token (user profile info)
export const loginRequest = {
	scopes: ['openid', 'profile', 'email'],
};

// Scopes for your backend API
export const apiRequest = {
	scopes: [`api://${import.meta.env.VITE_AZURE_CLIENT_ID}/access_as_user`],
};

// Scopes for Microsoft Graph (if needed)
export const graphRequest = {
	scopes: ['User.Read'],
};

Security note: The cacheLocation choice matters. Use sessionStorage for sensitive applications — tokens are cleared when the browser tab closes. Use localStorage only when persistent sessions are explicitly required.

#Step 4: Initialize the MSAL Provider

Wrap your application with MsalProvider at the root level:

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { PublicClientApplication, EventType } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
import { msalConfig } from '@/lib/auth/msal-config';
import App from './App';

// Create MSAL instance ONCE and reuse throughout the app
const msalInstance = new PublicClientApplication(msalConfig);

// Handle redirect promise on page load
msalInstance.initialize().then(() => {
	// Account selection logic for multi-account scenarios
	const accounts = msalInstance.getAllAccounts();
	if (accounts.length > 0) {
		msalInstance.setActiveAccount(accounts[0]);
	}

	// Listen for sign-in events
	msalInstance.addEventCallback((event) => {
		if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
			const account = (event.payload as { account: (typeof accounts)[0] }).account;
			msalInstance.setActiveAccount(account);
		}
	});

	ReactDOM.createRoot(document.getElementById('root')!).render(
		<React.StrictMode>
			<MsalProvider instance={msalInstance}>
				<App />
			</MsalProvider>
		</React.StrictMode>,
	);
});

Critical: Instantiate PublicClientApplication only once per application. Multiple instances cause cache conflicts and race conditions. This is explicitly called out in the MSAL documentation.

#Step 5: Create Authentication Components

Build reusable components for login/logout:

// src/components/auth/sign-in-button.tsx
import { useMsal } from '@azure/msal-react';
import { loginRequest } from '@/lib/auth/msal-config';
import { Button } from '@/components/ui/button';

export function SignInButton() {
	const { instance } = useMsal();

	const handleLogin = () => {
		// Redirect flow (recommended for most cases)
		instance.loginRedirect(loginRequest);

		// Alternative: Popup flow (better UX but blocked by some browsers)
		// instance.loginPopup(loginRequest);
	};

	return <Button onClick={handleLogin}>Sign in with Microsoft</Button>;
}
// src/components/auth/sign-out-button.tsx
import { useMsal } from '@azure/msal-react';
import { Button } from '@/components/ui/button';

export function SignOutButton() {
	const { instance } = useMsal();

	const handleLogout = () => {
		instance.logoutRedirect({
			postLogoutRedirectUri: window.location.origin,
		});
	};

	return (
		<Button variant='outline' onClick={handleLogout}>
			Sign out
		</Button>
	);
}

#Step 6: Protect Routes and Components

MSAL React provides template components for conditional rendering:

// src/components/auth/auth-guard.tsx
import { AuthenticatedTemplate, UnauthenticatedTemplate } from '@azure/msal-react';
import { SignInButton } from './sign-in-button';

interface AuthGuardProps {
	children: React.ReactNode;
	fallback?: React.ReactNode;
}

export function AuthGuard({ children, fallback }: AuthGuardProps) {
	return (
		<>
			<AuthenticatedTemplate>{children}</AuthenticatedTemplate>
			<UnauthenticatedTemplate>
				{fallback || (
					<div className='flex flex-col items-center justify-center min-h-screen gap-4'>
						<h1 className='text-2xl font-bold'>Welcome</h1>
						<p className='text-muted-foreground'>Please sign in to continue</p>
						<SignInButton />
					</div>
				)}
			</UnauthenticatedTemplate>
		</>
	);
}

For programmatic access to authentication state, use the hooks:

// src/hooks/use-auth.ts
import { useMsal, useIsAuthenticated } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';

export function useAuth() {
	const { instance, accounts, inProgress } = useMsal();
	const isAuthenticated = useIsAuthenticated();

	const activeAccount = instance.getActiveAccount();

	return {
		isAuthenticated,
		isLoading: inProgress !== InteractionStatus.None,
		user: activeAccount
			? {
					id: activeAccount.localAccountId,
					name: activeAccount.name,
					email: activeAccount.username,
					tenantId: activeAccount.tenantId,
			  }
			: null,
		accounts,
		instance,
	};
}

#Backend Integration Strategies

Here's where most tutorials fall short. Getting a user signed in is the easy part. The real challenge is securely connecting your authenticated frontend to your backend API.

Three patterns emerge, each with distinct tradeoffs:

#Pattern 1: Direct Token Validation (Stateless)

The frontend acquires an access token for your API and sends it with every request. The backend validates the JWT signature and claims without storing session state.

How it works:

  1. Frontend calls acquireTokenSilent to get an access token (from cache or refreshed)
  2. Frontend sends the API request with Authorization: Bearer <token> header
  3. Backend extracts the token and fetches Azure AD's public signing keys (JWKS) — cached after first request
  4. Backend verifies the JWT signature using the public keys
  5. Backend validates claims: audience (aud), issuer (iss), expiration (exp)
  6. If valid, request proceeds with user identity extracted from token claims

Performance note: The JWKS fetch happens once and is cached. Subsequent validations only perform cryptographic signature verification — typically sub-millisecond.

JWT validation flow showing token verification steps from signature check through claims validationJWT validation flow showing token verification steps from signature check through claims validation

Frontend: Acquire and attach tokens

// src/lib/api/auth-fetch.ts
import { PublicClientApplication } from '@azure/msal-browser';
import { apiRequest } from '@/lib/auth/msal-config';

export async function authFetch(
	msalInstance: PublicClientApplication,
	url: string,
	options: RequestInit = {},
): Promise<Response> {
	const account = msalInstance.getActiveAccount();
	if (!account) {
		throw new Error('No active account. User must sign in.');
	}

	// Always use acquireTokenSilent first - it handles caching and refresh
	const tokenResponse = await msalInstance.acquireTokenSilent({
		...apiRequest,
		account,
	});

	return fetch(url, {
		...options,
		headers: {
			...options.headers,
			Authorization: `Bearer ${tokenResponse.accessToken}`,
			'Content-Type': 'application/json',
		},
	});
}

Backend: Validate the JWT (Python/FastAPI example)

# src/infrastructure/auth/token_validator.py
import httpx
from jose import jwt, JWTError
from functools import lru_cache
from typing import Any

from src.infrastructure.config.settings import Settings


class TokenValidationError(Exception):
    """Raised when token validation fails."""
    pass


class AzureADTokenValidator:
    """Validates Azure AD JWT access tokens."""

    def __init__(self, settings: Settings):
        self._tenant_id = settings.azure_ad_tenant_id
        self._client_id = settings.azure_ad_client_id
        self._issuer = f"https://login.microsoftonline.com/{self._tenant_id}/v2.0"
        self._jwks_uri = (
            f"https://login.microsoftonline.com/{self._tenant_id}/discovery/v2.0/keys"
        )
        self._jwks_client = httpx.Client()

    @lru_cache(maxsize=1)
    def _get_signing_keys(self) -> dict[str, Any]:
        """Fetch and cache JWKS from Azure AD."""
        response = self._jwks_client.get(self._jwks_uri)
        response.raise_for_status()
        return response.json()

    def validate_token(self, token: str) -> dict[str, Any]:
        """
        Validate an Azure AD access token.

        Returns the decoded token claims if valid.
        Raises TokenValidationError if invalid.
        """
        try:
            # Get the signing keys
            jwks = self._get_signing_keys()

            # Decode and validate the token
            claims = jwt.decode(
                token,
                jwks,
                algorithms=["RS256"],
                audience=self._client_id,
                issuer=self._issuer,
                options={
                    "verify_aud": True,
                    "verify_iss": True,
                    "verify_exp": True,
                    "verify_nbf": True,
                },
            )

            return claims

        except JWTError as e:
            raise TokenValidationError(f"Token validation failed: {e}") from e

FastAPI dependency for protected routes:

# src/presentation/api/dependencies/auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

from src.infrastructure.auth.token_validator import (
    AzureADTokenValidator,
    TokenValidationError,
)

security = HTTPBearer()


async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    validator: AzureADTokenValidator = Depends(),
) -> dict:
    """Extract and validate the current user from the access token."""
    try:
        claims = validator.validate_token(credentials.credentials)

        return {
            "id": claims.get("oid"),  # Object ID (unique user identifier)
            "email": claims.get("preferred_username"),
            "name": claims.get("name"),
            "tenant_id": claims.get("tid"),
            "scopes": claims.get("scp", "").split(" "),
        }

    except TokenValidationError as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=str(e),
            headers={"WWW-Authenticate": "Bearer"},
        )

Pros:

  • Fully stateless — scales horizontally without session storage
  • Standard OAuth 2.0 pattern — well-documented and widely understood
  • No backend state to manage

Cons:

  • Token validation on every request (mitigated by caching JWKS)
  • Tokens are bearer tokens — if stolen, they grant access until expiry
  • Requires careful scope management

#Pattern 2: Token Exchange with Backend Sessions

The frontend sends the token once to establish a session. The backend validates it, creates a server-side session, and returns a session cookie. Subsequent requests use the cookie instead of the token.

Phase 1: Session Establishment

The user signs in via MSAL, and the frontend exchanges the Azure AD token for a server-side session. This happens once per login.

  1. Frontend acquires access token from MSAL (cached or refreshed)
  2. Frontend sends POST /auth/login with the token in the Authorization header
  3. Backend validates the JWT against Azure AD's signing keys
  4. Backend creates a session in Redis/database with user claims
  5. Backend returns a secure, httpOnly session cookie
  6. Frontend stores nothing — the browser handles the cookie automatically

Phase 2: Authenticated Requests

All subsequent API calls use the session cookie. No token management required on the frontend.

  1. Frontend calls GET /api/data — browser automatically includes the cookie
  2. Backend looks up session in the store, validates expiration
  3. Backend extracts user context from session data
  4. Request proceeds with full user identity

Frontend: Exchange token for session

// src/lib/auth/session-auth.ts
import { PublicClientApplication } from '@azure/msal-browser';
import { apiRequest } from '@/lib/auth/msal-config';

export async function establishSession(msalInstance: PublicClientApplication): Promise<void> {
	const account = msalInstance.getActiveAccount();
	if (!account) {
		throw new Error('No active account');
	}

	const tokenResponse = await msalInstance.acquireTokenSilent({
		...apiRequest,
		account,
	});

	const response = await fetch('/api/auth/login', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
			Authorization: `Bearer ${tokenResponse.accessToken}`,
		},
		credentials: 'include', // Important: include cookies
	});

	if (!response.ok) {
		throw new Error('Session establishment failed');
	}
}

// Regular API calls use cookies automatically
export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
	return fetch(url, {
		...options,
		credentials: 'include',
		headers: {
			...options.headers,
			'Content-Type': 'application/json',
		},
	});
}

Backend: Create and manage sessions

# src/presentation/api/routes/auth.py
from fastapi import APIRouter, Depends, Response, HTTPException
from uuid import uuid4

from src.infrastructure.auth.token_validator import AzureADTokenValidator
from src.infrastructure.session.session_store import SessionStore

router = APIRouter(prefix="/auth", tags=["auth"])


@router.post("/login")
async def login(
    response: Response,
    credentials: HTTPAuthorizationCredentials = Depends(security),
    validator: AzureADTokenValidator = Depends(),
    session_store: SessionStore = Depends(),
):
    """Exchange Azure AD token for a session cookie."""
    claims = validator.validate_token(credentials.credentials)

    # Create session with user data
    session_id = str(uuid4())
    session_data = {
        "user_id": claims.get("oid"),
        "email": claims.get("preferred_username"),
        "name": claims.get("name"),
        "tenant_id": claims.get("tid"),
    }

    await session_store.create(session_id, session_data, ttl=3600)

    # Set secure, httpOnly cookie
    response.set_cookie(
        key="session_id",
        value=session_id,
        httponly=True,
        secure=True,  # HTTPS only
        samesite="lax",
        max_age=3600,
    )

    return {"message": "Session established"}


@router.post("/logout")
async def logout(
    response: Response,
    session_id: str = Cookie(None),
    session_store: SessionStore = Depends(),
):
    """Invalidate the session."""
    if session_id:
        await session_store.delete(session_id)

    response.delete_cookie("session_id")
    return {"message": "Logged out"}

Pros:

  • Session can be revoked server-side (immediate logout)
  • Cookies are automatically sent — simpler frontend code
  • Can store additional session data not in the token

Cons:

  • Requires session storage (Redis, database, etc.)
  • Not purely stateless — horizontal scaling needs shared session store
  • Cookie handling complexity (CORS, SameSite, secure flags)

#Pattern 3: On-Behalf-Of Flow (Downstream APIs)

When your backend needs to call other Microsoft APIs (Graph, Azure services) on behalf of the user, use the On-Behalf-Of (OBO) flow. The backend exchanges the user's token for a new token with different scopes.

How it works:

  1. Frontend acquires a token scoped to your backend API and sends the request
  2. Backend receives the user's token and needs to call Microsoft Graph (or another API)
  3. Backend sends a token exchange request to Azure AD's token endpoint, including:
    • The user's original token (as assertion)
    • The backend's client credentials (client ID + secret)
    • The desired scopes for the downstream API (e.g., User.Read for Graph)
  4. Azure AD validates the assertion and issues a new token scoped for Graph
  5. Backend calls Microsoft Graph with the new token — Graph sees this as the user making the request
  6. Backend returns the combined result to the frontend

Key insight: The user never directly authenticates to Graph. Their identity "flows through" your backend via the OBO exchange. This maintains the principle of least privilege — the frontend token only grants access to your API, not to Graph directly.

Backend: Exchange tokens with OBO

# src/infrastructure/auth/obo_client.py
import httpx
from typing import Any

from src.infrastructure.config.settings import Settings


class OnBehalfOfClient:
    """Exchange user tokens for downstream API access."""

    def __init__(self, settings: Settings):
        self._tenant_id = settings.azure_ad_tenant_id
        self._client_id = settings.azure_ad_client_id
        self._client_secret = settings.azure_ad_client_secret
        self._token_endpoint = (
            f"https://login.microsoftonline.com/{self._tenant_id}/oauth2/v2.0/token"
        )

    async def exchange_token(
        self,
        user_token: str,
        scopes: list[str],
    ) -> dict[str, Any]:
        """
        Exchange a user's access token for a new token with different scopes.

        This implements the OAuth 2.0 On-Behalf-Of flow.
        """
        async with httpx.AsyncClient() as client:
            response = await client.post(
                self._token_endpoint,
                data={
                    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                    "client_id": self._client_id,
                    "client_secret": self._client_secret,
                    "assertion": user_token,
                    "scope": " ".join(scopes),
                    "requested_token_use": "on_behalf_of",
                },
            )

            if response.status_code != 200:
                raise Exception(f"Token exchange failed: {response.text}")

            return response.json()


# Usage in a service
class UserProfileService:
    """Fetch user profile from Microsoft Graph on behalf of the user."""

    def __init__(self, obo_client: OnBehalfOfClient):
        self._obo = obo_client

    async def get_user_photo(self, user_token: str) -> bytes:
        """Get user's profile photo from Graph API."""
        # Exchange for Graph token
        token_response = await self._obo.exchange_token(
            user_token,
            scopes=["https://graph.microsoft.com/User.Read"],
        )

        graph_token = token_response["access_token"]

        # Call Graph API
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://graph.microsoft.com/v1.0/me/photo/$value",
                headers={"Authorization": f"Bearer {graph_token}"},
            )

            return response.content

Important: OBO requires your backend to be registered as a confidential client with a client secret or certificate. The frontend SPA remains a public client.

Pros:

  • Access downstream Microsoft APIs with user context
  • User permissions are respected (delegated, not application permissions)
  • Single sign-on experience — user authenticates once

Cons:

  • Requires client secret on the backend (secure storage needed)
  • More complex token management
  • Consent must be configured correctly in app registration

#Handling Token Refresh

MSAL handles token refresh automatically when you use acquireTokenSilent. However, you need to handle the case where silent acquisition fails:

// src/hooks/use-api-token.ts
import { useMsal } from '@azure/msal-react';
import { InteractionRequiredAuthError } from '@azure/msal-browser';
import { apiRequest } from '@/lib/auth/msal-config';

export function useApiToken() {
	const { instance, accounts } = useMsal();

	const getToken = async (): Promise<string> => {
		const account = accounts[0];
		if (!account) {
			throw new Error('No account available');
		}

		try {
			// Try silent acquisition first (uses cached/refreshed token)
			const response = await instance.acquireTokenSilent({
				...apiRequest,
				account,
			});
			return response.accessToken;
		} catch (error) {
			// If silent fails, fall back to interactive
			if (error instanceof InteractionRequiredAuthError) {
				const response = await instance.acquireTokenPopup(apiRequest);
				return response.accessToken;
			}
			throw error;
		}
	};

	return { getToken };
}

Best practice: Always call acquireTokenSilent first. It checks the cache, validates expiration, and automatically refreshes if needed. Only fall back to interactive methods when required.

#Common Pitfalls and Solutions

#1. CORS Issues with Token Requests

MSAL makes requests to Microsoft endpoints, which handle CORS correctly. But your backend must also be configured:

# FastAPI CORS configuration
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Your frontend origin
    allow_credentials=True,  # Required for cookies
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["*"],
)

#2. Token Audience Mismatch

The aud claim in your token must match what your backend expects:

# Common mistake: checking against the wrong audience
# Token has aud: "api://your-client-id/access_as_user"
# But you're checking against: "your-client-id"

# Solution: Use the full API URI as audience
EXPECTED_AUDIENCE = f"api://{settings.azure_ad_client_id}"

#3. Multi-Account Handling

When users have multiple Microsoft accounts, MSAL might not know which to use:

// Always set the active account explicitly
const accounts = msalInstance.getAllAccounts();

if (accounts.length > 1) {
	// Let user choose or use business logic
	const workAccount = accounts.find((acc) => acc.tenantId === expectedTenantId);
	msalInstance.setActiveAccount(workAccount || accounts[0]);
} else if (accounts.length === 1) {
	msalInstance.setActiveAccount(accounts[0]);
}

#4. Popup Blockers

Some browsers block popups by default. Always provide a redirect fallback:

const handleLogin = async () => {
	try {
		await instance.loginPopup(loginRequest);
	} catch (error) {
		if (error instanceof BrowserAuthError && error.errorCode === 'popup_window_error') {
			// Fallback to redirect
			await instance.loginRedirect(loginRequest);
		}
		throw error;
	}
};

#Key Takeaways

  • Use MSAL React for Microsoft identity — it handles OAuth 2.0 with PKCE, token caching, and refresh automatically
  • Choose your backend pattern wisely — stateless JWT validation for scalability, sessions for revocability, OBO for downstream APIs
  • Always use acquireTokenSilent first — it handles caching and refresh; only fall back to interactive when required
  • Validate tokens properly — fetch JWKS from Microsoft, verify signature, audience, issuer, and expiration
  • Configure CORS and cookies correctly — most authentication issues stem from misconfigured CORS or cookie settings

For an alternative approach using AuthJS with Google OAuth in Next.js applications, check out our guide on AuthJS v5 with Google OAuth: Production-Ready Setup.

Enjoyed this article? Stay updated: