DEV Community

Hari Manoj
Hari Manoj

Posted on

Microsoft Auth in Next.js is a Nightmare — Here's How I Fixed It

If you've ever tried implementing Microsoft authentication (Azure AD/Entra ID) in a Next.js application, you know the pain. MSAL.js is powerful but complex, and the official examples don't quite fit the Next.js App Router paradigm.

After building authentication for multiple enterprise applications, I created @chemmangat/msal-next — a production-ready library that makes Microsoft auth as simple as it should be.


The Problem with MSAL in Next.js

Let's be honest — integrating MSAL.js with Next.js App Router is frustrating:

  • ❌ SSR/hydration issues everywhere
  • ❌ Boilerplate code in every component
  • ❌ Token refresh logic scattered across your app
  • ❌ No clear pattern for protecting routes
  • ❌ MS Graph API calls require manual token handling
  • ❌ Role-based access control is a nightmare

The Solution: 3 Lines of Code

Here's what authentication should look like in Next.js:

// app/layout.tsx
import { MsalAuthProvider } from '@chemmangat/msal-next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <MsalAuthProvider clientId={process.env.NEXT_PUBLIC_CLIENT_ID!}>
          {children}
        </MsalAuthProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's it. Your entire app now has Microsoft authentication.


Quick Start

1. Install the package

npm install @chemmangat/msal-next @azure/msal-browser @azure/msal-react
Enter fullscreen mode Exit fullscreen mode

2. Get your Azure AD credentials

  1. Go to the Azure Portal
  2. Navigate to Azure Active Directory → App registrations
  3. Create a new registration or use an existing one
  4. Copy your Application (client) ID
  5. Add http://localhost:3000 to Redirect URIs

3. Add environment variables

NEXT_PUBLIC_CLIENT_ID=your-client-id-here
Enter fullscreen mode Exit fullscreen mode

4. Add the provider

// app/layout.tsx
import { MsalAuthProvider } from '@chemmangat/msal-next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <MsalAuthProvider clientId={process.env.NEXT_PUBLIC_CLIENT_ID!}>
          {children}
        </MsalAuthProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Add authentication to your page

// app/page.tsx
'use client';

import { MicrosoftSignInButton, useMsalAuth } from '@chemmangat/msal-next';

