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

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 *