
Azure AD B2C Authentication
Summary
In this comprehensive guide, we’ll walk through implementing Azure AD B2C authentication in a Next.js 14 application using NextAuth.js. We’ll cover everything from initial setup to advanced features like token refresh and session management.
Prerequisites
- Node.js 18.17 or later installed
- An Azure account with an active subscription
- Basic knowledge of Next.js and React
- Understanding of OAuth 2.0 and OpenID Connect
Setting Up Azure AD B2C
Create a new Azure AD B2C tenant:
- Go to Azure Portal
- Search for “Azure AD B2C”
- Click “Create a new tenant”
- Fill in the required information
Register your application:
- Select "App registrations"
- Click "New registration"
- Name: "NextJS Auth App"
- Supported account types: "Accounts in any identity provider..."
- Redirect URI: http://localhost:3000/api/auth/callback/azure-ad-b2c
Configure authentication:
- Get your Application (client) ID
- Create a client secret
- Configure user flows (policies)
Project Setup
- Create a new Next.js project:
npx create-next-app@latest my-auth-app --typescript --tailwind --app
cd my-auth-app
- Install required dependencies:
npm install next-auth@beta @azure/msal-node
- Create environment variables (.env.local):
AZURE_AD_B2C_TENANT_NAME="your-tenant"
AZURE_AD_B2C_CLIENT_ID="your-client-id"
AZURE_AD_B2C_CLIENT_SECRET="your-client-secret"
AZURE_AD_B2C_PRIMARY_USER_FLOW="B2C_1_signin"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-random-secret"
Implementation
- Create the auth configuration (app/api/auth/[…nextauth]/route.ts):
import NextAuth from "next-auth";
import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
import { JWT } from "next-auth/jwt";
const tenantName = process.env.AZURE_AD_B2C_TENANT_NAME;
const policyName = process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW;
const issuer = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${policyName}/v2.0`;
// Token refresh configuration
const MAX_TOKEN_AGE = 1 * 60 * 60; // 1 hour
const REFRESH_TOKEN_THRESHOLD = 5 * 60; // 5 minutes
export const authOptions = {
providers: [
AzureADB2CProvider({
issuer,
clientId: process.env.AZURE_AD_B2C_CLIENT_ID!,
clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET!,
tenantId: process.env.AZURE_AD_B2C_TENANT_ID!,
primaryUserFlow: policyName,
authorization: {
params: {
scope: "offline_access openid profile email",
},
},
profile(profile) {
return {
id: profile.sub,
name: profile.name || `${profile.given_name} ${profile.family_name}`,
email: profile.emails?.[0] || profile.email,
image: profile.picture,
};
},
}),
],
callbacks: {
async jwt({ token, account, user }) {
if (account && user) {
return {
accessToken: account.access_token,
accessTokenExpires: Date.now() + (account.expires_in || 3600) * 1000,
refreshToken: account.refresh_token,
user,
};
}
if (Date.now() < token.accessTokenExpires - REFRESH_TOKEN_THRESHOLD * 1000) {
return token;
}
return refreshAccessToken(token);
},
async session({ session, token }) {
return {
...session,
accessToken: token.accessToken,
accessTokenExpires: token.accessTokenExpires,
error: token.error,
user: token.user,
};
},
},
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
session: {
strategy: "jwt",
maxAge: MAX_TOKEN_AGE,
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
- Implement token refresh function:
async function refreshAccessToken(token: JWT) {
try {
const response = await fetch(
`https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${policyName}/oauth2/v2.0/token`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: process.env.AZURE_AD_B2C_CLIENT_ID!,
client_secret: process.env.AZURE_AD_B2C_CLIENT_SECRET!,
refresh_token: token.refreshToken as string,
scope: "offline_access openid profile email",
}),
}
);
const refreshedToken = await response.json();
if (!response.ok) {
throw refreshedToken;
}
return {
...token,
accessToken: refreshedToken.access_token,
accessTokenExpires: Date.now() + (refreshedToken.expires_in || 3600) * 1000,
refreshToken: refreshedToken.refresh_token ?? token.refreshToken,
};
} catch (error) {
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
- Create a token verification component:
"use client";
import { useState } from "react";
import { useSession } from "next-auth/react";
export default function TokenVerifyButton() {
const [tokenStatus, setTokenStatus] = useState("");
const { data: session, update } = useSession();
const verifyToken = async () => {
try {
const response = await fetch("/api/verify-token", {
headers: {
'authorization': `Bearer ${session?.accessToken}`
}
});
const data = await response.json();
if (data.status === "refresh_failed" || data.status === "token_expired") {
await update();
setTokenStatus("Token refreshed, please verify again");
return;
}
setTokenStatus(
`Status: ${data.status}\nUser: ${data.user?.id?.slice(0, 20) || 'N/A'}...`
);
} catch (error) {
setTokenStatus("Error verifying token");
}
};
return (
<div className="space-y-4">
<button
onClick={verifyToken}
className="bg-blue-500 text-white hover:bg-blue-700 font-bold py-2 px-4 rounded"
>
Verify Token
</button>
{tokenStatus && (
<pre className="bg-gray-100 p-4 rounded whitespace-pre-wrap">
{tokenStatus}
</pre>
)}
</div>
);
}
- Implement the verification API endpoint (app/api/verify-token/route.ts):
import { headers } from 'next/headers';
import { getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]/route";
export async function GET() {
const session = await getServerSession(authOptions);
const headersList = headers();
const token = headersList.get('authorization');
if (!session || !token) {
return Response.json({ status: "unauthorized" }, { status: 401 });
}
if (!session.refreshToken) {
return Response.json({ status: "logged_out" }, { status: 401 });
}
if (session.error === "RefreshAccessTokenError") {
return Response.json({ status: "refresh_failed" }, { status: 401 });
}
if (Date.now() >= (session.accessTokenExpires as number)) {
return Response.json({ status: "token_expired" }, { status: 401 });
}
return Response.json({
status: "authenticated",
accessToken: session.accessToken,
user: session.user,
expires: session.accessTokenExpires,
});
}
Best Practices & Security
- Environment Variables
- Never commit .env files
- Use different variables for development and production
- Rotate secrets regularly
- Token Management
- Implement proper token refresh logic
- Handle token expiration gracefully
- Store tokens securely
- Error Handling
- Implement comprehensive error handling
- Provide clear error messages
- Log errors appropriately
- Session Management
- Use appropriate session timeouts
- Implement proper logout handling
- Clear session data on logout
Troubleshooting
Common issues and solutions:
- 401 Unauthorized Errors
- Check if tokens are being properly passed
- Verify token expiration
- Ensure proper refresh token handling
- Token Refresh Issues
- Verify refresh token is present
- Check Azure AD B2C configuration
- Validate scopes
- Callback URL Errors
- Verify redirect URI configuration
- Check for environment mismatches
- Validate protocol (http/https)
Conclusion
You now have a robust authentication system using Azure AD B2C with Next.js and NextAuth.js. This implementation includes token refresh, session management, and proper error handling.
Additional Resources
Remember to star this repo if you found it helpful! 🌟
Read More Blogs:
- Majorana 1: One of the Millions of Futures of Quantum Computing
- Alpine announce Kush Maini as Test and Reserve Driver for 2025 | Formula 1
- India vs New Zealand Champions Trophy 2025 Final: India Clinches Thrilling Victory – Relive the Epic Showdown!
- India vs Australia Cricket Match: Celebrating India’s Thrilling Victory | Key Highlights & Analysis
- IND vs PAK Live Match: Where to Watch, Streaming, and More!
Leave a Reply