@chemmangat/msal-next 4.0.2 โ 4.1.1
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/CHANGELOG.md +198 -0
- package/README.md +565 -726
- package/SECURITY.md +422 -110
- package/dist/index.d.mts +124 -5
- package/dist/index.d.ts +124 -5
- package/dist/index.js +2302 -43
- package/dist/index.mjs +2240 -43
- package/dist/server.js +89 -1
- package/dist/server.mjs +86 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,313 +4,231 @@ Production-grade MSAL authentication library for Next.js App Router with minimal
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@chemmangat/msal-next)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](./SECURITY.md)
|
|
7
8
|
|
|
8
|
-
> **๐ฆ Current Version: 4.
|
|
9
|
+
> **๐ฆ Current Version: 4.1.1** - Production-ready with automatic token refresh and enhanced security
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## ๐ What's New in v4.0.2
|
|
13
|
-
|
|
14
|
-
### Complete TypeScript Types
|
|
15
|
-
```tsx
|
|
16
|
-
const { profile } = useUserProfile();
|
|
17
|
-
|
|
18
|
-
// Now available with full type safety!
|
|
19
|
-
console.log(profile?.department);
|
|
20
|
-
console.log(profile?.preferredLanguage);
|
|
21
|
-
console.log(profile?.employeeId);
|
|
22
|
-
console.log(profile?.companyName);
|
|
23
|
-
console.log(profile?.country);
|
|
24
|
-
// ... and 20+ more fields!
|
|
25
|
-
```
|
|
11
|
+
---
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
```tsx
|
|
29
|
-
// Before: Cryptic error
|
|
30
|
-
Error: AADSTS50011
|
|
13
|
+
## โญ Why Choose @chemmangat/msal-next?
|
|
31
14
|
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
### ๐ **5-Minute Setup**
|
|
16
|
+
Get Azure AD authentication running in your Next.js app in just 5 minutes. No complex configuration, no boilerplate.
|
|
34
17
|
|
|
35
|
-
|
|
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)
|
|
36
20
|
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
### ๐ฏ **Production-Ready**
|
|
22
|
+
Used by 2,200+ developers in production. Automatic token refresh prevents unexpected logouts. Complete TypeScript support.
|
|
39
23
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
2. Select your app โ Authentication
|
|
43
|
-
3. Under "Single-page application", add your redirect URI:
|
|
44
|
-
โข http://localhost:3000 (for development)
|
|
45
|
-
โข https://yourdomain.com (for production)
|
|
46
|
-
4. Click "Save"
|
|
24
|
+
### ๐ค **AI-Friendly**
|
|
25
|
+
Complete documentation optimized for AI assistants. Setup instructions that work on the first try.
|
|
47
26
|
|
|
48
|
-
|
|
27
|
+
### โก **Zero Boilerplate**
|
|
28
|
+
```tsx
|
|
29
|
+
<MSALProvider clientId="...">
|
|
30
|
+
<MicrosoftSignInButton />
|
|
31
|
+
</MSALProvider>
|
|
49
32
|
```
|
|
33
|
+
That's it. You're done.
|
|
50
34
|
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
// Development mode automatically checks for:
|
|
54
|
-
โ ๏ธ Warnings (should fix)
|
|
35
|
+
---
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
Client ID appears to be a placeholder
|
|
37
|
+
## ๐ฏ Top Features
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 |
|
|
65
49
|
|
|
66
50
|
---
|
|
67
51
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
### โ Mistake #1: 'use client' in the wrong place
|
|
52
|
+
## ๐ Security First
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
61
|
|
|
76
|
-
|
|
62
|
+
**[Read Complete Security Policy โ](./SECURITY.md)**
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
const { isAuthenticated } = useMsalAuth();
|
|
80
|
-
// ...
|
|
81
|
-
}
|
|
82
|
-
```
|
|
64
|
+
---
|
|
83
65
|
|
|
84
|
-
|
|
85
|
-
// CORRECT - 'use client' comes FIRST
|
|
86
|
-
'use client';
|
|
66
|
+
## ๐ Quick Start (5 Minutes)
|
|
87
67
|
|
|
88
|
-
|
|
68
|
+
### Step 1: Install the Package
|
|
89
69
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// ...
|
|
93
|
-
}
|
|
70
|
+
```bash
|
|
71
|
+
npm install @chemmangat/msal-next @azure/msal-browser @azure/msal-react
|
|
94
72
|
```
|
|
95
73
|
|
|
96
|
-
###
|
|
74
|
+
### Step 2: Get Your Azure AD Credentials
|
|
97
75
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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**
|
|
101
84
|
|
|
102
|
-
|
|
103
|
-
return <MsalAuthProvider>{children}</MsalAuthProvider>;
|
|
104
|
-
}
|
|
105
|
-
```
|
|
85
|
+
### Step 3: Configure Environment Variables
|
|
106
86
|
|
|
107
|
-
|
|
108
|
-
// CORRECT - Use MSALProvider instead
|
|
109
|
-
import { MSALProvider } from '@chemmangat/msal-next';
|
|
87
|
+
Create `.env.local` in your project root:
|
|
110
88
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
93
|
```
|
|
115
94
|
|
|
116
|
-
|
|
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)
|
|
117
99
|
|
|
118
|
-
|
|
119
|
-
// WRONG - Placeholder values
|
|
120
|
-
<MSALProvider
|
|
121
|
-
clientId="your-client-id-here"
|
|
122
|
-
tenantId="your-tenant-id-here"
|
|
123
|
-
>
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
// CORRECT - Actual GUIDs from Azure Portal
|
|
128
|
-
<MSALProvider
|
|
129
|
-
clientId="12345678-1234-1234-1234-123456789012"
|
|
130
|
-
tenantId="87654321-4321-4321-4321-210987654321"
|
|
131
|
-
>
|
|
132
|
-
```
|
|
100
|
+
**Security Note:** All authentication happens in the browser. Your tokens never touch your Next.js server. [Learn more โ](./SECURITY.md)
|
|
133
101
|
|
|
134
|
-
###
|
|
102
|
+
### Step 4: Add Provider to Layout
|
|
135
103
|
|
|
136
104
|
```tsx
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
```
|
|
105
|
+
// app/layout.tsx (Server Component - no 'use client' needed!)
|
|
106
|
+
import { MSALProvider } from '@chemmangat/msal-next';
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
108
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
109
|
+
return (
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<body>
|
|
112
|
+
<MSALProvider
|
|
113
|
+
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
114
|
+
tenantId={process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID!}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</MSALProvider>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
147
122
|
```
|
|
148
123
|
|
|
149
|
-
|
|
150
|
-
# .env.local
|
|
151
|
-
NEXT_PUBLIC_AZURE_AD_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
|
152
|
-
NEXT_PUBLIC_AZURE_AD_TENANT_ID=87654321-4321-4321-4321-210987654321
|
|
153
|
-
```
|
|
124
|
+
**Note:** MSALProvider is already marked as 'use client' internally. You don't need to add 'use client' to your layout.tsx!
|
|
154
125
|
|
|
155
|
-
###
|
|
126
|
+
### Step 5: Add Sign-In Button
|
|
156
127
|
|
|
157
128
|
```tsx
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// CORRECT - HTTPS in production, HTTP only for localhost
|
|
162
|
-
redirectUri: process.env.NODE_ENV === 'production'
|
|
163
|
-
? "https://myapp.com"
|
|
164
|
-
: "http://localhost:3000"
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## ๐ What's New in v4.0.1
|
|
129
|
+
// app/page.tsx
|
|
130
|
+
'use client';
|
|
170
131
|
|
|
171
|
-
|
|
132
|
+
import { MicrosoftSignInButton, useMsalAuth } from '@chemmangat/msal-next';
|
|
172
133
|
|
|
173
|
-
|
|
134
|
+
export default function HomePage() {
|
|
135
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
174
136
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
137
|
+
if (isAuthenticated) {
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
<h1>Welcome, {account?.name}!</h1>
|
|
141
|
+
<p>You are signed in as {account?.username}</p>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
178
145
|
|
|
179
|
-
|
|
180
|
-
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<h1>Welcome to My App</h1>
|
|
149
|
+
<MicrosoftSignInButton />
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
181
152
|
}
|
|
182
153
|
```
|
|
183
154
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
| Before (v3.x) | After (v4.0) |
|
|
187
|
-
|---------------|--------------|
|
|
188
|
-
| 50+ lines of middleware | 1 line |
|
|
189
|
-
| Manual redirect logic | Automatic |
|
|
190
|
-
| Boilerplate in every page | Zero boilerplate |
|
|
191
|
-
| 30 min setup | 30 sec setup |
|
|
192
|
-
|
|
193
|
-
### More Examples
|
|
155
|
+
### Step 6: Run Your App
|
|
194
156
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
export const auth = {
|
|
198
|
-
required: true,
|
|
199
|
-
roles: ['admin', 'editor']
|
|
200
|
-
};
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Custom Validation:**
|
|
204
|
-
```tsx
|
|
205
|
-
export const auth = {
|
|
206
|
-
required: true,
|
|
207
|
-
validate: (account) => account.username.endsWith('@company.com')
|
|
208
|
-
};
|
|
157
|
+
```bash
|
|
158
|
+
npm run dev
|
|
209
159
|
```
|
|
210
160
|
|
|
211
|
-
|
|
212
|
-
```tsx
|
|
213
|
-
export const auth = {
|
|
214
|
-
required: true,
|
|
215
|
-
loading: <Spinner />,
|
|
216
|
-
unauthorized: <AccessDenied />
|
|
217
|
-
};
|
|
218
|
-
```
|
|
161
|
+
Visit `http://localhost:3000` and click the sign-in button!
|
|
219
162
|
|
|
220
163
|
---
|
|
221
164
|
|
|
222
|
-
##
|
|
223
|
-
|
|
224
|
-
โจ **Zero-Config Protection** - One line to protect any route
|
|
225
|
-
๐ฏ **Role-Based Access** - Built-in Azure AD role checking
|
|
226
|
-
๐ **Custom Validation** - Add your own auth logic
|
|
227
|
-
โก **Automatic Redirects** - Smart return URL handling
|
|
228
|
-
๐จ **Custom UI** - Override loading/unauthorized states
|
|
229
|
-
๐ฆ **TypeScript First** - Full type safety
|
|
230
|
-
๐ **Next.js 14+** - Built for App Router
|
|
165
|
+
## ๐ Complete Setup Guide for AI Assistants
|
|
231
166
|
|
|
232
|
-
|
|
167
|
+
When implementing MSAL authentication in a Next.js project, follow these steps:
|
|
233
168
|
|
|
234
|
-
|
|
169
|
+
### 1. Installation
|
|
235
170
|
|
|
236
|
-
### ๐ CLI Tool (NEW)
|
|
237
171
|
```bash
|
|
238
|
-
|
|
239
|
-
npx @chemmangat/msal-next init
|
|
172
|
+
npm install @chemmangat/msal-next @azure/msal-browser@^4.0.0 @azure/msal-react@^3.0.0
|
|
240
173
|
```
|
|
241
174
|
|
|
242
|
-
|
|
243
|
-
- Detects your Next.js structure (App Router/Pages Router)
|
|
244
|
-
- Installs dependencies
|
|
245
|
-
- Creates configuration files
|
|
246
|
-
- Generates example pages
|
|
247
|
-
- Sets up middleware
|
|
175
|
+
### 2. Azure AD Configuration
|
|
248
176
|
|
|
249
|
-
**
|
|
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)
|
|
250
182
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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`
|
|
254
186
|
|
|
255
|
-
|
|
256
|
-
enabled: true,
|
|
257
|
-
enablePerformance: true, // Track operation timing
|
|
258
|
-
enableNetworkLogs: true, // Log all requests/responses
|
|
259
|
-
});
|
|
187
|
+
### 3. Environment Variables
|
|
260
188
|
|
|
261
|
-
|
|
262
|
-
logger.startTiming('token-acquisition');
|
|
263
|
-
await acquireToken(['User.Read']);
|
|
264
|
-
logger.endTiming('token-acquisition'); // Logs: "โฑ๏ธ Completed: token-acquisition (45ms)"
|
|
189
|
+
Create `.env.local`:
|
|
265
190
|
|
|
266
|
-
|
|
267
|
-
|
|
191
|
+
```bash
|
|
192
|
+
NEXT_PUBLIC_AZURE_AD_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
|
193
|
+
NEXT_PUBLIC_AZURE_AD_TENANT_ID=87654321-4321-4321-4321-210987654321
|
|
268
194
|
```
|
|
269
195
|
|
|
270
|
-
|
|
271
|
-
-
|
|
272
|
-
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- Requires Node.js 18+ (was 16+)
|
|
276
|
-
- Requires Next.js 14.1+ (was 14.0+)
|
|
277
|
-
- Requires @azure/msal-browser v4+ (was v3+)
|
|
278
|
-
- Removed `ServerSession.accessToken` (use client-side `acquireToken()`)
|
|
279
|
-
|
|
280
|
-
[See Migration Guide](./MIGRATION_GUIDE_v3.md) for details.
|
|
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
|
|
281
201
|
|
|
282
|
-
|
|
202
|
+
### 4. Project Structure
|
|
283
203
|
|
|
284
|
-
### Option 1: CLI Setup (Recommended)
|
|
285
|
-
```bash
|
|
286
|
-
# Create Next.js app
|
|
287
|
-
npx create-next-app@latest my-app
|
|
288
|
-
cd my-app
|
|
289
|
-
|
|
290
|
-
# Initialize MSAL
|
|
291
|
-
npx @chemmangat/msal-next init
|
|
292
204
|
```
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
297
213
|
```
|
|
298
214
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
> **Important:** Use `MSALProvider` (not `MsalAuthProvider`) in your layout.tsx to avoid the "createContext only works in Client Components" error.
|
|
215
|
+
### 5. Implementation Files
|
|
302
216
|
|
|
303
|
-
|
|
217
|
+
**File 1: `app/layout.tsx` (Server Component)**
|
|
304
218
|
|
|
305
219
|
```tsx
|
|
306
|
-
// app/layout.tsx
|
|
307
220
|
import { MSALProvider } from '@chemmangat/msal-next';
|
|
221
|
+
import './globals.css';
|
|
308
222
|
|
|
309
|
-
export default function RootLayout({
|
|
223
|
+
export default function RootLayout({
|
|
224
|
+
children,
|
|
225
|
+
}: {
|
|
226
|
+
children: React.ReactNode;
|
|
227
|
+
}) {
|
|
310
228
|
return (
|
|
311
|
-
<html>
|
|
229
|
+
<html lang="en">
|
|
312
230
|
<body>
|
|
313
|
-
<MSALProvider
|
|
231
|
+
<MSALProvider
|
|
314
232
|
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
315
233
|
tenantId={process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID!}
|
|
316
234
|
>
|
|
@@ -322,94 +240,237 @@ export default function RootLayout({ children }) {
|
|
|
322
240
|
}
|
|
323
241
|
```
|
|
324
242
|
|
|
325
|
-
|
|
243
|
+
**File 2: `app/page.tsx` (Client Component)**
|
|
326
244
|
|
|
327
245
|
```tsx
|
|
328
|
-
// app/page.tsx
|
|
329
246
|
'use client';
|
|
330
247
|
|
|
331
|
-
import { MicrosoftSignInButton, useMsalAuth } from '@chemmangat/msal-next';
|
|
248
|
+
import { MicrosoftSignInButton, SignOutButton, useMsalAuth } from '@chemmangat/msal-next';
|
|
332
249
|
|
|
333
|
-
export default function
|
|
334
|
-
const { isAuthenticated } = useMsalAuth();
|
|
250
|
+
export default function HomePage() {
|
|
251
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
335
252
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
);
|
|
341
271
|
}
|
|
342
272
|
```
|
|
343
273
|
|
|
344
|
-
|
|
274
|
+
**File 3: `app/dashboard/page.tsx` (Protected Page)**
|
|
345
275
|
|
|
346
|
-
|
|
276
|
+
```tsx
|
|
277
|
+
'use client';
|
|
347
278
|
|
|
348
|
-
|
|
279
|
+
import { AuthGuard, useUserProfile } from '@chemmangat/msal-next';
|
|
349
280
|
|
|
350
|
-
|
|
281
|
+
export default function DashboardPage() {
|
|
282
|
+
return (
|
|
283
|
+
<AuthGuard>
|
|
284
|
+
<DashboardContent />
|
|
285
|
+
</AuthGuard>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
351
288
|
|
|
352
|
-
|
|
289
|
+
function DashboardContent() {
|
|
290
|
+
const { profile, loading } = useUserProfile();
|
|
353
291
|
|
|
354
|
-
|
|
355
|
-
// app/layout.tsx (Server Component)
|
|
356
|
-
import { MSALProvider } from '@chemmangat/msal-next';
|
|
292
|
+
if (loading) return <div>Loading profile...</div>;
|
|
357
293
|
|
|
358
|
-
export default function RootLayout({ children }) {
|
|
359
294
|
return (
|
|
360
|
-
<
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
>
|
|
368
|
-
{children}
|
|
369
|
-
</MSALProvider>
|
|
370
|
-
</body>
|
|
371
|
-
</html>
|
|
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>
|
|
372
302
|
);
|
|
373
303
|
}
|
|
374
304
|
```
|
|
375
305
|
|
|
376
|
-
###
|
|
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**
|
|
377
326
|
|
|
378
|
-
|
|
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**
|
|
379
363
|
|
|
380
364
|
```tsx
|
|
381
|
-
|
|
382
|
-
'use client'
|
|
365
|
+
'use client';
|
|
383
366
|
|
|
384
|
-
import {
|
|
367
|
+
import { AuthGuard } from '@chemmangat/msal-next';
|
|
385
368
|
|
|
386
|
-
export function
|
|
369
|
+
export default function ProtectedPage() {
|
|
387
370
|
return (
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
371
|
+
<AuthGuard
|
|
372
|
+
loadingComponent={<div>Checking authentication...</div>}
|
|
373
|
+
fallbackComponent={<div>Redirecting to login...</div>}
|
|
391
374
|
>
|
|
392
|
-
|
|
393
|
-
</
|
|
375
|
+
<div>This content is protected</div>
|
|
376
|
+
</AuthGuard>
|
|
394
377
|
);
|
|
395
378
|
}
|
|
396
379
|
```
|
|
397
380
|
|
|
398
|
-
###
|
|
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>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### 8. Troubleshooting Checklist
|
|
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
|
|
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
|
|
399
449
|
|
|
400
|
-
|
|
450
|
+
### Components
|
|
451
|
+
|
|
452
|
+
#### MSALProvider
|
|
453
|
+
Wrap your app to provide authentication context.
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
<MSALProvider clientId="..." tenantId="...">
|
|
457
|
+
{children}
|
|
458
|
+
</MSALProvider>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### MicrosoftSignInButton
|
|
462
|
+
Pre-styled sign-in button with Microsoft branding.
|
|
401
463
|
|
|
402
464
|
```tsx
|
|
403
465
|
<MicrosoftSignInButton
|
|
404
466
|
variant="dark" // or "light"
|
|
405
|
-
size="medium"
|
|
467
|
+
size="medium" // "small", "medium", "large"
|
|
406
468
|
onSuccess={() => console.log('Signed in!')}
|
|
407
469
|
/>
|
|
408
470
|
```
|
|
409
471
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
Pre-styled sign-out button matching the sign-in button style.
|
|
472
|
+
#### SignOutButton
|
|
473
|
+
Pre-styled sign-out button.
|
|
413
474
|
|
|
414
475
|
```tsx
|
|
415
476
|
<SignOutButton
|
|
@@ -419,22 +480,20 @@ Pre-styled sign-out button matching the sign-in button style.
|
|
|
419
480
|
/>
|
|
420
481
|
```
|
|
421
482
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
Protect pages/components that require authentication.
|
|
483
|
+
#### AuthGuard
|
|
484
|
+
Protect components that require authentication.
|
|
425
485
|
|
|
426
486
|
```tsx
|
|
427
487
|
<AuthGuard
|
|
428
488
|
loadingComponent={<div>Loading...</div>}
|
|
429
|
-
fallbackComponent={<div>
|
|
489
|
+
fallbackComponent={<div>Please sign in</div>}
|
|
430
490
|
>
|
|
431
491
|
<ProtectedContent />
|
|
432
492
|
</AuthGuard>
|
|
433
493
|
```
|
|
434
494
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
Display user photo from MS Graph with fallback initials.
|
|
495
|
+
#### UserAvatar
|
|
496
|
+
Display user photo from Microsoft Graph.
|
|
438
497
|
|
|
439
498
|
```tsx
|
|
440
499
|
<UserAvatar
|
|
@@ -444,65 +503,44 @@ Display user photo from MS Graph with fallback initials.
|
|
|
444
503
|
/>
|
|
445
504
|
```
|
|
446
505
|
|
|
447
|
-
###
|
|
448
|
-
|
|
449
|
-
Show current authentication state.
|
|
450
|
-
|
|
451
|
-
```tsx
|
|
452
|
-
<AuthStatus
|
|
453
|
-
showDetails={true}
|
|
454
|
-
renderAuthenticated={(username) => (
|
|
455
|
-
<div>Logged in as {username}</div>
|
|
456
|
-
)}
|
|
457
|
-
/>
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
### ErrorBoundary
|
|
506
|
+
### Hooks
|
|
461
507
|
|
|
462
|
-
|
|
508
|
+
#### useMsalAuth()
|
|
509
|
+
Main authentication hook.
|
|
463
510
|
|
|
464
511
|
```tsx
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
</ErrorBoundary>
|
|
512
|
+
const {
|
|
513
|
+
account, // Current user account
|
|
514
|
+
accounts, // All cached accounts
|
|
515
|
+
isAuthenticated, // Boolean: is user signed in?
|
|
516
|
+
inProgress, // Boolean: is auth in progress?
|
|
517
|
+
loginRedirect, // Function: sign in
|
|
518
|
+
logoutRedirect, // Function: sign out
|
|
519
|
+
acquireToken, // Function: get access token
|
|
520
|
+
} = useMsalAuth();
|
|
475
521
|
```
|
|
476
522
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
### useMsalAuth
|
|
480
|
-
|
|
481
|
-
Main authentication hook with all auth operations.
|
|
523
|
+
#### useUserProfile()
|
|
524
|
+
Fetch user profile from Microsoft Graph.
|
|
482
525
|
|
|
483
526
|
```tsx
|
|
484
527
|
const {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const token = await acquireToken(['User.Read']);
|
|
498
|
-
|
|
499
|
-
// Logout (redirects to Microsoft)
|
|
500
|
-
await logoutRedirect();
|
|
528
|
+
profile, // User profile with 30+ fields
|
|
529
|
+
loading, // Boolean: is loading?
|
|
530
|
+
error, // Error object if failed
|
|
531
|
+
refetch, // Function: refetch profile
|
|
532
|
+
clearCache, // Function: clear cached profile
|
|
533
|
+
} = useUserProfile();
|
|
534
|
+
|
|
535
|
+
// Access profile fields
|
|
536
|
+
console.log(profile?.displayName);
|
|
537
|
+
console.log(profile?.department);
|
|
538
|
+
console.log(profile?.preferredLanguage);
|
|
539
|
+
console.log(profile?.employeeId);
|
|
501
540
|
```
|
|
502
541
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
Pre-configured fetch wrapper for MS Graph API.
|
|
542
|
+
#### useGraphApi()
|
|
543
|
+
Pre-configured Microsoft Graph API client.
|
|
506
544
|
|
|
507
545
|
```tsx
|
|
508
546
|
const graph = useGraphApi();
|
|
@@ -515,503 +553,304 @@ const message = await graph.post('/me/messages', {
|
|
|
515
553
|
subject: 'Hello',
|
|
516
554
|
body: { content: 'World' }
|
|
517
555
|
});
|
|
518
|
-
|
|
519
|
-
// Custom request
|
|
520
|
-
const data = await graph.request('/me/drive', {
|
|
521
|
-
scopes: ['Files.Read'],
|
|
522
|
-
version: 'beta'
|
|
523
|
-
});
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### useUserProfile
|
|
527
|
-
|
|
528
|
-
Fetch and cache user profile from MS Graph with complete TypeScript types.
|
|
529
|
-
|
|
530
|
-
```tsx
|
|
531
|
-
const { profile, loading, error, refetch } = useUserProfile();
|
|
532
|
-
|
|
533
|
-
if (loading) return <div>Loading...</div>;
|
|
534
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
535
|
-
|
|
536
|
-
return (
|
|
537
|
-
<div>
|
|
538
|
-
<h1>{profile.displayName}</h1>
|
|
539
|
-
<p>{profile.mail}</p>
|
|
540
|
-
<p>{profile.jobTitle}</p>
|
|
541
|
-
<p>{profile.department}</p>
|
|
542
|
-
<p>{profile.preferredLanguage}</p>
|
|
543
|
-
<p>{profile.employeeId}</p>
|
|
544
|
-
<p>{profile.companyName}</p>
|
|
545
|
-
<p>{profile.officeLocation}</p>
|
|
546
|
-
</div>
|
|
547
|
-
);
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
**New in v4.0.2:** Complete TypeScript types with 30+ fields from Microsoft Graph:
|
|
551
|
-
- `department`, `preferredLanguage`, `employeeId`, `companyName`
|
|
552
|
-
- `country`, `city`, `state`, `streetAddress`, `postalCode`
|
|
553
|
-
- `manager`, `aboutMe`, `birthday`, `interests`, `skills`
|
|
554
|
-
- And many more!
|
|
555
|
-
|
|
556
|
-
**Generic type support:**
|
|
557
|
-
```tsx
|
|
558
|
-
interface MyProfile extends UserProfile {
|
|
559
|
-
customField: string;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const { profile } = useUserProfile<MyProfile>();
|
|
563
|
-
console.log(profile?.customField); // Type-safe!
|
|
564
556
|
```
|
|
565
557
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
Access user's Azure AD roles and groups.
|
|
558
|
+
#### useRoles()
|
|
559
|
+
Access user's Azure AD roles.
|
|
569
560
|
|
|
570
561
|
```tsx
|
|
571
|
-
const {
|
|
562
|
+
const {
|
|
563
|
+
roles, // Array of role names
|
|
564
|
+
groups, // Array of group IDs
|
|
565
|
+
hasRole, // Function: check single role
|
|
566
|
+
hasAnyRole, // Function: check multiple roles
|
|
567
|
+
hasAllRoles, // Function: check all roles
|
|
568
|
+
} = useRoles();
|
|
572
569
|
|
|
573
570
|
if (hasRole('Admin')) {
|
|
574
|
-
|
|
571
|
+
// Show admin content
|
|
575
572
|
}
|
|
576
|
-
|
|
577
|
-
if (hasAnyRole(['Editor', 'Contributor'])) {
|
|
578
|
-
return <EditorPanel />;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return <ViewerPanel />;
|
|
582
573
|
```
|
|
583
574
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
### withAuth
|
|
587
|
-
|
|
588
|
-
Higher-order component for protecting pages.
|
|
589
|
-
|
|
590
|
-
```tsx
|
|
591
|
-
const ProtectedPage = withAuth(MyPage, {
|
|
592
|
-
useRedirect: true,
|
|
593
|
-
scopes: ['User.Read']
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
export default ProtectedPage;
|
|
597
|
-
```
|
|
575
|
+
---
|
|
598
576
|
|
|
599
|
-
|
|
577
|
+
## ๐ Advanced Usage
|
|
600
578
|
|
|
601
|
-
|
|
579
|
+
### Automatic Token Refresh (NEW in v4.1.0)
|
|
602
580
|
|
|
603
|
-
|
|
581
|
+
Prevent unexpected logouts by automatically refreshing tokens before they expire:
|
|
604
582
|
|
|
605
583
|
```tsx
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (!session.isAuthenticated) {
|
|
614
|
-
redirect('/login');
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return <div>Welcome {session.username}</div>;
|
|
618
|
-
}
|
|
584
|
+
<MSALProvider
|
|
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>
|
|
619
591
|
```
|
|
620
592
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
Protect routes at the edge with middleware.
|
|
593
|
+
**Monitor token expiry:**
|
|
624
594
|
|
|
625
595
|
```tsx
|
|
626
|
-
|
|
627
|
-
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
628
|
-
|
|
629
|
-
export const middleware = createAuthMiddleware({
|
|
630
|
-
protectedRoutes: ['/dashboard', '/profile', '/api/protected'],
|
|
631
|
-
publicOnlyRoutes: ['/login'],
|
|
632
|
-
loginPath: '/login',
|
|
633
|
-
debug: true,
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
export const config = {
|
|
637
|
-
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
638
|
-
};
|
|
639
|
-
```
|
|
596
|
+
'use client';
|
|
640
597
|
|
|
641
|
-
|
|
598
|
+
import { useTokenRefresh } from '@chemmangat/msal-next';
|
|
642
599
|
|
|
643
|
-
|
|
600
|
+
export default function SessionWarning() {
|
|
601
|
+
const { expiresIn, isExpiringSoon } = useTokenRefresh();
|
|
644
602
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
{
|
|
652
|
-
maxRetries: 3,
|
|
653
|
-
initialDelay: 1000,
|
|
654
|
-
backoffMultiplier: 2,
|
|
655
|
-
debug: true
|
|
603
|
+
if (isExpiringSoon) {
|
|
604
|
+
return (
|
|
605
|
+
<div className="warning">
|
|
606
|
+
โ ๏ธ Your session will expire in {Math.floor(expiresIn / 60)} minutes
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
656
609
|
}
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
// Create a reusable retry wrapper
|
|
660
|
-
const acquireTokenWithRetry = createRetryWrapper(acquireToken, {
|
|
661
|
-
maxRetries: 3
|
|
662
|
-
});
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### Debug Logger
|
|
666
|
-
|
|
667
|
-
Comprehensive logging for troubleshooting with enhanced v3.0 features.
|
|
668
|
-
|
|
669
|
-
```tsx
|
|
670
|
-
import { getDebugLogger } from '@chemmangat/msal-next';
|
|
671
|
-
|
|
672
|
-
const logger = getDebugLogger({
|
|
673
|
-
enabled: true,
|
|
674
|
-
level: 'debug',
|
|
675
|
-
showTimestamp: true,
|
|
676
|
-
enablePerformance: true, // NEW in v3.0
|
|
677
|
-
enableNetworkLogs: true, // NEW in v3.0
|
|
678
|
-
maxHistorySize: 100, // NEW in v3.0
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Basic logging
|
|
682
|
-
logger.info('User logged in', { username: 'user@example.com' });
|
|
683
|
-
logger.error('Authentication failed', { error });
|
|
684
|
-
|
|
685
|
-
// NEW: Performance tracking
|
|
686
|
-
logger.startTiming('token-acquisition');
|
|
687
|
-
const token = await acquireToken(['User.Read']);
|
|
688
|
-
logger.endTiming('token-acquisition'); // Logs duration
|
|
689
|
-
|
|
690
|
-
// NEW: Network logging
|
|
691
|
-
logger.logRequest('GET', '/me');
|
|
692
|
-
logger.logResponse('GET', '/me', 200, userData);
|
|
693
|
-
|
|
694
|
-
// NEW: Export logs
|
|
695
|
-
const logs = logger.exportLogs();
|
|
696
|
-
logger.downloadLogs('debug-logs.json'); // Download as file
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
## TypeScript Support
|
|
700
|
-
|
|
701
|
-
### Custom Token Claims
|
|
702
610
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
```tsx
|
|
706
|
-
import { CustomTokenClaims } from '@chemmangat/msal-next';
|
|
707
|
-
|
|
708
|
-
interface MyCustomClaims extends CustomTokenClaims {
|
|
709
|
-
roles: string[];
|
|
710
|
-
department: string;
|
|
711
|
-
employeeId: string;
|
|
611
|
+
return null;
|
|
712
612
|
}
|
|
713
|
-
|
|
714
|
-
const { account } = useMsalAuth();
|
|
715
|
-
const claims = account?.idTokenClaims as MyCustomClaims;
|
|
716
|
-
|
|
717
|
-
console.log(claims.roles); // Type-safe!
|
|
718
|
-
console.log(claims.department); // Type-safe!
|
|
719
613
|
```
|
|
720
614
|
|
|
721
|
-
|
|
615
|
+
### Multi-Tenant Applications
|
|
722
616
|
|
|
723
|
-
|
|
617
|
+
For apps that support any Azure AD tenant:
|
|
724
618
|
|
|
725
619
|
```tsx
|
|
726
|
-
<
|
|
727
|
-
clientId=
|
|
728
|
-
authorityType="common"
|
|
620
|
+
<MSALProvider
|
|
621
|
+
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
622
|
+
authorityType="common" // No tenantId needed
|
|
729
623
|
>
|
|
730
624
|
{children}
|
|
731
|
-
</
|
|
625
|
+
</MSALProvider>
|
|
732
626
|
```
|
|
733
627
|
|
|
734
628
|
### Custom Scopes
|
|
735
629
|
|
|
630
|
+
Request additional Microsoft Graph permissions:
|
|
631
|
+
|
|
736
632
|
```tsx
|
|
737
|
-
<
|
|
738
|
-
clientId=
|
|
739
|
-
scopes={[
|
|
740
|
-
'User.Read',
|
|
741
|
-
'Mail.Read',
|
|
742
|
-
'Calendars.Read',
|
|
743
|
-
'Files.Read.All'
|
|
744
|
-
]}
|
|
633
|
+
<MSALProvider
|
|
634
|
+
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
635
|
+
scopes={['User.Read', 'Mail.Read', 'Calendars.Read', 'Files.Read']}
|
|
745
636
|
>
|
|
746
637
|
{children}
|
|
747
|
-
</
|
|
638
|
+
</MSALProvider>
|
|
748
639
|
```
|
|
749
640
|
|
|
750
|
-
###
|
|
751
|
-
|
|
752
|
-
```tsx
|
|
753
|
-
const { accounts, loginPopup } = useMsalAuth();
|
|
754
|
-
|
|
755
|
-
// Show account picker
|
|
756
|
-
await loginPopup(scopes, {
|
|
757
|
-
prompt: 'select_account'
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
// List all accounts
|
|
761
|
-
accounts.map(account => (
|
|
762
|
-
<div key={account.homeAccountId}>
|
|
763
|
-
{account.username}
|
|
764
|
-
</div>
|
|
765
|
-
));
|
|
766
|
-
```
|
|
641
|
+
### Server-Side Session
|
|
767
642
|
|
|
768
|
-
|
|
643
|
+
Access authentication state in Server Components:
|
|
769
644
|
|
|
770
645
|
```tsx
|
|
771
|
-
// app/profile/page.tsx
|
|
772
|
-
import { getServerSession } from '@chemmangat/msal-next';
|
|
646
|
+
// app/profile/page.tsx (Server Component)
|
|
647
|
+
import { getServerSession } from '@chemmangat/msal-next/server';
|
|
648
|
+
import { redirect } from 'next/navigation';
|
|
773
649
|
|
|
774
650
|
export default async function ProfilePage() {
|
|
775
651
|
const session = await getServerSession();
|
|
776
652
|
|
|
653
|
+
if (!session.isAuthenticated) {
|
|
654
|
+
redirect('/login');
|
|
655
|
+
}
|
|
656
|
+
|
|
777
657
|
return (
|
|
778
658
|
<div>
|
|
779
659
|
<h1>Profile</h1>
|
|
780
|
-
{session.
|
|
781
|
-
<p>Welcome {session.username}</p>
|
|
782
|
-
) : (
|
|
783
|
-
<p>Please sign in</p>
|
|
784
|
-
)}
|
|
660
|
+
<p>Welcome, {session.username}</p>
|
|
785
661
|
</div>
|
|
786
662
|
);
|
|
787
663
|
}
|
|
788
664
|
```
|
|
789
665
|
|
|
790
|
-
###
|
|
666
|
+
### Middleware Protection
|
|
791
667
|
|
|
792
|
-
|
|
793
|
-
'use client';
|
|
794
|
-
|
|
795
|
-
import { useRoles, AuthGuard } from '@chemmangat/msal-next';
|
|
796
|
-
|
|
797
|
-
function AdminPanel() {
|
|
798
|
-
const { hasRole, hasAnyRole, hasAllRoles } = useRoles();
|
|
668
|
+
Protect routes at the edge:
|
|
799
669
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
670
|
+
```tsx
|
|
671
|
+
// middleware.ts
|
|
672
|
+
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
803
673
|
|
|
804
|
-
|
|
805
|
-
|
|
674
|
+
export const middleware = createAuthMiddleware({
|
|
675
|
+
protectedRoutes: ['/dashboard', '/profile', '/api/protected'],
|
|
676
|
+
publicOnlyRoutes: ['/login'],
|
|
677
|
+
loginPath: '/login',
|
|
678
|
+
debug: true,
|
|
679
|
+
});
|
|
806
680
|
|
|
807
|
-
export
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
<AdminPanel />
|
|
811
|
-
</AuthGuard>
|
|
812
|
-
);
|
|
813
|
-
}
|
|
681
|
+
export const config = {
|
|
682
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
683
|
+
};
|
|
814
684
|
```
|
|
815
685
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
### MsalAuthConfig
|
|
819
|
-
|
|
820
|
-
| Option | Type | Default | Description |
|
|
821
|
-
|--------|------|---------|-------------|
|
|
822
|
-
| `clientId` | `string` | **Required** | Azure AD Application (client) ID |
|
|
823
|
-
| `tenantId` | `string` | `undefined` | Azure AD Directory (tenant) ID |
|
|
824
|
-
| `authorityType` | `'common' \| 'organizations' \| 'consumers' \| 'tenant'` | `'common'` | Authority type |
|
|
825
|
-
| `redirectUri` | `string` | `window.location.origin` | Redirect URI after auth |
|
|
826
|
-
| `scopes` | `string[]` | `['User.Read']` | Default scopes |
|
|
827
|
-
| `cacheLocation` | `'sessionStorage' \| 'localStorage' \| 'memoryStorage'` | `'sessionStorage'` | Token cache location |
|
|
828
|
-
| `enableLogging` | `boolean` | `false` | Enable debug logging |
|
|
829
|
-
|
|
830
|
-
## Troubleshooting
|
|
686
|
+
### Error Handling
|
|
831
687
|
|
|
832
|
-
|
|
688
|
+
Use enhanced error handling for better debugging:
|
|
833
689
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
**Issue**: Token acquisition fails
|
|
838
|
-
**Solution**: Check that required scopes are granted in Azure AD
|
|
839
|
-
|
|
840
|
-
**Issue**: SSR hydration mismatch
|
|
841
|
-
**Solution**: Use `'use client'` directive for components using auth hooks
|
|
842
|
-
|
|
843
|
-
**Issue**: Middleware not protecting routes
|
|
844
|
-
**Solution**: Ensure session cookies are being set after login
|
|
845
|
-
|
|
846
|
-
**Issue**: "createContext only works in Client Components"
|
|
847
|
-
**Solution**: Use `MSALProvider` (not `MsalAuthProvider`) in layout.tsx
|
|
848
|
-
|
|
849
|
-
**Issue**: Redirect URI mismatch (AADSTS50011)
|
|
850
|
-
**Solution**: Add your redirect URI to Azure Portal โ App registrations โ Authentication
|
|
851
|
-
|
|
852
|
-
**Issue**: Missing environment variables
|
|
853
|
-
**Solution**: Create `.env.local` with `NEXT_PUBLIC_AZURE_AD_CLIENT_ID` and `NEXT_PUBLIC_AZURE_AD_TENANT_ID`
|
|
690
|
+
```tsx
|
|
691
|
+
'use client';
|
|
854
692
|
|
|
855
|
-
|
|
693
|
+
import { useMsalAuth, wrapMsalError } from '@chemmangat/msal-next';
|
|
694
|
+
|
|
695
|
+
export default function LoginPage() {
|
|
696
|
+
const { loginRedirect } = useMsalAuth();
|
|
697
|
+
|
|
698
|
+
const handleLogin = async () => {
|
|
699
|
+
try {
|
|
700
|
+
await loginRedirect();
|
|
701
|
+
} catch (error) {
|
|
702
|
+
const msalError = wrapMsalError(error);
|
|
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
|
+
};
|
|
713
|
+
|
|
714
|
+
return <button onClick={handleLogin}>Sign In</button>;
|
|
715
|
+
}
|
|
716
|
+
```
|
|
856
717
|
|
|
857
|
-
###
|
|
718
|
+
### Custom Profile Fields
|
|
858
719
|
|
|
859
|
-
|
|
720
|
+
Extend UserProfile with organization-specific fields:
|
|
860
721
|
|
|
861
722
|
```tsx
|
|
862
|
-
|
|
863
|
-
clientId="YOUR_CLIENT_ID"
|
|
864
|
-
enableLogging={true}
|
|
865
|
-
>
|
|
866
|
-
{children}
|
|
867
|
-
</MsalAuthProvider>
|
|
868
|
-
```
|
|
723
|
+
'use client';
|
|
869
724
|
|
|
870
|
-
|
|
725
|
+
import { useUserProfile, UserProfile } from '@chemmangat/msal-next';
|
|
871
726
|
|
|
872
|
-
|
|
727
|
+
interface MyCompanyProfile extends UserProfile {
|
|
728
|
+
customField: string;
|
|
729
|
+
}
|
|
873
730
|
|
|
874
|
-
|
|
875
|
-
|
|
731
|
+
export default function ProfilePage() {
|
|
732
|
+
const { profile } = useUserProfile<MyCompanyProfile>();
|
|
876
733
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (msalError.isUserCancellation()) {
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Get actionable error message with fix instructions
|
|
888
|
-
console.error(msalError.toConsoleString());
|
|
889
|
-
|
|
890
|
-
// Access error details
|
|
891
|
-
console.log('Error code:', msalError.code);
|
|
892
|
-
console.log('Fix instructions:', msalError.fix);
|
|
893
|
-
console.log('Documentation:', msalError.docs);
|
|
734
|
+
return (
|
|
735
|
+
<div>
|
|
736
|
+
<p>Department: {profile?.department}</p>
|
|
737
|
+
<p>Custom Field: {profile?.customField}</p>
|
|
738
|
+
</div>
|
|
739
|
+
);
|
|
894
740
|
}
|
|
895
741
|
```
|
|
896
742
|
|
|
897
|
-
|
|
743
|
+
---
|
|
898
744
|
|
|
899
|
-
|
|
745
|
+
## ๐ง Configuration Reference
|
|
900
746
|
|
|
901
|
-
|
|
747
|
+
### MSALProvider Props
|
|
902
748
|
|
|
903
|
-
|
|
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 |
|
|
904
758
|
|
|
905
|
-
|
|
906
|
-
```tsx
|
|
907
|
-
// middleware.ts
|
|
908
|
-
export async function middleware(request) {
|
|
909
|
-
const session = await getServerSession();
|
|
910
|
-
if (!session) return redirect('/login');
|
|
911
|
-
}
|
|
759
|
+
### Authority Types
|
|
912
760
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
return <div>Protected</div>;
|
|
918
|
-
}
|
|
919
|
-
```
|
|
761
|
+
- **`common`** - Multi-tenant (any Azure AD tenant or Microsoft account)
|
|
762
|
+
- **`organizations`** - Any organizational Azure AD tenant
|
|
763
|
+
- **`consumers`** - Microsoft personal accounts only
|
|
764
|
+
- **`tenant`** - Single-tenant (requires `tenantId`)
|
|
920
765
|
|
|
921
|
-
|
|
922
|
-
```tsx
|
|
923
|
-
// app/dashboard/page.tsx
|
|
924
|
-
export const auth = { required: true };
|
|
766
|
+
---
|
|
925
767
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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)
|
|
930
789
|
|
|
931
|
-
|
|
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)
|
|
932
794
|
|
|
933
795
|
---
|
|
934
796
|
|
|
935
|
-
##
|
|
797
|
+
## โ FAQ
|
|
936
798
|
|
|
937
|
-
|
|
799
|
+
**Q: Do I need to create an Azure AD app registration?**
|
|
800
|
+
A: Yes, you need an app registration in Azure Portal to get the client ID and tenant ID.
|
|
938
801
|
|
|
939
|
-
|
|
802
|
+
**Q: Can I use this with Next.js Pages Router?**
|
|
803
|
+
A: This package is designed for App Router. For Pages Router, use v2.x or consider migrating to App Router.
|
|
940
804
|
|
|
941
|
-
**
|
|
805
|
+
**Q: Is this free to use?**
|
|
806
|
+
A: Yes, the package is MIT licensed and free. Azure AD has a free tier for up to 50,000 users.
|
|
942
807
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
npm install @chemmangat/msal-next@3.0.0
|
|
946
|
-
npm install @azure/msal-browser@^4.0.0
|
|
947
|
-
npm install @azure/msal-react@^3.0.0
|
|
948
|
-
npm install next@^14.1.0
|
|
808
|
+
**Q: Can I use this for multi-tenant SaaS apps?**
|
|
809
|
+
A: Yes! Set `authorityType="common"` and omit `tenantId`.
|
|
949
810
|
|
|
950
|
-
|
|
951
|
-
|
|
811
|
+
**Q: How do I get additional user information?**
|
|
812
|
+
A: Use `useUserProfile()` hook which provides 30+ fields from Microsoft Graph.
|
|
952
813
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const session = await getServerSession();
|
|
956
|
-
const token = session.accessToken; // โ Removed
|
|
814
|
+
**Q: Can I customize the sign-in button?**
|
|
815
|
+
A: Yes, `MicrosoftSignInButton` accepts `variant`, `size`, `className`, and `style` props.
|
|
957
816
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const { acquireToken } = useMsalAuth();
|
|
961
|
-
const token = await acquireToken(['User.Read']); // โ
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
### From v1.x to v2.x
|
|
817
|
+
**Q: Does this work with Azure AD B2C?**
|
|
818
|
+
A: This package is designed for Azure AD. For B2C, you may need additional configuration.
|
|
965
819
|
|
|
966
|
-
|
|
820
|
+
**Q: How do I protect API routes?**
|
|
821
|
+
A: Use `getServerSession()` in API routes or `createAuthMiddleware()` for edge protection.
|
|
967
822
|
|
|
968
|
-
|
|
969
|
-
// v1.x - Still works!
|
|
970
|
-
import { MsalAuthProvider, useMsalAuth } from '@chemmangat/msal-next';
|
|
971
|
-
|
|
972
|
-
// v2.x - New features
|
|
973
|
-
import {
|
|
974
|
-
AuthGuard,
|
|
975
|
-
SignOutButton,
|
|
976
|
-
UserAvatar,
|
|
977
|
-
useGraphApi,
|
|
978
|
-
useUserProfile,
|
|
979
|
-
useRoles,
|
|
980
|
-
withAuth,
|
|
981
|
-
createAuthMiddleware,
|
|
982
|
-
} from '@chemmangat/msal-next';
|
|
983
|
-
```
|
|
823
|
+
---
|
|
984
824
|
|
|
985
|
-
## Contributing
|
|
825
|
+
## ๐ค Contributing
|
|
986
826
|
|
|
987
827
|
Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
988
828
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
MIT ยฉ [Chemmangat](https://github.com/chemmangat)
|
|
992
|
-
|
|
993
|
-
## Support
|
|
994
|
-
|
|
995
|
-
- ๐ [Documentation](https://github.com/chemmangat/msal-next#readme)
|
|
996
|
-
- ๐ [Issue Tracker](https://github.com/chemmangat/msal-next/issues)
|
|
997
|
-
- ๐ฌ [Discussions](https://github.com/chemmangat/msal-next/discussions)
|
|
998
|
-
- ๐ [CLI Tool](https://www.npmjs.com/package/@chemmangat/msal-next-cli)
|
|
999
|
-
- ๐ [Migration Guide](./MIGRATION_GUIDE_v3.md)
|
|
1000
|
-
- ๐งช [Testing Guide](./TESTING_GUIDE.md)
|
|
829
|
+
---
|
|
1001
830
|
|
|
1002
|
-
##
|
|
831
|
+
## ๐ License
|
|
1003
832
|
|
|
1004
|
-
|
|
1005
|
-
- ๐ 6+ additional examples
|
|
1006
|
-
- โก Performance optimizations
|
|
1007
|
-
- ๐ Security audit
|
|
1008
|
-
- ๐ New hooks and components
|
|
833
|
+
MIT ยฉ [Chemmangat](https://github.com/chemmangat)
|
|
1009
834
|
|
|
1010
|
-
|
|
835
|
+
---
|
|
1011
836
|
|
|
1012
|
-
## Acknowledgments
|
|
837
|
+
## ๐ Acknowledgments
|
|
1013
838
|
|
|
1014
839
|
Built with:
|
|
1015
840
|
- [@azure/msal-browser](https://github.com/AzureAD/microsoft-authentication-library-for-js)
|
|
1016
841
|
- [@azure/msal-react](https://github.com/AzureAD/microsoft-authentication-library-for-js)
|
|
1017
842
|
- [Next.js](https://nextjs.org/)
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## ๐ Stats
|
|
847
|
+
|
|
848
|
+
- ๐ฆ 2,200+ weekly downloads
|
|
849
|
+
- โญ Used in production by developers worldwide
|
|
850
|
+
- ๐ Security-focused with regular updates
|
|
851
|
+
- ๐ Comprehensive documentation
|
|
852
|
+
- ๐ฏ TypeScript-first with complete type safety
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
**Need help?** Check [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) or [open an issue](https://github.com/chemmangat/msal-next/issues)!
|