Implementing Azure AD B2C Authentication in Next.js 14 with NextAuth.js

Azure AD B2C Authentication

Azure AD B2C Authentication


Summary

Prerequisites

Setting Up Azure AD B2C

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

Project Setup

npx create-next-app@latest my-auth-app --typescript --tailwind --app
cd my-auth-app
npm install next-auth@beta @azure/msal-node
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"








  1. 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 };
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",
    };
  }
}
"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>
  );
}
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

Troubleshooting

Conclusion

Additional Resources

Remember to star this repo if you found it helpful! 🌟


Read More Blogs:

Leave a Reply

Your email address will not be published. Required fields are marked *