export default function Home() {
  const { isAuthenticated, account } = useMsalAuth();

  if (!isAuthenticated) {
    return (
      <div>
        <h1>Welcome!</h1>
        <MicrosoftSignInButton />
      </div>
    );
  }

  return (
    <div>
      <h1>Hello, {account?.name}!</h1>
      <p>Email: {account?.username}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Run npm run dev and you have working Microsoft authentication! 🎉


Real-World Features

Beautiful Pre-Built Components

No need to design authentication UI from scratch:

import {
  MicrosoftSignInButton,
  SignOutButton,
  UserAvatar,
  AuthStatus,
  AuthGuard,
} from '@chemmangat/msal-next';

function MyApp() {
  return (
    <div>
      {/* Microsoft-branded sign-in button */}
      <MicrosoftSignInButton variant="dark" size="large" />

      {/* User avatar with MS Graph photo */}
      <UserAvatar size={48} showTooltip />

      {/* Current auth status */}
      <AuthStatus showDetails />

      {/* Protect sensitive content */}
      <AuthGuard>
        <AdminPanel />
      </AuthGuard>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

MS Graph API Made Easy

Calling Microsoft Graph API is now trivial:

'use client';

import { useGraphApi } from '@chemmangat/msal-next';

function EmailList() {
  const graph = useGraphApi();

  const fetchEmails = async () => {
    // Automatically handles token acquisition and injection
    const response = await graph.get('/me/messages');
    return response.value;
  };

  const sendEmail = async () => {
    await graph.post('/me/sendMail', {
      message: {
        subject: 'Hello from Next.js!',
        body: { content: 'Sent via MS Graph API' },
      },
    });
  };

  return <div>{/* Your UI */}</div>;
}
Enter fullscreen mode Exit fullscreen mode

User Profile with Caching

Get user data with automatic caching:

import { useUserProfile } from '@chemmangat/msal-next';

function ProfilePage() {
  const { profile, loading, error } = useUserProfile();

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{profile.displayName}</h1>
      <p>{profile.mail}</p>
      <p>{profile.jobTitle}</p>
      <p>{profile.officeLocation}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Role-Based Access Control

Implement RBAC in seconds:

import { useRoles } from '@chemmangat/msal-next';

function AdminPanel() {
  const { hasRole, hasAnyRole, roles } = useRoles();

  if (!hasRole('Admin')) {
    return <div>Access denied</div>;
  }

  return (
    <div>
      <h1>Admin Panel</h1>
      {hasAnyRole(['Editor', 'Contributor']) && (
        <button>Edit Content</button>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Protect Routes with Middleware

Secure your entire app at the edge:

// middleware.ts
import { createAuthMiddleware } from '@chemmangat/msal-next';

export const middleware = createAuthMiddleware({
  protectedRoutes: ['/dashboard', '/profile', '/api/protected'],
  publicOnlyRoutes: ['/login'],
  loginPath: '/login',
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Enter fullscreen mode Exit fullscreen mode

Server-Side Authentication

Check auth status in Server Components:

// app/dashboard/page.tsx
import { getServerSession } from '@chemmangat/msal-next/server';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await getServerSession();

  if (!session.isAuthenticated) {
    redirect('/login');
  }

  return (
    <div>
      <h1>Welcome {session.username}</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Production-Ready Features

Automatic Token Refresh with Retry

Built-in exponential backoff for token acquisition:

import { retryWithBackoff } from '@chemmangat/msal-next';

const token = await retryWithBackoff(
  () => acquireToken(['User.Read']),
  {
    maxRetries: 3,
    initialDelay: 1000,
    backoffMultiplier: 2,
  }
);
Enter fullscreen mode Exit fullscreen mode

Comprehensive Error Handling

Catch and handle auth errors gracefully:

import { ErrorBoundary } from '@chemmangat/msal-next';

function App() {
  return (
    <ErrorBoundary
      fallback={(error, reset) => (
        <div>
          <h2>Authentication Error</h2>
          <p>{error.message}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}
    >
      <YourApp />
    </ErrorBoundary>
  );
}
Enter fullscreen mode Exit fullscreen mode

Debug Mode

Enable detailed logging for troubleshooting:

<MsalAuthProvider
  clientId="..."
  enableLogging={true}
>
  {children}
</MsalAuthProvider>
Enter fullscreen mode Exit fullscreen mode

TypeScript Support

Full type safety with custom claims:

import { CustomTokenClaims } from '@chemmangat/msal-next';

interface MyCustomClaims extends CustomTokenClaims {
  roles: string[];
  department: string;
  employeeId: string;
}

const { account } = useMsalAuth();
const claims = account?.idTokenClaims as MyCustomClaims;

console.log(claims.roles);      // Type-safe!
console.log(claims.department); // Type-safe!
Enter fullscreen mode Exit fullscreen mode

Comparison with Alternatives

Feature @chemmangat/msal-next next-auth clerk
Microsoft Auth ✅ Native ⚠️ Via provider ✅ Yes
Setup Complexity 🟢 Minimal 🟡 Medium 🟢 Minimal
MS Graph API ✅ Built-in ❌ Manual ❌ Manual
Role-Based Access ✅ Built-in ⚠️ Custom ✅ Yes
Edge Middleware ✅ Yes ✅ Yes ✅ Yes
Pricing 🟢 Free 🟢 Free 🔴 Paid
Bundle Size 🟢 Small 🟡 Medium 🟡 Medium

Common Use Cases

Enterprise SaaS Application

// Multi-tenant support out of the box
<MsalAuthProvider
  clientId="..."
  authorityType="common" // Supports any Azure AD tenant
>
  {children}
</MsalAuthProvider>
Enter fullscreen mode Exit fullscreen mode

Internal Business Application

// Single-tenant with specific permissions
<MsalAuthProvider
  clientId="..."
  tenantId="your-tenant-id"
  scopes={['User.Read', 'Mail.Read', 'Calendars.Read']}
>
  {children}
</MsalAuthProvider>
Enter fullscreen mode Exit fullscreen mode

API Route Protection

// app/api/data/route.ts
import { getServerSession } from '@chemmangat/msal-next/server';

export async function GET() {
  const session = await getServerSession();

  if (!session.isAuthenticated) {
    return new Response('Unauthorized', { status: 401 });
  }

  return Response.json({ data: 'protected data' });
}
Enter fullscreen mode Exit fullscreen mode

Migration Guide

From next-auth

// Before (next-auth)
import { useSession, signIn, signOut } from 'next-auth/react';

const { data: session } = useSession();
if (session) {
  // authenticated
}

// After (@chemmangat/msal-next)
import { useMsalAuth } from '@chemmangat/msal-next';

const { isAuthenticated, account, loginPopup, logoutPopup } = useMsalAuth();
if (isAuthenticated) {
  // authenticated
}
Enter fullscreen mode Exit fullscreen mode

From Raw MSAL

// Before (raw MSAL)
const msalInstance = new PublicClientApplication(config);
await msalInstance.initialize();
const accounts = msalInstance.getAllAccounts();
// ... lots of boilerplate

// After (@chemmangat/msal-next)
<MsalAuthProvider clientId="...">
  {children}
</MsalAuthProvider>
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

"No active account" error

Make sure the user is logged in before calling acquireToken:

const { isAuthenticated, acquireToken } = useMsalAuth();

if (isAuthenticated) {
  const token = await acquireToken(['User.Read']);
}
Enter fullscreen mode Exit fullscreen mode

SSR hydration mismatch

Always use the 'use client' directive for components using auth hooks:

'use client';

import { useMsalAuth } from '@chemmangat/msal-next';
Enter fullscreen mode Exit fullscreen mode

Token acquisition fails

Check that required scopes are granted in Azure AD:

  1. Go to Azure Portal → App registrations
  2. Select your app → API permissions
  3. Add required permissions (e.g., User.Read, Mail.Read)
  4. Grant admin consent if needed

Performance Tips

  1. Use session storage (default) for better performance
  2. Enable caching for user profiles and roles (on by default)
  3. Use middleware for route protection instead of client-side checks
  4. Lazy load auth components with dynamic imports
import dynamic from 'next/dynamic';

const AuthGuard = dynamic(
  () => import('@chemmangat/msal-next').then((mod) => mod.AuthGuard),
  { ssr: false }
);
Enter fullscreen mode Exit fullscreen mode

What's Next?

The library is actively maintained with these features already available or in progress:

  • ✅ B2C support
  • ✅ Certificate-based authentication
  • ✅ Advanced caching strategies
  • ✅ React Server Components integration
  • ✅ CLI scaffolding tool

Conclusion

Microsoft authentication in Next.js doesn't have to be complicated. With @chemmangat/msal-next, you get:

  • ✅ Production-ready authentication in 3 lines of code
  • ✅ Beautiful pre-built components
  • ✅ MS Graph API integration
  • ✅ Role-based access control
  • ✅ Full TypeScript support
  • ✅ Zero configuration required

Give it a try:

npm install @chemmangat/msal-next @azure/msal-browser @azure/msal-react
Enter fullscreen mode Exit fullscreen mode

Resources


Found this helpful? Give it a ⭐ on GitHub and drop any questions in the comments below! 👇

Top comments (0)