@chemmangat/msal-next 4.2.0 → 4.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +483 -614
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,108 +1,29 @@
|
|
|
1
1
|
# @chemmangat/msal-next
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Microsoft/Azure AD authentication for Next.js App Router. Minimal setup, full TypeScript support, production-ready.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@chemmangat/msal-next)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](./SECURITY.md)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Current version: 4.2.1**
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
### 🚀 **5-Minute Setup**
|
|
16
|
-
Get Azure AD authentication running in your Next.js app in just 5 minutes. No complex configuration, no boilerplate.
|
|
17
|
-
|
|
18
|
-
### 🔒 **Enterprise Security**
|
|
19
|
-
Built on Microsoft's official MSAL library. All authentication happens client-side - tokens never touch your server. [Read Security Policy →](./SECURITY.md)
|
|
20
|
-
|
|
21
|
-
### 🎯 **Production-Ready**
|
|
22
|
-
Used by 2,200+ developers in production. Automatic token refresh prevents unexpected logouts. Complete TypeScript support.
|
|
23
|
-
|
|
24
|
-
### 🤖 **AI-Friendly**
|
|
25
|
-
Complete documentation optimized for AI assistants. Setup instructions that work on the first try.
|
|
26
|
-
|
|
27
|
-
### ⚡ **Zero Boilerplate**
|
|
28
|
-
```tsx
|
|
29
|
-
<MSALProvider clientId="...">
|
|
30
|
-
<MicrosoftSignInButton />
|
|
31
|
-
</MSALProvider>
|
|
32
|
-
```
|
|
33
|
-
That's it. You're done.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 🎯 Top Features
|
|
38
|
-
|
|
39
|
-
| Feature | Description | Status |
|
|
40
|
-
|---------|-------------|--------|
|
|
41
|
-
| **Automatic Token Refresh** | Prevents unexpected logouts | ✅ v4.1.0 |
|
|
42
|
-
| **Complete TypeScript Types** | 30+ user profile fields | ✅ v4.0.2 |
|
|
43
|
-
| **Actionable Error Messages** | Fix instructions included | ✅ v4.0.2 |
|
|
44
|
-
| **Configuration Validation** | Catches mistakes in dev mode | ✅ v4.0.2 |
|
|
45
|
-
| **Zero-Config Protected Routes** | One line to protect pages | ✅ v4.0.1 |
|
|
46
|
-
| **Server Components Support** | Works in Next.js layouts | ✅ Always |
|
|
47
|
-
| **Microsoft Graph Integration** | Pre-configured API client | ✅ Always |
|
|
48
|
-
| **Role-Based Access Control** | Built-in RBAC support | ✅ Always |
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 🔒 Security First
|
|
53
|
-
|
|
54
|
-
**Your tokens never leave the browser:**
|
|
55
|
-
- ✅ Client-side authentication only
|
|
56
|
-
- ✅ No server-side token storage
|
|
57
|
-
- ✅ Microsoft's official MSAL library
|
|
58
|
-
- ✅ Secure token storage (sessionStorage/localStorage)
|
|
59
|
-
- ✅ Automatic error sanitization
|
|
60
|
-
- ✅ HTTPS enforcement in production
|
|
61
|
-
|
|
62
|
-
**[Read Complete Security Policy →](./SECURITY.md)**
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## 🚀 Quick Start (5 Minutes)
|
|
67
|
-
|
|
68
|
-
### Step 1: Install the Package
|
|
13
|
+
## Install
|
|
69
14
|
|
|
70
15
|
```bash
|
|
71
16
|
npm install @chemmangat/msal-next @azure/msal-browser @azure/msal-react
|
|
72
17
|
```
|
|
73
18
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
1. Go to [Azure Portal](https://portal.azure.com)
|
|
77
|
-
2. Navigate to **Azure Active Directory** → **App registrations**
|
|
78
|
-
3. Click **New registration**
|
|
79
|
-
4. Enter a name (e.g., "My Next.js App")
|
|
80
|
-
5. Select **Single-page application (SPA)**
|
|
81
|
-
6. Add redirect URI: `http://localhost:3000` (for development)
|
|
82
|
-
7. Click **Register**
|
|
83
|
-
8. Copy the **Application (client) ID** and **Directory (tenant) ID**
|
|
84
|
-
|
|
85
|
-
### Step 3: Configure Environment Variables
|
|
86
|
-
|
|
87
|
-
Create `.env.local` in your project root:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
# .env.local
|
|
91
|
-
NEXT_PUBLIC_AZURE_AD_CLIENT_ID=your-client-id-here
|
|
92
|
-
NEXT_PUBLIC_AZURE_AD_TENANT_ID=your-tenant-id-here
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Important:**
|
|
96
|
-
- Replace `your-client-id-here` and `your-tenant-id-here` with actual values from Azure Portal
|
|
97
|
-
- Never commit `.env.local` to version control
|
|
98
|
-
- Variables starting with `NEXT_PUBLIC_` are exposed to the browser (this is correct for MSAL)
|
|
19
|
+
---
|
|
99
20
|
|
|
100
|
-
|
|
21
|
+
## Quick Start
|
|
101
22
|
|
|
102
|
-
###
|
|
23
|
+
### 1. Add the provider to your layout
|
|
103
24
|
|
|
104
25
|
```tsx
|
|
105
|
-
// app/layout.tsx (Server Component
|
|
26
|
+
// app/layout.tsx (Server Component — no 'use client' needed)
|
|
106
27
|
import { MSALProvider } from '@chemmangat/msal-next';
|
|
107
28
|
|
|
108
29
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
@@ -121,489 +42,274 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
121
42
|
}
|
|
122
43
|
```
|
|
123
44
|
|
|
124
|
-
|
|
45
|
+
`MSALProvider` is already marked `'use client'` internally.
|
|
125
46
|
|
|
126
|
-
###
|
|
47
|
+
### 2. Add a sign-in button
|
|
127
48
|
|
|
128
49
|
```tsx
|
|
129
50
|
// app/page.tsx
|
|
130
51
|
'use client';
|
|
131
52
|
|
|
132
|
-
import { MicrosoftSignInButton, useMsalAuth } from '@chemmangat/msal-next';
|
|
53
|
+
import { MicrosoftSignInButton, SignOutButton, useMsalAuth } from '@chemmangat/msal-next';
|
|
133
54
|
|
|
134
55
|
export default function HomePage() {
|
|
135
56
|
const { isAuthenticated, account } = useMsalAuth();
|
|
136
57
|
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<div>
|
|
140
|
-
<h1>Welcome, {account?.name}!</h1>
|
|
141
|
-
<p>You are signed in as {account?.username}</p>
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return (
|
|
58
|
+
return isAuthenticated ? (
|
|
147
59
|
<div>
|
|
148
|
-
<
|
|
149
|
-
<
|
|
60
|
+
<p>Welcome, {account?.name}</p>
|
|
61
|
+
<SignOutButton />
|
|
150
62
|
</div>
|
|
63
|
+
) : (
|
|
64
|
+
<MicrosoftSignInButton />
|
|
151
65
|
);
|
|
152
66
|
}
|
|
153
67
|
```
|
|
154
68
|
|
|
155
|
-
###
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
npm run dev
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Visit `http://localhost:3000` and click the sign-in button!
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## 📋 Complete Setup Guide for AI Assistants
|
|
166
|
-
|
|
167
|
-
When implementing MSAL authentication in a Next.js project, follow these steps:
|
|
168
|
-
|
|
169
|
-
### 1. Installation
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
npm install @chemmangat/msal-next @azure/msal-browser@^4.0.0 @azure/msal-react@^3.0.0
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### 2. Azure AD Configuration
|
|
176
|
-
|
|
177
|
-
**Required Azure AD Setup:**
|
|
178
|
-
- App registration created in Azure Portal
|
|
179
|
-
- Redirect URI configured: `http://localhost:3000` (dev) and `https://yourdomain.com` (prod)
|
|
180
|
-
- Platform type: **Single-page application (SPA)**
|
|
181
|
-
- API permissions: At minimum `User.Read` (usually granted by default)
|
|
182
|
-
|
|
183
|
-
**Get these values from Azure Portal:**
|
|
184
|
-
- `Application (client) ID` → Use as `NEXT_PUBLIC_AZURE_AD_CLIENT_ID`
|
|
185
|
-
- `Directory (tenant) ID` → Use as `NEXT_PUBLIC_AZURE_AD_TENANT_ID`
|
|
186
|
-
|
|
187
|
-
### 3. Environment Variables
|
|
188
|
-
|
|
189
|
-
Create `.env.local`:
|
|
69
|
+
### 3. Set environment variables
|
|
190
70
|
|
|
191
71
|
```bash
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
**Critical Rules:**
|
|
197
|
-
- Variables MUST start with `NEXT_PUBLIC_` to be accessible in browser
|
|
198
|
-
- Use actual GUIDs, not placeholder text
|
|
199
|
-
- Never commit `.env.local` to version control
|
|
200
|
-
- Restart dev server after changing environment variables
|
|
201
|
-
|
|
202
|
-
### 4. Project Structure
|
|
203
|
-
|
|
204
|
-
```
|
|
205
|
-
your-app/
|
|
206
|
-
├── app/
|
|
207
|
-
│ ├── layout.tsx # Add MSALProvider here
|
|
208
|
-
│ ├── page.tsx # Add sign-in button here
|
|
209
|
-
│ └── dashboard/
|
|
210
|
-
│ └── page.tsx # Protected page example
|
|
211
|
-
├── .env.local # Environment variables
|
|
212
|
-
└── package.json
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### 5. Implementation Files
|
|
216
|
-
|
|
217
|
-
**File 1: `app/layout.tsx` (Server Component)**
|
|
218
|
-
|
|
219
|
-
```tsx
|
|
220
|
-
import { MSALProvider } from '@chemmangat/msal-next';
|
|
221
|
-
import './globals.css';
|
|
222
|
-
|
|
223
|
-
export default function RootLayout({
|
|
224
|
-
children,
|
|
225
|
-
}: {
|
|
226
|
-
children: React.ReactNode;
|
|
227
|
-
}) {
|
|
228
|
-
return (
|
|
229
|
-
<html lang="en">
|
|
230
|
-
<body>
|
|
231
|
-
<MSALProvider
|
|
232
|
-
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
233
|
-
tenantId={process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID!}
|
|
234
|
-
>
|
|
235
|
-
{children}
|
|
236
|
-
</MSALProvider>
|
|
237
|
-
</body>
|
|
238
|
-
</html>
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
**File 2: `app/page.tsx` (Client Component)**
|
|
244
|
-
|
|
245
|
-
```tsx
|
|
246
|
-
'use client';
|
|
247
|
-
|
|
248
|
-
import { MicrosoftSignInButton, SignOutButton, useMsalAuth } from '@chemmangat/msal-next';
|
|
249
|
-
|
|
250
|
-
export default function HomePage() {
|
|
251
|
-
const { isAuthenticated, account } = useMsalAuth();
|
|
252
|
-
|
|
253
|
-
return (
|
|
254
|
-
<div style={{ padding: '2rem' }}>
|
|
255
|
-
<h1>My App</h1>
|
|
256
|
-
|
|
257
|
-
{isAuthenticated ? (
|
|
258
|
-
<div>
|
|
259
|
-
<p>Welcome, {account?.name}!</p>
|
|
260
|
-
<p>Email: {account?.username}</p>
|
|
261
|
-
<SignOutButton />
|
|
262
|
-
</div>
|
|
263
|
-
) : (
|
|
264
|
-
<div>
|
|
265
|
-
<p>Please sign in to continue</p>
|
|
266
|
-
<MicrosoftSignInButton />
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
**File 3: `app/dashboard/page.tsx` (Protected Page)**
|
|
275
|
-
|
|
276
|
-
```tsx
|
|
277
|
-
'use client';
|
|
278
|
-
|
|
279
|
-
import { AuthGuard, useUserProfile } from '@chemmangat/msal-next';
|
|
280
|
-
|
|
281
|
-
export default function DashboardPage() {
|
|
282
|
-
return (
|
|
283
|
-
<AuthGuard>
|
|
284
|
-
<DashboardContent />
|
|
285
|
-
</AuthGuard>
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function DashboardContent() {
|
|
290
|
-
const { profile, loading } = useUserProfile();
|
|
291
|
-
|
|
292
|
-
if (loading) return <div>Loading profile...</div>;
|
|
293
|
-
|
|
294
|
-
return (
|
|
295
|
-
<div style={{ padding: '2rem' }}>
|
|
296
|
-
<h1>Dashboard</h1>
|
|
297
|
-
<p>Name: {profile?.displayName}</p>
|
|
298
|
-
<p>Email: {profile?.mail}</p>
|
|
299
|
-
<p>Job Title: {profile?.jobTitle}</p>
|
|
300
|
-
<p>Department: {profile?.department}</p>
|
|
301
|
-
</div>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### 6. Common Patterns
|
|
307
|
-
|
|
308
|
-
**Pattern 1: Check Authentication Status**
|
|
309
|
-
|
|
310
|
-
```tsx
|
|
311
|
-
'use client';
|
|
312
|
-
|
|
313
|
-
import { useMsalAuth } from '@chemmangat/msal-next';
|
|
314
|
-
|
|
315
|
-
export default function MyComponent() {
|
|
316
|
-
const { isAuthenticated, account, inProgress } = useMsalAuth();
|
|
317
|
-
|
|
318
|
-
if (inProgress) return <div>Loading...</div>;
|
|
319
|
-
if (!isAuthenticated) return <div>Please sign in</div>;
|
|
320
|
-
|
|
321
|
-
return <div>Hello, {account?.name}!</div>;
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**Pattern 2: Get Access Token**
|
|
326
|
-
|
|
327
|
-
```tsx
|
|
328
|
-
'use client';
|
|
329
|
-
|
|
330
|
-
import { useMsalAuth } from '@chemmangat/msal-next';
|
|
331
|
-
import { useEffect, useState } from 'react';
|
|
332
|
-
|
|
333
|
-
export default function DataComponent() {
|
|
334
|
-
const { acquireToken, isAuthenticated } = useMsalAuth();
|
|
335
|
-
const [data, setData] = useState(null);
|
|
336
|
-
|
|
337
|
-
useEffect(() => {
|
|
338
|
-
async function fetchData() {
|
|
339
|
-
if (!isAuthenticated) return;
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
const token = await acquireToken(['User.Read']);
|
|
343
|
-
|
|
344
|
-
const response = await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
345
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const result = await response.json();
|
|
349
|
-
setData(result);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error('Error fetching data:', error);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
fetchData();
|
|
356
|
-
}, [isAuthenticated, acquireToken]);
|
|
357
|
-
|
|
358
|
-
return <div>{/* Render data */}</div>;
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
**Pattern 3: Protect Routes**
|
|
363
|
-
|
|
364
|
-
```tsx
|
|
365
|
-
'use client';
|
|
366
|
-
|
|
367
|
-
import { AuthGuard } from '@chemmangat/msal-next';
|
|
368
|
-
|
|
369
|
-
export default function ProtectedPage() {
|
|
370
|
-
return (
|
|
371
|
-
<AuthGuard
|
|
372
|
-
loadingComponent={<div>Checking authentication...</div>}
|
|
373
|
-
fallbackComponent={<div>Redirecting to login...</div>}
|
|
374
|
-
>
|
|
375
|
-
<div>This content is protected</div>
|
|
376
|
-
</AuthGuard>
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
### 7. Configuration Options
|
|
382
|
-
|
|
383
|
-
```tsx
|
|
384
|
-
<MSALProvider
|
|
385
|
-
// Required
|
|
386
|
-
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
387
|
-
|
|
388
|
-
// Optional - for single-tenant apps
|
|
389
|
-
tenantId={process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}
|
|
390
|
-
|
|
391
|
-
// Optional - for multi-tenant apps (default: 'common')
|
|
392
|
-
authorityType="common" // or "organizations", "consumers", "tenant"
|
|
393
|
-
|
|
394
|
-
// Optional - default scopes (default: ['User.Read'])
|
|
395
|
-
scopes={['User.Read', 'Mail.Read']}
|
|
396
|
-
|
|
397
|
-
// Optional - enable debug logging (default: false)
|
|
398
|
-
enableLogging={true}
|
|
399
|
-
|
|
400
|
-
// Optional - custom redirect URI (default: window.location.origin)
|
|
401
|
-
redirectUri="https://yourdomain.com"
|
|
402
|
-
|
|
403
|
-
// Optional - cache location (default: 'sessionStorage')
|
|
404
|
-
cacheLocation="sessionStorage" // or "localStorage", "memoryStorage"
|
|
405
|
-
|
|
406
|
-
// NEW in v4.1.0 - automatic token refresh
|
|
407
|
-
autoRefreshToken={true} // Prevents unexpected logouts
|
|
408
|
-
refreshBeforeExpiry={300} // Refresh 5 min before expiry
|
|
409
|
-
>
|
|
410
|
-
{children}
|
|
411
|
-
</MSALProvider>
|
|
72
|
+
# .env.local
|
|
73
|
+
NEXT_PUBLIC_AZURE_AD_CLIENT_ID=your-client-id
|
|
74
|
+
NEXT_PUBLIC_AZURE_AD_TENANT_ID=your-tenant-id
|
|
412
75
|
```
|
|
413
76
|
|
|
414
|
-
###
|
|
415
|
-
|
|
416
|
-
**If authentication doesn't work:**
|
|
417
|
-
|
|
418
|
-
1. ✅ Check environment variables are set correctly
|
|
419
|
-
2. ✅ Restart dev server after adding `.env.local`
|
|
420
|
-
3. ✅ Verify redirect URI in Azure Portal matches your app URL
|
|
421
|
-
4. ✅ Ensure `'use client'` is at the TOP of files using hooks
|
|
422
|
-
5. ✅ Check browser console for errors
|
|
423
|
-
6. ✅ Enable debug logging: `enableLogging={true}`
|
|
424
|
-
7. ✅ Verify client ID and tenant ID are valid GUIDs
|
|
425
|
-
|
|
426
|
-
**Common Errors:**
|
|
77
|
+
### 4. Azure AD setup
|
|
427
78
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
79
|
+
1. Go to [Azure Portal](https://portal.azure.com) → Azure Active Directory → App registrations
|
|
80
|
+
2. Create a new registration, platform type: **Single-page application (SPA)**
|
|
81
|
+
3. Add redirect URI: `http://localhost:3000` (dev) and your production URL
|
|
82
|
+
4. Copy the **Application (client) ID** and **Directory (tenant) ID**
|
|
432
83
|
|
|
433
84
|
---
|
|
434
85
|
|
|
435
|
-
##
|
|
436
|
-
|
|
437
|
-
- ✅ **Zero-Config Setup** - Works out of the box with minimal configuration
|
|
438
|
-
- ✅ **Automatic Token Refresh** - Prevents unexpected logouts (NEW in v4.1.0)
|
|
439
|
-
- ✅ **TypeScript First** - Complete type safety with 30+ user profile fields
|
|
440
|
-
- ✅ **Next.js 14+ Ready** - Built for App Router with Server Components support
|
|
441
|
-
- ✅ **Automatic Validation** - Catches configuration mistakes in development
|
|
442
|
-
- ✅ **Actionable Errors** - Clear error messages with fix instructions
|
|
443
|
-
- ✅ **Production Ready** - Used by 2,200+ developers in production
|
|
444
|
-
- ✅ **Fixed Interaction Issues** - No more "interaction in progress" errors (v4.1.0)
|
|
445
|
-
|
|
446
|
-
---
|
|
447
|
-
|
|
448
|
-
## 📚 API Reference
|
|
86
|
+
## API Reference
|
|
449
87
|
|
|
450
88
|
### Components
|
|
451
89
|
|
|
452
90
|
#### MSALProvider
|
|
453
|
-
Wrap your app to provide authentication context.
|
|
454
91
|
|
|
455
92
|
```tsx
|
|
456
|
-
<MSALProvider
|
|
93
|
+
<MSALProvider
|
|
94
|
+
clientId="..."
|
|
95
|
+
tenantId="..."
|
|
96
|
+
authorityType="common"
|
|
97
|
+
scopes={['User.Read']}
|
|
98
|
+
redirectUri="https://myapp.com"
|
|
99
|
+
cacheLocation="sessionStorage"
|
|
100
|
+
enableLogging={false}
|
|
101
|
+
autoRefreshToken={true}
|
|
102
|
+
refreshBeforeExpiry={300}
|
|
103
|
+
allowedRedirectUris={['https://myapp.com']}
|
|
104
|
+
protection={{ defaultRedirectTo: '/login' }}
|
|
105
|
+
>
|
|
457
106
|
{children}
|
|
458
107
|
</MSALProvider>
|
|
459
108
|
```
|
|
460
109
|
|
|
110
|
+
| Prop | Type | Default | Description |
|
|
111
|
+
|------|------|---------|-------------|
|
|
112
|
+
| `clientId` | `string` | required | Azure AD Application (client) ID |
|
|
113
|
+
| `tenantId` | `string` | — | Directory (tenant) ID, single-tenant only |
|
|
114
|
+
| `authorityType` | `'common' \| 'organizations' \| 'consumers' \| 'tenant'` | `'common'` | Authority type |
|
|
115
|
+
| `redirectUri` | `string` | `window.location.origin` | Redirect URI after auth |
|
|
116
|
+
| `postLogoutRedirectUri` | `string` | `redirectUri` | Redirect URI after logout |
|
|
117
|
+
| `scopes` | `string[]` | `['User.Read']` | Default scopes |
|
|
118
|
+
| `cacheLocation` | `'sessionStorage' \| 'localStorage' \| 'memoryStorage'` | `'sessionStorage'` | Token cache location |
|
|
119
|
+
| `enableLogging` | `boolean` | `false` | Debug logging |
|
|
120
|
+
| `autoRefreshToken` | `boolean` | `false` | Auto-refresh tokens before expiry |
|
|
121
|
+
| `refreshBeforeExpiry` | `number` | `300` | Seconds before expiry to refresh |
|
|
122
|
+
| `allowedRedirectUris` | `string[]` | — | Whitelist of allowed redirect URIs |
|
|
123
|
+
| `protection` | `AuthProtectionConfig` | — | Zero-config route protection config |
|
|
124
|
+
|
|
461
125
|
#### MicrosoftSignInButton
|
|
462
|
-
Pre-styled sign-in button with Microsoft branding.
|
|
463
126
|
|
|
464
127
|
```tsx
|
|
465
128
|
<MicrosoftSignInButton
|
|
466
|
-
variant="dark"
|
|
467
|
-
size="medium"
|
|
468
|
-
|
|
129
|
+
variant="dark" // 'dark' | 'light'
|
|
130
|
+
size="medium" // 'small' | 'medium' | 'large'
|
|
131
|
+
text="Sign in with Microsoft"
|
|
132
|
+
scopes={['User.Read']}
|
|
133
|
+
onSuccess={() => {}}
|
|
134
|
+
onError={(error) => {}}
|
|
469
135
|
/>
|
|
470
136
|
```
|
|
471
137
|
|
|
472
138
|
#### SignOutButton
|
|
473
|
-
Pre-styled sign-out button.
|
|
474
139
|
|
|
475
140
|
```tsx
|
|
476
141
|
<SignOutButton
|
|
477
142
|
variant="light"
|
|
478
143
|
size="medium"
|
|
479
|
-
onSuccess={() =>
|
|
144
|
+
onSuccess={() => {}}
|
|
145
|
+
onError={(error) => {}}
|
|
480
146
|
/>
|
|
481
147
|
```
|
|
482
148
|
|
|
483
149
|
#### AuthGuard
|
|
484
|
-
|
|
150
|
+
|
|
151
|
+
Protects content and redirects unauthenticated users to login.
|
|
485
152
|
|
|
486
153
|
```tsx
|
|
487
154
|
<AuthGuard
|
|
488
155
|
loadingComponent={<div>Loading...</div>}
|
|
489
|
-
fallbackComponent={<div>
|
|
156
|
+
fallbackComponent={<div>Redirecting...</div>}
|
|
157
|
+
scopes={['User.Read']}
|
|
158
|
+
onAuthRequired={() => {}}
|
|
490
159
|
>
|
|
491
160
|
<ProtectedContent />
|
|
492
161
|
</AuthGuard>
|
|
493
162
|
```
|
|
494
163
|
|
|
495
164
|
#### UserAvatar
|
|
496
|
-
Display user photo from Microsoft Graph.
|
|
497
165
|
|
|
498
166
|
```tsx
|
|
499
|
-
<UserAvatar
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
167
|
+
<UserAvatar size={48} showTooltip fallbackImage="/avatar.png" />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### AccountSwitcher
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<AccountSwitcher
|
|
174
|
+
variant="default" // 'default' | 'compact' | 'minimal'
|
|
175
|
+
maxAccounts={5}
|
|
176
|
+
showAvatars
|
|
177
|
+
showAddButton
|
|
178
|
+
showRemoveButton
|
|
179
|
+
onSwitch={(account) => {}}
|
|
180
|
+
onAdd={() => {}}
|
|
181
|
+
onRemove={(account) => {}}
|
|
182
|
+
/>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### AccountList
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
<AccountList
|
|
189
|
+
showAvatars
|
|
190
|
+
showDetails
|
|
191
|
+
showActiveIndicator
|
|
192
|
+
clickToSwitch
|
|
193
|
+
orientation="vertical" // 'vertical' | 'horizontal'
|
|
194
|
+
onAccountClick={(account) => {}}
|
|
503
195
|
/>
|
|
504
196
|
```
|
|
505
197
|
|
|
198
|
+
---
|
|
199
|
+
|
|
506
200
|
### Hooks
|
|
507
201
|
|
|
508
202
|
#### useMsalAuth()
|
|
509
|
-
Main authentication hook.
|
|
510
203
|
|
|
511
204
|
```tsx
|
|
512
205
|
const {
|
|
513
|
-
account,
|
|
514
|
-
accounts,
|
|
515
|
-
isAuthenticated,
|
|
516
|
-
inProgress,
|
|
517
|
-
loginRedirect,
|
|
518
|
-
logoutRedirect,
|
|
519
|
-
acquireToken,
|
|
206
|
+
account, // AccountInfo | null
|
|
207
|
+
accounts, // AccountInfo[]
|
|
208
|
+
isAuthenticated, // boolean
|
|
209
|
+
inProgress, // boolean
|
|
210
|
+
loginRedirect, // (scopes?: string[]) => Promise<void>
|
|
211
|
+
logoutRedirect, // () => Promise<void>
|
|
212
|
+
acquireToken, // (scopes: string[]) => Promise<string> — silent with redirect fallback
|
|
213
|
+
acquireTokenSilent, // (scopes: string[]) => Promise<string> — silent only
|
|
214
|
+
acquireTokenRedirect, // (scopes: string[]) => Promise<void>
|
|
215
|
+
clearSession, // () => Promise<void> — clears cache without Microsoft logout
|
|
520
216
|
} = useMsalAuth();
|
|
521
217
|
```
|
|
522
218
|
|
|
523
219
|
#### useUserProfile()
|
|
524
|
-
|
|
220
|
+
|
|
221
|
+
Fetches user profile from Microsoft Graph `/me` (30+ fields).
|
|
525
222
|
|
|
526
223
|
```tsx
|
|
527
224
|
const {
|
|
528
|
-
profile, //
|
|
529
|
-
loading, //
|
|
530
|
-
error, // Error
|
|
531
|
-
refetch, //
|
|
532
|
-
clearCache, //
|
|
225
|
+
profile, // UserProfile | null
|
|
226
|
+
loading, // boolean
|
|
227
|
+
error, // Error | null
|
|
228
|
+
refetch, // () => Promise<void>
|
|
229
|
+
clearCache, // () => void
|
|
533
230
|
} = useUserProfile();
|
|
534
231
|
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
console.log(profile?.preferredLanguage);
|
|
539
|
-
console.log(profile?.employeeId);
|
|
232
|
+
// With custom fields
|
|
233
|
+
interface MyProfile extends UserProfile { customField: string }
|
|
234
|
+
const { profile } = useUserProfile<MyProfile>();
|
|
540
235
|
```
|
|
541
236
|
|
|
542
237
|
#### useGraphApi()
|
|
543
|
-
Pre-configured Microsoft Graph API client.
|
|
544
238
|
|
|
545
239
|
```tsx
|
|
546
240
|
const graph = useGraphApi();
|
|
547
241
|
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
body: { content: 'World' }
|
|
555
|
-
});
|
|
242
|
+
const user = await graph.get('/me');
|
|
243
|
+
const result = await graph.post('/me/messages', body);
|
|
244
|
+
await graph.put('/me/photo/$value', blob);
|
|
245
|
+
await graph.patch('/me', { displayName: 'New Name' });
|
|
246
|
+
await graph.delete('/me/messages/{id}');
|
|
247
|
+
const data = await graph.request('/me', { version: 'beta' });
|
|
556
248
|
```
|
|
557
249
|
|
|
558
250
|
#### useRoles()
|
|
559
|
-
Access user's Azure AD roles.
|
|
560
251
|
|
|
561
252
|
```tsx
|
|
562
253
|
const {
|
|
563
|
-
roles,
|
|
564
|
-
groups,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
254
|
+
roles, // string[]
|
|
255
|
+
groups, // string[]
|
|
256
|
+
loading, // boolean
|
|
257
|
+
error, // Error | null
|
|
258
|
+
hasRole, // (role: string) => boolean
|
|
259
|
+
hasGroup, // (groupId: string) => boolean
|
|
260
|
+
hasAnyRole, // (roles: string[]) => boolean
|
|
261
|
+
hasAllRoles, // (roles: string[]) => boolean
|
|
262
|
+
refetch, // () => Promise<void>
|
|
568
263
|
} = useRoles();
|
|
569
|
-
|
|
570
|
-
if (hasRole('Admin')) {
|
|
571
|
-
// Show admin content
|
|
572
|
-
}
|
|
573
264
|
```
|
|
574
265
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
## 🎓 Advanced Usage
|
|
578
|
-
|
|
579
|
-
### Automatic Token Refresh (NEW in v4.1.0)
|
|
266
|
+
#### useTokenRefresh()
|
|
580
267
|
|
|
581
|
-
|
|
268
|
+
Monitors token expiry and optionally refreshes tokens automatically in the background. Use this when you want to show a session-expiry warning or trigger a manual refresh without relying solely on `MSALProvider`'s `autoRefreshToken` prop.
|
|
582
269
|
|
|
583
270
|
```tsx
|
|
584
|
-
|
|
585
|
-
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
586
|
-
autoRefreshToken={true} // Enable automatic refresh
|
|
587
|
-
refreshBeforeExpiry={300} // Refresh 5 minutes before expiry
|
|
588
|
-
>
|
|
589
|
-
{children}
|
|
590
|
-
</MSALProvider>
|
|
271
|
+
useTokenRefresh(options?: UseTokenRefreshOptions): UseTokenRefreshReturn
|
|
591
272
|
```
|
|
592
273
|
|
|
593
|
-
|
|
274
|
+
Options (`UseTokenRefreshOptions`):
|
|
275
|
+
|
|
276
|
+
| Option | Type | Default | Description |
|
|
277
|
+
|--------|------|---------|-------------|
|
|
278
|
+
| `enabled` | `boolean` | `true` | Enable automatic background refresh |
|
|
279
|
+
| `refreshBeforeExpiry` | `number` | `300` | Seconds before expiry to trigger refresh |
|
|
280
|
+
| `scopes` | `string[]` | `['User.Read']` | Scopes to refresh |
|
|
281
|
+
| `onRefresh` | `(expiresIn: number) => void` | — | Called after a successful refresh |
|
|
282
|
+
| `onError` | `(error: Error) => void` | — | Called when refresh fails |
|
|
283
|
+
|
|
284
|
+
Return values (`UseTokenRefreshReturn`):
|
|
285
|
+
|
|
286
|
+
| Value | Type | Description |
|
|
287
|
+
|-------|------|-------------|
|
|
288
|
+
| `expiresIn` | `number \| null` | Seconds until the current token expires |
|
|
289
|
+
| `isExpiringSoon` | `boolean` | `true` when within `refreshBeforeExpiry` window |
|
|
290
|
+
| `refresh` | `() => Promise<void>` | Manually trigger a token refresh |
|
|
291
|
+
| `lastRefresh` | `Date \| null` | Timestamp of the last successful refresh |
|
|
594
292
|
|
|
595
293
|
```tsx
|
|
596
294
|
'use client';
|
|
597
295
|
|
|
598
296
|
import { useTokenRefresh } from '@chemmangat/msal-next';
|
|
599
297
|
|
|
600
|
-
export default function
|
|
601
|
-
const { expiresIn, isExpiringSoon } = useTokenRefresh(
|
|
298
|
+
export default function SessionBanner() {
|
|
299
|
+
const { expiresIn, isExpiringSoon, refresh, lastRefresh } = useTokenRefresh({
|
|
300
|
+
enabled: true,
|
|
301
|
+
refreshBeforeExpiry: 300, // warn/refresh 5 min before expiry
|
|
302
|
+
scopes: ['User.Read'],
|
|
303
|
+
onRefresh: (expiresIn) => console.log(`Refreshed — expires in ${expiresIn}s`),
|
|
304
|
+
onError: (error) => console.error('Refresh failed', error),
|
|
305
|
+
});
|
|
602
306
|
|
|
603
307
|
if (isExpiringSoon) {
|
|
604
308
|
return (
|
|
605
|
-
<div className="warning">
|
|
606
|
-
|
|
309
|
+
<div className="session-warning">
|
|
310
|
+
Session expires in {Math.floor((expiresIn ?? 0) / 60)} minutes.{' '}
|
|
311
|
+
<button onClick={refresh}>Stay signed in</button>
|
|
312
|
+
{lastRefresh && <span> Last refreshed: {lastRefresh.toLocaleTimeString()}</span>}
|
|
607
313
|
</div>
|
|
608
314
|
);
|
|
609
315
|
}
|
|
@@ -612,35 +318,217 @@ export default function SessionWarning() {
|
|
|
612
318
|
}
|
|
613
319
|
```
|
|
614
320
|
|
|
615
|
-
|
|
321
|
+
#### useMultiAccount()
|
|
616
322
|
|
|
617
|
-
|
|
323
|
+
Manages multiple simultaneously signed-in Microsoft accounts. Use this when your app needs to support users who work across multiple tenants or have both personal and work accounts. Accepts an optional `defaultScopes` argument used when adding a new account.
|
|
618
324
|
|
|
619
325
|
```tsx
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
326
|
+
useMultiAccount(defaultScopes?: string[]): UseMultiAccountReturn
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Return values (`UseMultiAccountReturn`):
|
|
330
|
+
|
|
331
|
+
| Value | Type | Description |
|
|
332
|
+
|-------|------|-------------|
|
|
333
|
+
| `accounts` | `AccountInfo[]` | All accounts currently in the MSAL cache |
|
|
334
|
+
| `activeAccount` | `AccountInfo \| null` | The currently active account |
|
|
335
|
+
| `hasMultipleAccounts` | `boolean` | `true` when more than one account is cached |
|
|
336
|
+
| `accountCount` | `number` | Total number of cached accounts |
|
|
337
|
+
| `inProgress` | `boolean` | `true` while an MSAL interaction is running |
|
|
338
|
+
| `switchAccount` | `(account: AccountInfo) => void` | Set a different account as active |
|
|
339
|
+
| `addAccount` | `(scopes?: string[]) => Promise<void>` | Sign in with an additional account |
|
|
340
|
+
| `removeAccount` | `(account: AccountInfo) => Promise<void>` | Remove an account from the cache |
|
|
341
|
+
| `signOutAccount` | `(account: AccountInfo) => Promise<void>` | Sign out a specific account |
|
|
342
|
+
| `signOutAll` | `() => Promise<void>` | Sign out all accounts |
|
|
343
|
+
| `getAccountByUsername` | `(username: string) => AccountInfo \| undefined` | Look up an account by username |
|
|
344
|
+
| `getAccountById` | `(homeAccountId: string) => AccountInfo \| undefined` | Look up an account by home account ID |
|
|
345
|
+
| `isActiveAccount` | `(account: AccountInfo) => boolean` | Check whether a given account is active |
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
'use client';
|
|
349
|
+
|
|
350
|
+
import { useMultiAccount } from '@chemmangat/msal-next';
|
|
351
|
+
|
|
352
|
+
export default function AccountManager() {
|
|
353
|
+
const {
|
|
354
|
+
accounts,
|
|
355
|
+
activeAccount,
|
|
356
|
+
hasMultipleAccounts,
|
|
357
|
+
accountCount,
|
|
358
|
+
inProgress,
|
|
359
|
+
switchAccount,
|
|
360
|
+
addAccount,
|
|
361
|
+
signOutAccount,
|
|
362
|
+
signOutAll,
|
|
363
|
+
isActiveAccount,
|
|
364
|
+
} = useMultiAccount(['User.Read']);
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<div>
|
|
368
|
+
<p>Signed in as: {activeAccount?.name} ({activeAccount?.username})</p>
|
|
369
|
+
<p>{accountCount} account{accountCount !== 1 ? 's' : ''} cached</p>
|
|
370
|
+
|
|
371
|
+
{hasMultipleAccounts && (
|
|
372
|
+
<ul>
|
|
373
|
+
{accounts.map((account) => (
|
|
374
|
+
<li key={account.homeAccountId}>
|
|
375
|
+
{account.name}
|
|
376
|
+
{isActiveAccount(account) && ' (active)'}
|
|
377
|
+
<button onClick={() => switchAccount(account)} disabled={isActiveAccount(account)}>
|
|
378
|
+
Switch
|
|
379
|
+
</button>
|
|
380
|
+
<button onClick={() => signOutAccount(account)}>Sign out</button>
|
|
381
|
+
</li>
|
|
382
|
+
))}
|
|
383
|
+
</ul>
|
|
384
|
+
)}
|
|
385
|
+
|
|
386
|
+
<button onClick={() => addAccount()} disabled={inProgress}>
|
|
387
|
+
Add another account
|
|
388
|
+
</button>
|
|
389
|
+
<button onClick={() => signOutAll()} disabled={inProgress}>
|
|
390
|
+
Sign out all
|
|
391
|
+
</button>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
626
395
|
```
|
|
627
396
|
|
|
628
|
-
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### Higher-Order Components
|
|
629
400
|
|
|
630
|
-
|
|
401
|
+
#### withAuth
|
|
631
402
|
|
|
632
403
|
```tsx
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
scopes
|
|
404
|
+
const ProtectedPage = withAuth(MyPage, {
|
|
405
|
+
loadingComponent: <Spinner />,
|
|
406
|
+
scopes: ['User.Read'],
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### withPageAuth
|
|
411
|
+
|
|
412
|
+
Wraps a page component with authentication and optional role-based access control. Returns a new component that checks auth before rendering. Takes a `PageAuthConfig` as the second argument and an optional `AuthProtectionConfig` as the third for global defaults.
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
withPageAuth<P>(
|
|
416
|
+
Component: ComponentType<P>,
|
|
417
|
+
authConfig: PageAuthConfig,
|
|
418
|
+
globalConfig?: AuthProtectionConfig
|
|
419
|
+
): ComponentType<P>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`PageAuthConfig` options:
|
|
423
|
+
|
|
424
|
+
| Option | Type | Default | Description |
|
|
425
|
+
|--------|------|---------|-------------|
|
|
426
|
+
| `required` | `boolean` | `false` | Whether authentication is required |
|
|
427
|
+
| `roles` | `string[]` | — | User must have at least one of these roles (from `idTokenClaims.roles`) |
|
|
428
|
+
| `redirectTo` | `string` | `'/login'` | Where to redirect unauthenticated users |
|
|
429
|
+
| `loading` | `ReactNode` | — | Component shown while auth state is resolving |
|
|
430
|
+
| `unauthorized` | `ReactNode` | — | Component shown instead of redirecting when access is denied |
|
|
431
|
+
| `validate` | `(account: any) => boolean \| Promise<boolean>` | — | Custom access check; return `true` to allow, `false` to deny |
|
|
432
|
+
|
|
433
|
+
`AuthProtectionConfig` options (global defaults, third argument):
|
|
434
|
+
|
|
435
|
+
| Option | Type | Default | Description |
|
|
436
|
+
|--------|------|---------|-------------|
|
|
437
|
+
| `defaultRedirectTo` | `string` | `'/login'` | Fallback redirect for unauthenticated users |
|
|
438
|
+
| `defaultLoading` | `ReactNode` | — | Fallback loading component |
|
|
439
|
+
| `defaultUnauthorized` | `ReactNode` | — | Fallback unauthorized component |
|
|
440
|
+
| `debug` | `boolean` | `false` | Log auth decisions to the console |
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
// app/dashboard/page.tsx
|
|
444
|
+
'use client';
|
|
445
|
+
|
|
446
|
+
import { withPageAuth } from '@chemmangat/msal-next';
|
|
447
|
+
|
|
448
|
+
function Dashboard() {
|
|
449
|
+
return <div>Dashboard — only admins and editors can see this</div>;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export default withPageAuth(
|
|
453
|
+
Dashboard,
|
|
454
|
+
{
|
|
455
|
+
required: true,
|
|
456
|
+
roles: ['Admin', 'Editor'],
|
|
457
|
+
redirectTo: '/login',
|
|
458
|
+
loading: <div>Checking access...</div>,
|
|
459
|
+
unauthorized: <div>You do not have permission to view this page.</div>,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
debug: process.env.NODE_ENV === 'development',
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Custom `validate` — restrict by email domain:
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
export default withPageAuth(Dashboard, {
|
|
471
|
+
required: true,
|
|
472
|
+
validate: (account) => account.username.endsWith('@company.com'),
|
|
473
|
+
unauthorized: <div>Only company accounts are allowed.</div>,
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### ProtectedPage
|
|
478
|
+
|
|
479
|
+
The underlying component that `withPageAuth` uses internally. Use it directly when you need to protect arbitrary JSX rather than a whole page component — for example inside a layout or a slot.
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
<ProtectedPage
|
|
483
|
+
config={PageAuthConfig}
|
|
484
|
+
defaultRedirectTo="/login"
|
|
485
|
+
defaultLoading={<Spinner />}
|
|
486
|
+
defaultUnauthorized={<div>Access denied</div>}
|
|
487
|
+
debug={false}
|
|
636
488
|
>
|
|
637
489
|
{children}
|
|
638
|
-
</
|
|
490
|
+
</ProtectedPage>
|
|
639
491
|
```
|
|
640
492
|
|
|
641
|
-
|
|
493
|
+
Props (`ProtectedPageProps`):
|
|
642
494
|
|
|
643
|
-
|
|
495
|
+
| Prop | Type | Default | Description |
|
|
496
|
+
|------|------|---------|-------------|
|
|
497
|
+
| `children` | `ReactNode` | required | Content to render when access is granted |
|
|
498
|
+
| `config` | `PageAuthConfig` | required | Per-page auth rules (same shape as `withPageAuth` second arg) |
|
|
499
|
+
| `defaultRedirectTo` | `string` | `'/login'` | Redirect path when unauthenticated |
|
|
500
|
+
| `defaultLoading` | `ReactNode` | — | Loading component while auth resolves |
|
|
501
|
+
| `defaultUnauthorized` | `ReactNode` | — | Component shown when access is denied |
|
|
502
|
+
| `debug` | `boolean` | `false` | Log auth decisions to the console |
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
// app/settings/layout.tsx
|
|
506
|
+
'use client';
|
|
507
|
+
|
|
508
|
+
import { ProtectedPage } from '@chemmangat/msal-next';
|
|
509
|
+
|
|
510
|
+
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
|
511
|
+
return (
|
|
512
|
+
<ProtectedPage
|
|
513
|
+
config={{
|
|
514
|
+
required: true,
|
|
515
|
+
roles: ['Admin'],
|
|
516
|
+
redirectTo: '/login',
|
|
517
|
+
}}
|
|
518
|
+
defaultLoading={<div>Loading...</div>}
|
|
519
|
+
defaultUnauthorized={<div>Admins only.</div>}
|
|
520
|
+
>
|
|
521
|
+
{children}
|
|
522
|
+
</ProtectedPage>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
### Server Utilities
|
|
530
|
+
|
|
531
|
+
#### getServerSession
|
|
644
532
|
|
|
645
533
|
```tsx
|
|
646
534
|
// app/profile/page.tsx (Server Component)
|
|
@@ -649,33 +537,48 @@ import { redirect } from 'next/navigation';
|
|
|
649
537
|
|
|
650
538
|
export default async function ProfilePage() {
|
|
651
539
|
const session = await getServerSession();
|
|
540
|
+
if (!session.isAuthenticated) redirect('/login');
|
|
541
|
+
return <div>Welcome, {session.username}</div>;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
652
544
|
|
|
653
|
-
|
|
654
|
-
redirect('/login');
|
|
655
|
-
}
|
|
545
|
+
---
|
|
656
546
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
547
|
+
### Middleware
|
|
548
|
+
|
|
549
|
+
#### createAuthMiddleware
|
|
550
|
+
|
|
551
|
+
Creates a Next.js Edge middleware function that enforces authentication at the routing level — before any page or API route renders. Use this to protect entire route groups without adding `AuthGuard` or `withPageAuth` to every page.
|
|
552
|
+
|
|
553
|
+
The middleware reads a session cookie (`msal.account` by default) to determine auth state. Because MSAL runs in the browser, the cookie must be set client-side after login (e.g. via `setServerSessionCookie` from `@chemmangat/msal-next/server`). You can also supply a custom `isAuthenticated` function to integrate your own session store.
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
createAuthMiddleware(config?: AuthMiddlewareConfig): (request: NextRequest) => Promise<NextResponse>
|
|
664
557
|
```
|
|
665
558
|
|
|
666
|
-
|
|
559
|
+
`AuthMiddlewareConfig` options:
|
|
667
560
|
|
|
668
|
-
|
|
561
|
+
| Option | Type | Default | Description |
|
|
562
|
+
|--------|------|---------|-------------|
|
|
563
|
+
| `protectedRoutes` | `string[]` | — | Paths that require authentication. Unauthenticated requests redirect to `loginPath` |
|
|
564
|
+
| `publicOnlyRoutes` | `string[]` | — | Paths only accessible when NOT authenticated (e.g. `/login`). Authenticated users are redirected to `redirectAfterLogin` |
|
|
565
|
+
| `loginPath` | `string` | `'/login'` | Where to redirect unauthenticated users |
|
|
566
|
+
| `redirectAfterLogin` | `string` | `'/'` | Where to redirect authenticated users away from `publicOnlyRoutes` |
|
|
567
|
+
| `sessionCookie` | `string` | `'msal.account'` | Cookie name used to detect an active session |
|
|
568
|
+
| `isAuthenticated` | `(request: NextRequest) => boolean \| Promise<boolean>` | — | Custom auth check; overrides the default cookie check |
|
|
569
|
+
| `debug` | `boolean` | `false` | Log routing decisions to the console |
|
|
669
570
|
|
|
670
571
|
```tsx
|
|
671
572
|
// middleware.ts
|
|
672
573
|
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
673
574
|
|
|
674
575
|
export const middleware = createAuthMiddleware({
|
|
675
|
-
protectedRoutes: ['/dashboard', '/profile', '/api/protected'],
|
|
676
|
-
publicOnlyRoutes: ['/login'],
|
|
576
|
+
protectedRoutes: ['/dashboard', '/profile', '/settings', '/api/protected'],
|
|
577
|
+
publicOnlyRoutes: ['/login', '/signup'],
|
|
677
578
|
loginPath: '/login',
|
|
678
|
-
|
|
579
|
+
redirectAfterLogin: '/dashboard',
|
|
580
|
+
sessionCookie: 'msal.account',
|
|
581
|
+
debug: process.env.NODE_ENV === 'development',
|
|
679
582
|
});
|
|
680
583
|
|
|
681
584
|
export const config = {
|
|
@@ -683,174 +586,140 @@ export const config = {
|
|
|
683
586
|
};
|
|
684
587
|
```
|
|
685
588
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
Use enhanced error handling for better debugging:
|
|
589
|
+
Custom `isAuthenticated` — use your own session store:
|
|
689
590
|
|
|
690
591
|
```tsx
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
import {
|
|
592
|
+
// middleware.ts
|
|
593
|
+
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
594
|
+
import { verifySession } from './lib/session';
|
|
694
595
|
|
|
695
|
-
export
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// Check if user cancelled (not a real error)
|
|
705
|
-
if (msalError.isUserCancellation()) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Display actionable error message
|
|
710
|
-
console.error(msalError.toConsoleString());
|
|
711
|
-
}
|
|
712
|
-
};
|
|
596
|
+
export const middleware = createAuthMiddleware({
|
|
597
|
+
protectedRoutes: ['/dashboard'],
|
|
598
|
+
loginPath: '/login',
|
|
599
|
+
isAuthenticated: async (request) => {
|
|
600
|
+
const token = request.cookies.get('session-token')?.value;
|
|
601
|
+
if (!token) return false;
|
|
602
|
+
return verifySession(token);
|
|
603
|
+
},
|
|
604
|
+
});
|
|
713
605
|
|
|
714
|
-
|
|
715
|
-
|
|
606
|
+
export const config = {
|
|
607
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
608
|
+
};
|
|
716
609
|
```
|
|
717
610
|
|
|
718
|
-
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Common Patterns
|
|
719
614
|
|
|
720
|
-
|
|
615
|
+
### Protect a page with AuthGuard
|
|
721
616
|
|
|
722
617
|
```tsx
|
|
723
618
|
'use client';
|
|
724
619
|
|
|
725
|
-
import {
|
|
620
|
+
import { AuthGuard, useUserProfile } from '@chemmangat/msal-next';
|
|
726
621
|
|
|
727
|
-
|
|
728
|
-
|
|
622
|
+
export default function DashboardPage() {
|
|
623
|
+
return (
|
|
624
|
+
<AuthGuard>
|
|
625
|
+
<DashboardContent />
|
|
626
|
+
</AuthGuard>
|
|
627
|
+
);
|
|
729
628
|
}
|
|
730
629
|
|
|
731
|
-
|
|
732
|
-
const { profile } = useUserProfile
|
|
733
|
-
|
|
630
|
+
function DashboardContent() {
|
|
631
|
+
const { profile, loading } = useUserProfile();
|
|
632
|
+
if (loading) return <div>Loading...</div>;
|
|
734
633
|
return (
|
|
735
634
|
<div>
|
|
736
|
-
<p>
|
|
737
|
-
<p>
|
|
635
|
+
<p>{profile?.displayName}</p>
|
|
636
|
+
<p>{profile?.mail}</p>
|
|
637
|
+
<p>{profile?.department}</p>
|
|
738
638
|
</div>
|
|
739
639
|
);
|
|
740
640
|
}
|
|
741
641
|
```
|
|
742
642
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
## 🔧 Configuration Reference
|
|
746
|
-
|
|
747
|
-
### MSALProvider Props
|
|
748
|
-
|
|
749
|
-
| Prop | Type | Required | Default | Description |
|
|
750
|
-
|------|------|----------|---------|-------------|
|
|
751
|
-
| `clientId` | `string` | ✅ Yes | - | Azure AD Application (client) ID |
|
|
752
|
-
| `tenantId` | `string` | No | - | Azure AD Directory (tenant) ID (for single-tenant) |
|
|
753
|
-
| `authorityType` | `'common' \| 'organizations' \| 'consumers' \| 'tenant'` | No | `'common'` | Authority type |
|
|
754
|
-
| `redirectUri` | `string` | No | `window.location.origin` | Redirect URI after authentication |
|
|
755
|
-
| `scopes` | `string[]` | No | `['User.Read']` | Default scopes |
|
|
756
|
-
| `cacheLocation` | `'sessionStorage' \| 'localStorage' \| 'memoryStorage'` | No | `'sessionStorage'` | Token cache location |
|
|
757
|
-
| `enableLogging` | `boolean` | No | `false` | Enable debug logging |
|
|
758
|
-
|
|
759
|
-
### Authority Types
|
|
643
|
+
### Acquire a token for API calls
|
|
760
644
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
- **`consumers`** - Microsoft personal accounts only
|
|
764
|
-
- **`tenant`** - Single-tenant (requires `tenantId`)
|
|
765
|
-
|
|
766
|
-
---
|
|
767
|
-
|
|
768
|
-
## 📖 Additional Resources
|
|
769
|
-
|
|
770
|
-
### Documentation
|
|
771
|
-
- [SECURITY.md](./SECURITY.md) - **Security policy and best practices** ⭐
|
|
772
|
-
- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) - Common issues and solutions
|
|
773
|
-
- [CHANGELOG.md](./CHANGELOG.md) - Version history
|
|
774
|
-
- [MIGRATION_GUIDE_v3.md](./MIGRATION_GUIDE_v3.md) - Migrating from v2.x
|
|
775
|
-
- [EXAMPLES_v4.0.2.md](./EXAMPLES_v4.0.2.md) - Code examples
|
|
776
|
-
|
|
777
|
-
### Security
|
|
778
|
-
- 🔒 [Security Policy](./SECURITY.md) - Complete security documentation
|
|
779
|
-
- 🛡️ [Best Practices](./SECURITY.md#-best-practices) - Security guidelines
|
|
780
|
-
- ⚠️ [Common Mistakes](./SECURITY.md#-common-security-mistakes) - What to avoid
|
|
781
|
-
- ✅ [Security Checklist](./SECURITY.md#-security-checklist) - Pre-deployment checklist
|
|
782
|
-
|
|
783
|
-
### Links
|
|
784
|
-
- 📦 [npm Package](https://www.npmjs.com/package/@chemmangat/msal-next)
|
|
785
|
-
- 🚀 [Live Demo](https://github.com/Chemmangat/msal-next-demo) - Sample implementation
|
|
786
|
-
- 🐛 [Report Issues](https://github.com/chemmangat/msal-next/issues)
|
|
787
|
-
- 💬 [Discussions](https://github.com/chemmangat/msal-next/discussions)
|
|
788
|
-
- ⭐ [GitHub Repository](https://github.com/chemmangat/msal-next)
|
|
789
|
-
|
|
790
|
-
### Microsoft Resources
|
|
791
|
-
- [Azure AD Documentation](https://learn.microsoft.com/en-us/azure/active-directory/)
|
|
792
|
-
- [MSAL.js Documentation](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview)
|
|
793
|
-
- [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview)
|
|
794
|
-
|
|
795
|
-
---
|
|
645
|
+
```tsx
|
|
646
|
+
'use client';
|
|
796
647
|
|
|
797
|
-
|
|
648
|
+
import { useMsalAuth } from '@chemmangat/msal-next';
|
|
798
649
|
|
|
799
|
-
|
|
800
|
-
|
|
650
|
+
export default function DataComponent() {
|
|
651
|
+
const { acquireToken, isAuthenticated } = useMsalAuth();
|
|
801
652
|
|
|
802
|
-
|
|
803
|
-
|
|
653
|
+
const fetchData = async () => {
|
|
654
|
+
if (!isAuthenticated) return;
|
|
655
|
+
const token = await acquireToken(['User.Read']);
|
|
656
|
+
const res = await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
657
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
658
|
+
});
|
|
659
|
+
return res.json();
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
```
|
|
804
663
|
|
|
805
|
-
|
|
806
|
-
A: Yes, the package is MIT licensed and free. Azure AD has a free tier for up to 50,000 users.
|
|
664
|
+
### Error handling
|
|
807
665
|
|
|
808
|
-
|
|
809
|
-
|
|
666
|
+
```tsx
|
|
667
|
+
import { useMsalAuth, wrapMsalError } from '@chemmangat/msal-next';
|
|
810
668
|
|
|
811
|
-
|
|
812
|
-
A: Use `useUserProfile()` hook which provides 30+ fields from Microsoft Graph.
|
|
669
|
+
const { loginRedirect } = useMsalAuth();
|
|
813
670
|
|
|
814
|
-
|
|
815
|
-
|
|
671
|
+
const handleLogin = async () => {
|
|
672
|
+
try {
|
|
673
|
+
await loginRedirect();
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const msalError = wrapMsalError(error);
|
|
676
|
+
if (msalError.isUserCancellation()) return;
|
|
677
|
+
console.error(msalError.toConsoleString());
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
```
|
|
816
681
|
|
|
817
|
-
|
|
818
|
-
A: This package is designed for Azure AD. For B2C, you may need additional configuration.
|
|
682
|
+
### Automatic token refresh
|
|
819
683
|
|
|
820
|
-
|
|
821
|
-
|
|
684
|
+
```tsx
|
|
685
|
+
<MSALProvider
|
|
686
|
+
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
687
|
+
autoRefreshToken={true}
|
|
688
|
+
refreshBeforeExpiry={300}
|
|
689
|
+
>
|
|
690
|
+
{children}
|
|
691
|
+
</MSALProvider>
|
|
692
|
+
```
|
|
822
693
|
|
|
823
694
|
---
|
|
824
695
|
|
|
825
|
-
##
|
|
826
|
-
|
|
827
|
-
Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
696
|
+
## Troubleshooting
|
|
828
697
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
698
|
+
- **"createContext only works in Client Components"** — use `MSALProvider` (not `MsalAuthProvider`) in layout.tsx
|
|
699
|
+
- **"AADSTS50011: Redirect URI mismatch"** — add your URL to Azure Portal → Authentication → Redirect URIs
|
|
700
|
+
- **"No active account"** — user must sign in before calling `acquireToken()`
|
|
701
|
+
- **Environment variables undefined** — restart the dev server after creating `.env.local`
|
|
832
702
|
|
|
833
|
-
|
|
703
|
+
See [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for more.
|
|
834
704
|
|
|
835
705
|
---
|
|
836
706
|
|
|
837
|
-
##
|
|
707
|
+
## Additional Resources
|
|
838
708
|
|
|
839
|
-
|
|
840
|
-
- [
|
|
841
|
-
- [
|
|
842
|
-
- [
|
|
709
|
+
- [SECURITY.md](./SECURITY.md) — security policy and best practices
|
|
710
|
+
- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) — common issues
|
|
711
|
+
- [CHANGELOG.md](./CHANGELOG.md) — version history
|
|
712
|
+
- [MIGRATION_GUIDE_v3.md](./MIGRATION_GUIDE_v3.md) — migrating from v2.x
|
|
713
|
+
- [npm package](https://www.npmjs.com/package/@chemmangat/msal-next)
|
|
714
|
+
- [GitHub](https://github.com/chemmangat/msal-next)
|
|
715
|
+
- [Report issues](https://github.com/chemmangat/msal-next/issues)
|
|
843
716
|
|
|
844
717
|
---
|
|
845
718
|
|
|
846
|
-
##
|
|
719
|
+
## Contributing
|
|
847
720
|
|
|
848
|
-
|
|
849
|
-
- ⭐ Used in production by developers worldwide
|
|
850
|
-
- 🔒 Security-focused with regular updates
|
|
851
|
-
- 📚 Comprehensive documentation
|
|
852
|
-
- 🎯 TypeScript-first with complete type safety
|
|
721
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
853
722
|
|
|
854
|
-
|
|
723
|
+
## License
|
|
855
724
|
|
|
856
|
-
|
|
725
|
+
MIT © [Chemmangat](https://github.com/chemmangat)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemmangat/msal-next",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "Production-ready Microsoft/Azure AD authentication for Next.js App Router. Zero-config setup, TypeScript-first, multi-account support, auto token refresh. The easiest way to add Microsoft login to your Next.js app.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|