@chemmangat/msal-next 4.2.1 → 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 +454 -708
- 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,544 +42,356 @@ 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:**
|
|
427
|
-
|
|
428
|
-
- **"createContext only works in Client Components"** → Use `MSALProvider` (not `MsalAuthProvider`) in layout.tsx
|
|
429
|
-
- **"AADSTS50011: Redirect URI mismatch"** → Add your URL to Azure Portal → Authentication → Redirect URIs
|
|
430
|
-
- **"No active account"** → User needs to sign in first before calling `acquireToken()`
|
|
431
|
-
- **Environment variables undefined** → Restart dev server after creating `.env.local`
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
## 🎯 Key Features
|
|
77
|
+
### 4. Azure AD setup
|
|
436
78
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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)
|
|
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**
|
|
445
83
|
|
|
446
84
|
---
|
|
447
85
|
|
|
448
|
-
##
|
|
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
|
-
|
|
469
|
-
|
|
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) => {}}
|
|
470
135
|
/>
|
|
471
136
|
```
|
|
472
137
|
|
|
473
138
|
#### SignOutButton
|
|
474
|
-
Pre-styled sign-out button.
|
|
475
139
|
|
|
476
140
|
```tsx
|
|
477
141
|
<SignOutButton
|
|
478
142
|
variant="light"
|
|
479
143
|
size="medium"
|
|
480
|
-
onSuccess={() =>
|
|
481
|
-
onError={(error) =>
|
|
144
|
+
onSuccess={() => {}}
|
|
145
|
+
onError={(error) => {}}
|
|
482
146
|
/>
|
|
483
147
|
```
|
|
484
148
|
|
|
485
149
|
#### AuthGuard
|
|
486
|
-
|
|
150
|
+
|
|
151
|
+
Protects content and redirects unauthenticated users to login.
|
|
487
152
|
|
|
488
153
|
```tsx
|
|
489
154
|
<AuthGuard
|
|
490
155
|
loadingComponent={<div>Loading...</div>}
|
|
491
|
-
fallbackComponent={<div>
|
|
156
|
+
fallbackComponent={<div>Redirecting...</div>}
|
|
157
|
+
scopes={['User.Read']}
|
|
158
|
+
onAuthRequired={() => {}}
|
|
492
159
|
>
|
|
493
160
|
<ProtectedContent />
|
|
494
161
|
</AuthGuard>
|
|
495
162
|
```
|
|
496
163
|
|
|
497
164
|
#### UserAvatar
|
|
498
|
-
Display user photo from Microsoft Graph.
|
|
499
165
|
|
|
500
166
|
```tsx
|
|
501
|
-
<UserAvatar
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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) => {}}
|
|
505
195
|
/>
|
|
506
196
|
```
|
|
507
197
|
|
|
198
|
+
---
|
|
199
|
+
|
|
508
200
|
### Hooks
|
|
509
201
|
|
|
510
202
|
#### useMsalAuth()
|
|
511
|
-
Main authentication hook.
|
|
512
203
|
|
|
513
204
|
```tsx
|
|
514
205
|
const {
|
|
515
|
-
account,
|
|
516
|
-
accounts,
|
|
517
|
-
isAuthenticated,
|
|
518
|
-
inProgress,
|
|
519
|
-
loginRedirect,
|
|
520
|
-
logoutRedirect,
|
|
521
|
-
acquireToken,
|
|
522
|
-
acquireTokenSilent,
|
|
523
|
-
acquireTokenRedirect,
|
|
524
|
-
clearSession,
|
|
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
|
|
525
216
|
} = useMsalAuth();
|
|
526
217
|
```
|
|
527
218
|
|
|
528
219
|
#### useUserProfile()
|
|
529
|
-
|
|
220
|
+
|
|
221
|
+
Fetches user profile from Microsoft Graph `/me` (30+ fields).
|
|
530
222
|
|
|
531
223
|
```tsx
|
|
532
224
|
const {
|
|
533
|
-
profile, //
|
|
534
|
-
loading, //
|
|
535
|
-
error, // Error
|
|
536
|
-
refetch, //
|
|
537
|
-
clearCache, //
|
|
225
|
+
profile, // UserProfile | null
|
|
226
|
+
loading, // boolean
|
|
227
|
+
error, // Error | null
|
|
228
|
+
refetch, // () => Promise<void>
|
|
229
|
+
clearCache, // () => void
|
|
538
230
|
} = useUserProfile();
|
|
539
231
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
console.log(profile?.preferredLanguage);
|
|
544
|
-
console.log(profile?.employeeId);
|
|
232
|
+
// With custom fields
|
|
233
|
+
interface MyProfile extends UserProfile { customField: string }
|
|
234
|
+
const { profile } = useUserProfile<MyProfile>();
|
|
545
235
|
```
|
|
546
236
|
|
|
547
237
|
#### useGraphApi()
|
|
548
|
-
Pre-configured Microsoft Graph API client.
|
|
549
238
|
|
|
550
239
|
```tsx
|
|
551
240
|
const graph = useGraphApi();
|
|
552
241
|
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
// POST request
|
|
557
|
-
const message = await graph.post('/me/messages', {
|
|
558
|
-
subject: 'Hello',
|
|
559
|
-
body: { content: 'World' }
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// PUT, PATCH, DELETE
|
|
563
|
-
await graph.put('/me/photo/$value', photoBlob);
|
|
242
|
+
const user = await graph.get('/me');
|
|
243
|
+
const result = await graph.post('/me/messages', body);
|
|
244
|
+
await graph.put('/me/photo/$value', blob);
|
|
564
245
|
await graph.patch('/me', { displayName: 'New Name' });
|
|
565
246
|
await graph.delete('/me/messages/{id}');
|
|
566
|
-
|
|
567
|
-
// Custom request with options
|
|
568
|
-
const data = await graph.request('/me', { version: 'beta', scopes: ['User.Read'] });
|
|
247
|
+
const data = await graph.request('/me', { version: 'beta' });
|
|
569
248
|
```
|
|
570
249
|
|
|
571
250
|
#### useRoles()
|
|
572
|
-
Access user's Azure AD roles and groups.
|
|
573
251
|
|
|
574
252
|
```tsx
|
|
575
253
|
const {
|
|
576
|
-
roles,
|
|
577
|
-
groups,
|
|
578
|
-
loading,
|
|
579
|
-
error,
|
|
580
|
-
hasRole,
|
|
581
|
-
hasGroup,
|
|
582
|
-
hasAnyRole,
|
|
583
|
-
hasAllRoles,
|
|
584
|
-
refetch,
|
|
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>
|
|
585
263
|
} = useRoles();
|
|
586
|
-
|
|
587
|
-
if (hasRole('Admin')) {
|
|
588
|
-
// Show admin content
|
|
589
|
-
}
|
|
590
264
|
```
|
|
591
265
|
|
|
592
266
|
#### useTokenRefresh()
|
|
593
|
-
|
|
267
|
+
|
|
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.
|
|
594
269
|
|
|
595
270
|
```tsx
|
|
596
|
-
|
|
597
|
-
expiresIn, // Seconds until token expires (null if unknown)
|
|
598
|
-
isExpiringSoon, // Boolean: token expiring within threshold
|
|
599
|
-
refresh, // Function: manually trigger token refresh
|
|
600
|
-
lastRefresh, // Date: when token was last refreshed
|
|
601
|
-
} = useTokenRefresh({
|
|
602
|
-
refreshBeforeExpiry: 300, // seconds before expiry to refresh
|
|
603
|
-
scopes: ['User.Read'],
|
|
604
|
-
onRefresh: (expiresIn) => console.log(`Refreshed, expires in ${expiresIn}s`),
|
|
605
|
-
onError: (error) => console.error(error),
|
|
606
|
-
});
|
|
271
|
+
useTokenRefresh(options?: UseTokenRefreshOptions): UseTokenRefreshReturn
|
|
607
272
|
```
|
|
608
273
|
|
|
609
|
-
|
|
610
|
-
|
|
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 |
|
|
611
292
|
|
|
612
293
|
```tsx
|
|
613
|
-
|
|
614
|
-
accounts, // All signed-in accounts
|
|
615
|
-
activeAccount, // Currently active account
|
|
616
|
-
hasMultipleAccounts, // Boolean: more than one account signed in
|
|
617
|
-
accountCount, // Number of signed-in accounts
|
|
618
|
-
inProgress, // Boolean: interaction in progress
|
|
619
|
-
switchAccount, // Function: switch active account
|
|
620
|
-
addAccount, // Function: sign in with another account
|
|
621
|
-
removeAccount, // Function: remove account from cache
|
|
622
|
-
signOutAccount, // Function: sign out a specific account
|
|
623
|
-
signOutAll, // Function: sign out all accounts
|
|
624
|
-
getAccountByUsername, // Function: find account by username
|
|
625
|
-
getAccountById, // Function: find account by homeAccountId
|
|
626
|
-
isActiveAccount, // Function: check if account is active
|
|
627
|
-
} = useMultiAccount();
|
|
628
|
-
```
|
|
294
|
+
'use client';
|
|
629
295
|
|
|
630
|
-
|
|
296
|
+
import { useTokenRefresh } from '@chemmangat/msal-next';
|
|
631
297
|
|
|
632
|
-
|
|
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
|
+
});
|
|
633
306
|
|
|
634
|
-
|
|
635
|
-
|
|
307
|
+
if (isExpiringSoon) {
|
|
308
|
+
return (
|
|
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>}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### useMultiAccount()
|
|
322
|
+
|
|
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.
|
|
636
324
|
|
|
637
325
|
```tsx
|
|
638
|
-
|
|
639
|
-
showAvatars={true}
|
|
640
|
-
maxAccounts={5}
|
|
641
|
-
variant="default" // "default", "compact", "minimal"
|
|
642
|
-
showAddButton={true}
|
|
643
|
-
showRemoveButton={true}
|
|
644
|
-
onSwitch={(account) => console.log('Switched to', account.name)}
|
|
645
|
-
onAdd={() => console.log('Adding account')}
|
|
646
|
-
onRemove={(account) => console.log('Removed', account.name)}
|
|
647
|
-
/>
|
|
326
|
+
useMultiAccount(defaultScopes?: string[]): UseMultiAccountReturn
|
|
648
327
|
```
|
|
649
328
|
|
|
650
|
-
|
|
651
|
-
|
|
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 |
|
|
652
346
|
|
|
653
347
|
```tsx
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
+
}
|
|
662
395
|
```
|
|
663
396
|
|
|
664
397
|
---
|
|
@@ -666,99 +399,136 @@ Display all signed-in accounts in a list.
|
|
|
666
399
|
### Higher-Order Components
|
|
667
400
|
|
|
668
401
|
#### withAuth
|
|
669
|
-
Protect a component by wrapping it with `AuthGuard`.
|
|
670
402
|
|
|
671
403
|
```tsx
|
|
672
|
-
const ProtectedPage = withAuth(MyPage);
|
|
673
|
-
|
|
674
|
-
// With options
|
|
675
404
|
const ProtectedPage = withAuth(MyPage, {
|
|
676
405
|
loadingComponent: <Spinner />,
|
|
677
|
-
fallbackComponent: <div>Please sign in</div>,
|
|
678
406
|
scopes: ['User.Read'],
|
|
679
407
|
});
|
|
680
408
|
```
|
|
681
409
|
|
|
682
410
|
#### withPageAuth
|
|
683
|
-
|
|
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.
|
|
684
413
|
|
|
685
414
|
```tsx
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
export default ProtectedDashboard;
|
|
415
|
+
withPageAuth<P>(
|
|
416
|
+
Component: ComponentType<P>,
|
|
417
|
+
authConfig: PageAuthConfig,
|
|
418
|
+
globalConfig?: AuthProtectionConfig
|
|
419
|
+
): ComponentType<P>
|
|
692
420
|
```
|
|
693
421
|
|
|
694
|
-
|
|
422
|
+
`PageAuthConfig` options:
|
|
695
423
|
|
|
696
|
-
|
|
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 |
|
|
697
432
|
|
|
698
|
-
|
|
433
|
+
`AuthProtectionConfig` options (global defaults, third argument):
|
|
699
434
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
{children}
|
|
707
|
-
</MSALProvider>
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
**Monitor token expiry:**
|
|
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 |
|
|
711
441
|
|
|
712
442
|
```tsx
|
|
443
|
+
// app/dashboard/page.tsx
|
|
713
444
|
'use client';
|
|
714
445
|
|
|
715
|
-
import {
|
|
446
|
+
import { withPageAuth } from '@chemmangat/msal-next';
|
|
716
447
|
|
|
717
|
-
|
|
718
|
-
|
|
448
|
+
function Dashboard() {
|
|
449
|
+
return <div>Dashboard — only admins and editors can see this</div>;
|
|
450
|
+
}
|
|
719
451
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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',
|
|
727
463
|
}
|
|
464
|
+
);
|
|
465
|
+
```
|
|
728
466
|
|
|
729
|
-
|
|
730
|
-
|
|
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
|
+
});
|
|
731
475
|
```
|
|
732
476
|
|
|
733
|
-
|
|
477
|
+
#### ProtectedPage
|
|
734
478
|
|
|
735
|
-
|
|
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.
|
|
736
480
|
|
|
737
481
|
```tsx
|
|
738
|
-
<
|
|
739
|
-
|
|
740
|
-
|
|
482
|
+
<ProtectedPage
|
|
483
|
+
config={PageAuthConfig}
|
|
484
|
+
defaultRedirectTo="/login"
|
|
485
|
+
defaultLoading={<Spinner />}
|
|
486
|
+
defaultUnauthorized={<div>Access denied</div>}
|
|
487
|
+
debug={false}
|
|
741
488
|
>
|
|
742
489
|
{children}
|
|
743
|
-
</
|
|
490
|
+
</ProtectedPage>
|
|
744
491
|
```
|
|
745
492
|
|
|
746
|
-
|
|
493
|
+
Props (`ProtectedPageProps`):
|
|
747
494
|
|
|
748
|
-
|
|
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 |
|
|
749
503
|
|
|
750
504
|
```tsx
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
}
|
|
757
525
|
```
|
|
758
526
|
|
|
759
|
-
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
### Server Utilities
|
|
760
530
|
|
|
761
|
-
|
|
531
|
+
#### getServerSession
|
|
762
532
|
|
|
763
533
|
```tsx
|
|
764
534
|
// app/profile/page.tsx (Server Component)
|
|
@@ -767,33 +537,48 @@ import { redirect } from 'next/navigation';
|
|
|
767
537
|
|
|
768
538
|
export default async function ProfilePage() {
|
|
769
539
|
const session = await getServerSession();
|
|
540
|
+
if (!session.isAuthenticated) redirect('/login');
|
|
541
|
+
return <div>Welcome, {session.username}</div>;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
770
544
|
|
|
771
|
-
|
|
772
|
-
redirect('/login');
|
|
773
|
-
}
|
|
545
|
+
---
|
|
774
546
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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>
|
|
782
557
|
```
|
|
783
558
|
|
|
784
|
-
|
|
559
|
+
`AuthMiddlewareConfig` options:
|
|
785
560
|
|
|
786
|
-
|
|
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 |
|
|
787
570
|
|
|
788
571
|
```tsx
|
|
789
572
|
// middleware.ts
|
|
790
573
|
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
791
574
|
|
|
792
575
|
export const middleware = createAuthMiddleware({
|
|
793
|
-
protectedRoutes: ['/dashboard', '/profile', '/api/protected'],
|
|
794
|
-
publicOnlyRoutes: ['/login'],
|
|
576
|
+
protectedRoutes: ['/dashboard', '/profile', '/settings', '/api/protected'],
|
|
577
|
+
publicOnlyRoutes: ['/login', '/signup'],
|
|
795
578
|
loginPath: '/login',
|
|
796
|
-
|
|
579
|
+
redirectAfterLogin: '/dashboard',
|
|
580
|
+
sessionCookie: 'msal.account',
|
|
581
|
+
debug: process.env.NODE_ENV === 'development',
|
|
797
582
|
});
|
|
798
583
|
|
|
799
584
|
export const config = {
|
|
@@ -801,179 +586,140 @@ export const config = {
|
|
|
801
586
|
};
|
|
802
587
|
```
|
|
803
588
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
Use enhanced error handling for better debugging:
|
|
589
|
+
Custom `isAuthenticated` — use your own session store:
|
|
807
590
|
|
|
808
591
|
```tsx
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
import {
|
|
592
|
+
// middleware.ts
|
|
593
|
+
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
594
|
+
import { verifySession } from './lib/session';
|
|
812
595
|
|
|
813
|
-
export
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
// Check if user cancelled (not a real error)
|
|
823
|
-
if (msalError.isUserCancellation()) {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Display actionable error message
|
|
828
|
-
console.error(msalError.toConsoleString());
|
|
829
|
-
}
|
|
830
|
-
};
|
|
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
|
+
});
|
|
831
605
|
|
|
832
|
-
|
|
833
|
-
|
|
606
|
+
export const config = {
|
|
607
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
608
|
+
};
|
|
834
609
|
```
|
|
835
610
|
|
|
836
|
-
|
|
611
|
+
---
|
|
837
612
|
|
|
838
|
-
|
|
613
|
+
## Common Patterns
|
|
614
|
+
|
|
615
|
+
### Protect a page with AuthGuard
|
|
839
616
|
|
|
840
617
|
```tsx
|
|
841
618
|
'use client';
|
|
842
619
|
|
|
843
|
-
import {
|
|
620
|
+
import { AuthGuard, useUserProfile } from '@chemmangat/msal-next';
|
|
844
621
|
|
|
845
|
-
|
|
846
|
-
|
|
622
|
+
export default function DashboardPage() {
|
|
623
|
+
return (
|
|
624
|
+
<AuthGuard>
|
|
625
|
+
<DashboardContent />
|
|
626
|
+
</AuthGuard>
|
|
627
|
+
);
|
|
847
628
|
}
|
|
848
629
|
|
|
849
|
-
|
|
850
|
-
const { profile } = useUserProfile
|
|
851
|
-
|
|
630
|
+
function DashboardContent() {
|
|
631
|
+
const { profile, loading } = useUserProfile();
|
|
632
|
+
if (loading) return <div>Loading...</div>;
|
|
852
633
|
return (
|
|
853
634
|
<div>
|
|
854
|
-
<p>
|
|
855
|
-
<p>
|
|
635
|
+
<p>{profile?.displayName}</p>
|
|
636
|
+
<p>{profile?.mail}</p>
|
|
637
|
+
<p>{profile?.department}</p>
|
|
856
638
|
</div>
|
|
857
639
|
);
|
|
858
640
|
}
|
|
859
641
|
```
|
|
860
642
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
## 🔧 Configuration Reference
|
|
864
|
-
|
|
865
|
-
### MSALProvider Props
|
|
643
|
+
### Acquire a token for API calls
|
|
866
644
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
| `clientId` | `string` | ✅ Yes | - | Azure AD Application (client) ID |
|
|
870
|
-
| `tenantId` | `string` | No | - | Azure AD Directory (tenant) ID (for single-tenant) |
|
|
871
|
-
| `authorityType` | `'common' \| 'organizations' \| 'consumers' \| 'tenant'` | No | `'common'` | Authority type |
|
|
872
|
-
| `redirectUri` | `string` | No | `window.location.origin` | Redirect URI after authentication |
|
|
873
|
-
| `postLogoutRedirectUri` | `string` | No | `redirectUri` | Redirect URI after logout |
|
|
874
|
-
| `scopes` | `string[]` | No | `['User.Read']` | Default scopes |
|
|
875
|
-
| `cacheLocation` | `'sessionStorage' \| 'localStorage' \| 'memoryStorage'` | No | `'sessionStorage'` | Token cache location |
|
|
876
|
-
| `enableLogging` | `boolean` | No | `false` | Enable debug logging |
|
|
877
|
-
| `autoRefreshToken` | `boolean` | No | `false` | Automatically refresh tokens before expiry |
|
|
878
|
-
| `refreshBeforeExpiry` | `number` | No | `300` | Seconds before expiry to refresh token |
|
|
879
|
-
| `allowedRedirectUris` | `string[]` | No | - | Whitelist of allowed redirect URIs |
|
|
880
|
-
| `protection` | `AuthProtectionConfig` | No | - | Zero-config protected routes configuration |
|
|
881
|
-
|
|
882
|
-
### Authority Types
|
|
883
|
-
|
|
884
|
-
- **`common`** - Multi-tenant (any Azure AD tenant or Microsoft account)
|
|
885
|
-
- **`organizations`** - Any organizational Azure AD tenant
|
|
886
|
-
- **`consumers`** - Microsoft personal accounts only
|
|
887
|
-
- **`tenant`** - Single-tenant (requires `tenantId`)
|
|
888
|
-
|
|
889
|
-
---
|
|
890
|
-
|
|
891
|
-
## 📖 Additional Resources
|
|
892
|
-
|
|
893
|
-
### Documentation
|
|
894
|
-
- [SECURITY.md](./SECURITY.md) - **Security policy and best practices** ⭐
|
|
895
|
-
- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) - Common issues and solutions
|
|
896
|
-
- [CHANGELOG.md](./CHANGELOG.md) - Version history
|
|
897
|
-
- [MIGRATION_GUIDE_v3.md](./MIGRATION_GUIDE_v3.md) - Migrating from v2.x
|
|
898
|
-
- [EXAMPLES_v4.0.2.md](./EXAMPLES_v4.0.2.md) - Code examples
|
|
899
|
-
|
|
900
|
-
### Security
|
|
901
|
-
- 🔒 [Security Policy](./SECURITY.md) - Complete security documentation
|
|
902
|
-
- 🛡️ [Best Practices](./SECURITY.md#-best-practices) - Security guidelines
|
|
903
|
-
- ⚠️ [Common Mistakes](./SECURITY.md#-common-security-mistakes) - What to avoid
|
|
904
|
-
- ✅ [Security Checklist](./SECURITY.md#-security-checklist) - Pre-deployment checklist
|
|
905
|
-
|
|
906
|
-
### Links
|
|
907
|
-
- 📦 [npm Package](https://www.npmjs.com/package/@chemmangat/msal-next)
|
|
908
|
-
- 🚀 [Live Demo](https://github.com/Chemmangat/msal-next-demo) - Sample implementation
|
|
909
|
-
- 🐛 [Report Issues](https://github.com/chemmangat/msal-next/issues)
|
|
910
|
-
- 💬 [Discussions](https://github.com/chemmangat/msal-next/discussions)
|
|
911
|
-
- ⭐ [GitHub Repository](https://github.com/chemmangat/msal-next)
|
|
912
|
-
|
|
913
|
-
### Microsoft Resources
|
|
914
|
-
- [Azure AD Documentation](https://learn.microsoft.com/en-us/azure/active-directory/)
|
|
915
|
-
- [MSAL.js Documentation](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview)
|
|
916
|
-
- [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview)
|
|
917
|
-
|
|
918
|
-
---
|
|
645
|
+
```tsx
|
|
646
|
+
'use client';
|
|
919
647
|
|
|
920
|
-
|
|
648
|
+
import { useMsalAuth } from '@chemmangat/msal-next';
|
|
921
649
|
|
|
922
|
-
|
|
923
|
-
|
|
650
|
+
export default function DataComponent() {
|
|
651
|
+
const { acquireToken, isAuthenticated } = useMsalAuth();
|
|
924
652
|
|
|
925
|
-
|
|
926
|
-
|
|
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
|
+
```
|
|
927
663
|
|
|
928
|
-
|
|
929
|
-
A: Yes, the package is MIT licensed and free. Azure AD has a free tier for up to 50,000 users.
|
|
664
|
+
### Error handling
|
|
930
665
|
|
|
931
|
-
|
|
932
|
-
|
|
666
|
+
```tsx
|
|
667
|
+
import { useMsalAuth, wrapMsalError } from '@chemmangat/msal-next';
|
|
933
668
|
|
|
934
|
-
|
|
935
|
-
A: Use `useUserProfile()` hook which provides 30+ fields from Microsoft Graph.
|
|
669
|
+
const { loginRedirect } = useMsalAuth();
|
|
936
670
|
|
|
937
|
-
|
|
938
|
-
|
|
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
|
+
```
|
|
939
681
|
|
|
940
|
-
|
|
941
|
-
A: This package is designed for Azure AD. For B2C, you may need additional configuration.
|
|
682
|
+
### Automatic token refresh
|
|
942
683
|
|
|
943
|
-
|
|
944
|
-
|
|
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
|
+
```
|
|
945
693
|
|
|
946
694
|
---
|
|
947
695
|
|
|
948
|
-
##
|
|
696
|
+
## Troubleshooting
|
|
949
697
|
|
|
950
|
-
|
|
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`
|
|
951
702
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
## 📄 License
|
|
955
|
-
|
|
956
|
-
MIT © [Chemmangat](https://github.com/chemmangat)
|
|
703
|
+
See [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for more.
|
|
957
704
|
|
|
958
705
|
---
|
|
959
706
|
|
|
960
|
-
##
|
|
707
|
+
## Additional Resources
|
|
961
708
|
|
|
962
|
-
|
|
963
|
-
- [
|
|
964
|
-
- [
|
|
965
|
-
- [
|
|
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)
|
|
966
716
|
|
|
967
717
|
---
|
|
968
718
|
|
|
969
|
-
##
|
|
719
|
+
## Contributing
|
|
970
720
|
|
|
971
|
-
|
|
972
|
-
- ⭐ Used in production by developers worldwide
|
|
973
|
-
- 🔒 Security-focused with regular updates
|
|
974
|
-
- 📚 Comprehensive documentation
|
|
975
|
-
- 🎯 TypeScript-first with complete type safety
|
|
721
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
976
722
|
|
|
977
|
-
|
|
723
|
+
## License
|
|
978
724
|
|
|
979
|
-
|
|
725
|
+
MIT © [Chemmangat](https://github.com/chemmangat)
|