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>
);
}
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
2. Get your Azure AD credentials
- Go to the Azure Portal
- Navigate to Azure Active Directory → App registrations
- Create a new registration or use an existing one
- Copy your Application (client) ID
- Add
http://localhost:3000to Redirect URIs
3. Add environment variables
NEXT_PUBLIC_CLIENT_ID=your-client-id-here
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>
);
}
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>
);
}
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>
);
}
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>;
}
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>
);
}
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>
);
}
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).*)'],
};
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>
);
}
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,
}
);
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>
);
}
Debug Mode
Enable detailed logging for troubleshooting:
<MsalAuthProvider
clientId="..."
enableLogging={true}
>
{children}
</MsalAuthProvider>
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!
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>
Internal Business Application
// Single-tenant with specific permissions
<MsalAuthProvider
clientId="..."
tenantId="your-tenant-id"
scopes={['User.Read', 'Mail.Read', 'Calendars.Read']}
>
{children}
</MsalAuthProvider>
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' });
}
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
}
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>
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']);
}
SSR hydration mismatch
Always use the 'use client' directive for components using auth hooks:
'use client';
import { useMsalAuth } from '@chemmangat/msal-next';
Token acquisition fails
Check that required scopes are granted in Azure AD:
- Go to Azure Portal → App registrations
- Select your app → API permissions
- Add required permissions (e.g.,
User.Read,Mail.Read) - Grant admin consent if needed
Performance Tips
- Use session storage (default) for better performance
- Enable caching for user profiles and roles (on by default)
- Use middleware for route protection instead of client-side checks
- 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 }
);
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
Resources
Found this helpful? Give it a ⭐ on GitHub and drop any questions in the comments below! 👇
Top comments (0)