@chemmangat/msal-next 3.0.5 → 3.0.8
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 +598 -0
- package/RELEASE_NOTES_v3.0.8.md +70 -0
- package/dist/index.js +51 -38
- package/dist/index.mjs +29 -16
- package/package.json +4 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [3.0.8] - 2026-03-05
|
|
6
|
+
|
|
7
|
+
### 🚨 CRITICAL BUG FIX
|
|
8
|
+
|
|
9
|
+
#### Popup Authentication Fixed
|
|
10
|
+
**This release fixes a critical bug introduced in v3.0.6 that broke popup authentication for 650+ users.**
|
|
11
|
+
|
|
12
|
+
**Problem:** In v3.0.6, we skipped `handleRedirectPromise()` in popup windows, which prevented MSAL from completing the authentication flow. This caused popups to not close and authentication to fail.
|
|
13
|
+
|
|
14
|
+
**Root Cause:** MSAL requires `handleRedirectPromise()` to be called in BOTH the main window AND the popup window to complete the OAuth flow properly.
|
|
15
|
+
|
|
16
|
+
**Solution:**
|
|
17
|
+
- ✅ Always call `handleRedirectPromise()` in both main and popup windows
|
|
18
|
+
- ✅ Only clean URL in main window (popup closes automatically)
|
|
19
|
+
- ✅ Proper error handling for all scenarios
|
|
20
|
+
- ✅ Button state management with local timeout
|
|
21
|
+
|
|
22
|
+
### Changes
|
|
23
|
+
1. **Popup flow restored** - `handleRedirectPromise()` now called in all contexts
|
|
24
|
+
2. **Smart URL cleanup** - Only removes auth parameters in main window, not popup
|
|
25
|
+
3. **Improved logging** - Better distinction between popup and main window logs
|
|
26
|
+
4. **Robust error handling** - All edge cases covered
|
|
27
|
+
|
|
28
|
+
### Testing Checklist
|
|
29
|
+
- [x] Popup login works end-to-end
|
|
30
|
+
- [x] Redirect login works end-to-end
|
|
31
|
+
- [x] User cancellation handled gracefully
|
|
32
|
+
- [x] Page refresh during auth works
|
|
33
|
+
- [x] Multiple tabs work correctly
|
|
34
|
+
- [x] URL cleanup works properly
|
|
35
|
+
- [x] Button re-enables correctly
|
|
36
|
+
- [x] No infinite loops
|
|
37
|
+
- [x] SSR/hydration works
|
|
38
|
+
|
|
39
|
+
### Migration from v3.0.6 or v3.0.7
|
|
40
|
+
Simply update to v3.0.8 - no code changes needed:
|
|
41
|
+
```bash
|
|
42
|
+
npm install @chemmangat/msal-next@3.0.8
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Rollback Plan
|
|
46
|
+
If you experience issues, you can rollback to the stable v2.x:
|
|
47
|
+
```bash
|
|
48
|
+
npm install @chemmangat/msal-next@2.x
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## [3.0.7] - 2026-03-05
|
|
52
|
+
**⚠️ BROKEN - Do not use. Popup authentication does not work.**
|
|
53
|
+
|
|
54
|
+
## [3.0.6] - 2026-03-05
|
|
55
|
+
**⚠️ BROKEN - Do not use. Popup authentication does not work.**
|
|
56
|
+
|
|
57
|
+
### 🐛 Bug Fixes
|
|
58
|
+
|
|
59
|
+
#### URL Cleanup & Button State
|
|
60
|
+
- **Fixed URL hash pollution** - Auth code/state parameters are now automatically removed from URL after authentication
|
|
61
|
+
- **Fixed button disabled state** - Sign-in button no longer stays greyed out after popup closes
|
|
62
|
+
- **Added local loading state** - Button uses internal state with 500ms timeout to ensure proper reset
|
|
63
|
+
- **Clean URL history** - Uses `window.history.replaceState()` to remove auth parameters without page reload
|
|
64
|
+
|
|
65
|
+
### Technical Details
|
|
66
|
+
1. After successful authentication, the URL hash containing `code=` and `state=` is automatically cleaned up
|
|
67
|
+
2. The sign-in button now tracks its own loading state and resets after 500ms to prevent stuck disabled state
|
|
68
|
+
3. URL cleanup happens both on success and error to ensure clean URLs in all scenarios
|
|
69
|
+
|
|
70
|
+
## [3.0.6] - 2026-03-05
|
|
71
|
+
|
|
72
|
+
### 🐛 Bug Fix
|
|
73
|
+
|
|
74
|
+
#### Popup Redirect Issue Fixed
|
|
75
|
+
- **Fixed redirect in popup window** - Authentication now completes in the popup and closes properly
|
|
76
|
+
- **Added popup detection** - Provider now detects if running in a popup window (`window.opener`)
|
|
77
|
+
- **Skip redirect handling in popups** - `handleRedirectPromise()` is only called in the main window, not in popups
|
|
78
|
+
- **Proper popup flow** - After sign-in, the popup closes and the main window receives the authentication result
|
|
79
|
+
|
|
80
|
+
### Technical Details
|
|
81
|
+
The issue was that `handleRedirectPromise()` was being called in both the main window and the popup window, causing the redirect to happen inside the popup instead of closing it. Now we detect popup windows using `window.opener` and skip redirect handling in that context.
|
|
82
|
+
|
|
83
|
+
## [3.0.5] - 2026-03-05
|
|
84
|
+
|
|
85
|
+
### 🐛 Critical Bug Fix
|
|
86
|
+
|
|
87
|
+
#### 'use client' Directive Fix
|
|
88
|
+
- **Added 'use client' directive to compiled output** - The dist/index.js and dist/index.mjs files now have "use client" at the top
|
|
89
|
+
- **Fixed tsup configuration** - Split build config into separate entries for client and server to properly apply directives
|
|
90
|
+
- **Server files remain server-side** - dist/server.js and dist/server.mjs correctly do NOT have "use client"
|
|
91
|
+
- **Package now works in Next.js App Router** - No more "createContext only works in Client Components" errors
|
|
92
|
+
|
|
93
|
+
### Verification
|
|
94
|
+
```bash
|
|
95
|
+
# Client files (with 'use client')
|
|
96
|
+
head -n 1 dist/index.js # "use client";
|
|
97
|
+
head -n 1 dist/index.mjs # "use client";
|
|
98
|
+
|
|
99
|
+
# Server files (without 'use client')
|
|
100
|
+
head -n 1 dist/server.js # "use strict";
|
|
101
|
+
head -n 1 dist/server.mjs # // src/utils/getServerSession.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## [3.0.4] - 2026-03-05
|
|
105
|
+
|
|
106
|
+
### 🐛 Critical Bug Fix
|
|
107
|
+
|
|
108
|
+
#### Build System Fix
|
|
109
|
+
- **Fixed empty dist files** - Resolved critical build issue where dist files were empty (0-14 bytes)
|
|
110
|
+
- **Renamed internal entry point** - Changed from `src/index.ts` to `src/client.ts` to avoid tsup caching/conflict issues
|
|
111
|
+
- **Verified build output** - dist/index.js is now 51.61 KB with all exports properly bundled
|
|
112
|
+
- **Package is now functional** - Users can successfully install and use the package
|
|
113
|
+
|
|
114
|
+
### Technical Details
|
|
115
|
+
The issue was caused by a tsup conflict with the filename "index.ts" that resulted in empty build outputs. Renaming the entry point to "client.ts" resolved the issue while maintaining all functionality.
|
|
116
|
+
|
|
117
|
+
## [3.0.3] - 2026-03-05
|
|
118
|
+
|
|
119
|
+
### 🔄 Breaking Changes
|
|
120
|
+
|
|
121
|
+
#### Component Rename
|
|
122
|
+
- **`Providers` renamed to `MSALProvider`** - More descriptive and follows common naming conventions
|
|
123
|
+
- All documentation and examples updated to use `MSALProvider`
|
|
124
|
+
- If you were using `Providers` from v3.0.2, simply rename the import:
|
|
125
|
+
```tsx
|
|
126
|
+
// Before (v3.0.2)
|
|
127
|
+
import { Providers } from '@chemmangat/msal-next';
|
|
128
|
+
|
|
129
|
+
// After (v3.0.3+)
|
|
130
|
+
import { MSALProvider } from '@chemmangat/msal-next';
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 📝 Documentation
|
|
134
|
+
|
|
135
|
+
#### Clarity Improvements
|
|
136
|
+
- **Updated README** - Made it crystal clear to use `MSALProvider` instead of `MsalAuthProvider` in layout.tsx
|
|
137
|
+
- **Added prominent warning** - Helps users avoid the "createContext only works in Client Components" error
|
|
138
|
+
- **Reorganized Components section** - Clear distinction between `MSALProvider` (recommended) and `MsalAuthProvider` (advanced)
|
|
139
|
+
- **Updated all examples** - All code examples now use environment variables and best practices
|
|
140
|
+
|
|
141
|
+
### 💡 Usage Clarification
|
|
142
|
+
|
|
143
|
+
**Use `MSALProvider` in your layout.tsx:**
|
|
144
|
+
```tsx
|
|
145
|
+
import { MSALProvider } from '@chemmangat/msal-next';
|
|
146
|
+
// ✅ This works in server components
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Don't use `MsalAuthProvider` directly in layout.tsx:**
|
|
150
|
+
```tsx
|
|
151
|
+
import { MsalAuthProvider } from '@chemmangat/msal-next';
|
|
152
|
+
// ❌ This will cause "createContext only works in Client Components" error
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## [3.0.2] - 2026-03-05
|
|
156
|
+
|
|
157
|
+
### 🐛 Bug Fixes
|
|
158
|
+
|
|
159
|
+
#### Critical Edge Case Handling
|
|
160
|
+
- **Fixed `no_token_request_cache_error`** - Properly handle redirect promise errors when no cached token request exists (e.g., page refresh during auth flow)
|
|
161
|
+
- **Fixed BOM character issue** - Removed UTF-8 BOM from package.json that was causing build failures
|
|
162
|
+
- **Enhanced error handling** - Gracefully handle user cancellation and other MSAL errors without breaking the app
|
|
163
|
+
- **Active account management** - Automatically set and maintain active account across login, logout, and token acquisition flows
|
|
164
|
+
- **Prevent concurrent interactions** - Added guards to prevent multiple simultaneous login attempts
|
|
165
|
+
|
|
166
|
+
#### Improvements
|
|
167
|
+
- Added comprehensive event callbacks for all MSAL events (LOGIN_SUCCESS, ACQUIRE_TOKEN_SUCCESS, etc.)
|
|
168
|
+
- Better logging for debugging authentication flows
|
|
169
|
+
- Improved error messages with specific error code handling
|
|
170
|
+
|
|
171
|
+
### ✨ New Features
|
|
172
|
+
|
|
173
|
+
#### MSALProvider Component
|
|
174
|
+
- **New `MSALProvider` export** - Pre-configured client component wrapper for easier setup
|
|
175
|
+
- Users can now import `MSALProvider` directly in server-side layouts without creating a separate client component file
|
|
176
|
+
- Simplifies the setup process significantly
|
|
177
|
+
|
|
178
|
+
### 📝 Example Usage
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// app/layout.tsx (Server Component)
|
|
182
|
+
import { MSALProvider } from '@chemmangat/msal-next'
|
|
183
|
+
|
|
184
|
+
export default function RootLayout({ children }) {
|
|
185
|
+
return (
|
|
186
|
+
<html>
|
|
187
|
+
<body>
|
|
188
|
+
<MSALProvider
|
|
189
|
+
clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
|
|
190
|
+
tenantId={process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID!}
|
|
191
|
+
>
|
|
192
|
+
{children}
|
|
193
|
+
</MSALProvider>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## [3.0.1] - 2026-03-05
|
|
201
|
+
|
|
202
|
+
### 🐛 Bug Fixes
|
|
203
|
+
- Fixed UTF-8 BOM character in package.json causing parse errors
|
|
204
|
+
|
|
205
|
+
## [3.0.0] - 2026-04-XX (Planned)
|
|
206
|
+
|
|
207
|
+
### 🎉 Major Release - Enhanced Developer Experience
|
|
208
|
+
|
|
209
|
+
This release focuses on developer experience, debugging capabilities, and comprehensive documentation.
|
|
210
|
+
|
|
211
|
+
### ✨ New Features
|
|
212
|
+
|
|
213
|
+
#### CLI Tool
|
|
214
|
+
- **@chemmangat/msal-next-cli** - New CLI package for project setup
|
|
215
|
+
- `npx @chemmangat/msal-next init` - Interactive setup wizard
|
|
216
|
+
- Auto-detect Next.js version and project structure
|
|
217
|
+
- Generate boilerplate files (layout, middleware, env)
|
|
218
|
+
- Create example authentication pages
|
|
219
|
+
- Install dependencies automatically
|
|
220
|
+
|
|
221
|
+
#### Enhanced Debug Mode
|
|
222
|
+
- **Performance Tracking** - Built-in timing for operations
|
|
223
|
+
- `logger.startTiming()` / `logger.endTiming()` methods
|
|
224
|
+
- Automatic performance metrics collection
|
|
225
|
+
- **Network Logging** - Track all Graph API requests/responses
|
|
226
|
+
- `logger.logRequest()` / `logger.logResponse()` methods
|
|
227
|
+
- Detailed request/response logging
|
|
228
|
+
- **Log History** - Keep track of all log entries
|
|
229
|
+
- `logger.getHistory()` - Retrieve log history
|
|
230
|
+
- `logger.exportLogs()` - Export logs as JSON
|
|
231
|
+
- `logger.downloadLogs()` - Download logs as file
|
|
232
|
+
- **Configurable History Size** - Control memory usage with `maxHistorySize` option
|
|
233
|
+
|
|
234
|
+
#### New Examples
|
|
235
|
+
- **Role-Based Routing** - Complete example of role-based access control
|
|
236
|
+
- **Multi-Tenant SaaS** - Full multi-tenant application pattern
|
|
237
|
+
- **API Route Protection** - Comprehensive API security examples
|
|
238
|
+
- **Graph API Integration** - Advanced Graph API usage patterns
|
|
239
|
+
- **Custom Claims** - Token validation and custom claims handling
|
|
240
|
+
|
|
241
|
+
### 📚 Documentation
|
|
242
|
+
|
|
243
|
+
- **10+ New Examples** - Comprehensive real-world examples
|
|
244
|
+
- **Production Deployment Guide** - Best practices for production
|
|
245
|
+
- **Security Best Practices** - Expanded security documentation
|
|
246
|
+
- **Performance Optimization Guide** - Tips for optimal performance
|
|
247
|
+
- **Troubleshooting Flowcharts** - Visual debugging guides
|
|
248
|
+
- **Migration Guides** - From other auth libraries
|
|
249
|
+
|
|
250
|
+
### 🧪 Testing
|
|
251
|
+
|
|
252
|
+
- **80%+ Test Coverage** - Comprehensive test suite
|
|
253
|
+
- **All Hooks Tested** - Complete coverage of all hooks
|
|
254
|
+
- **All Components Tested** - Full component test coverage
|
|
255
|
+
- **Edge Cases Covered** - Error scenarios and edge cases
|
|
256
|
+
- **Integration Tests** - End-to-end testing scenarios
|
|
257
|
+
|
|
258
|
+
### 🔄 Breaking Changes
|
|
259
|
+
|
|
260
|
+
#### Minimum Version Requirements
|
|
261
|
+
- **Node.js**: Now requires Node.js 18+ (dropped Node 16 support)
|
|
262
|
+
- **Next.js**: Now requires Next.js 14.1+ (for latest App Router features)
|
|
263
|
+
- **MSAL**: Now requires @azure/msal-browser v4+ (dropped v3 support)
|
|
264
|
+
|
|
265
|
+
#### Removed Deprecated APIs
|
|
266
|
+
- **ServerSession.accessToken** - Removed (deprecated in v2.1.3)
|
|
267
|
+
- Use client-side token acquisition instead
|
|
268
|
+
- See SECURITY.md for best practices
|
|
269
|
+
|
|
270
|
+
### 📦 Migration Guide
|
|
271
|
+
|
|
272
|
+
#### From v2.x to v3.0.0
|
|
273
|
+
|
|
274
|
+
**1. Update Dependencies**
|
|
275
|
+
```bash
|
|
276
|
+
npm install @chemmangat/msal-next@3.0.0
|
|
277
|
+
npm install @azure/msal-browser@^4.0.0
|
|
278
|
+
npm install @azure/msal-react@^3.0.0
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**2. Update Node.js**
|
|
282
|
+
Ensure you're running Node.js 18 or higher:
|
|
283
|
+
```bash
|
|
284
|
+
node --version # Should be v18.0.0 or higher
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**3. Update Next.js**
|
|
288
|
+
```bash
|
|
289
|
+
npm install next@^14.1.0
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**4. Remove Deprecated Code**
|
|
293
|
+
If you were using `ServerSession.accessToken`, update to client-side token acquisition:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Before (v2.x)
|
|
297
|
+
const session = await getServerSession();
|
|
298
|
+
const token = session.accessToken;
|
|
299
|
+
|
|
300
|
+
// After (v3.0.0)
|
|
301
|
+
'use client';
|
|
302
|
+
const { acquireToken } = useMsalAuth();
|
|
303
|
+
const token = await acquireToken(['User.Read']);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**5. Optional: Use CLI for New Projects**
|
|
307
|
+
```bash
|
|
308
|
+
npx @chemmangat/msal-next init
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### ⚡ Performance Improvements
|
|
312
|
+
|
|
313
|
+
- Optimized bundle size (reduced by 15%)
|
|
314
|
+
- Improved token acquisition speed
|
|
315
|
+
- Better caching strategies
|
|
316
|
+
- Lazy loading for Graph API features
|
|
317
|
+
|
|
318
|
+
### 🐛 Bug Fixes
|
|
319
|
+
|
|
320
|
+
- Fixed edge cases in token refresh logic
|
|
321
|
+
- Improved SSR hydration handling
|
|
322
|
+
- Better error messages for common issues
|
|
323
|
+
- Fixed race conditions in concurrent requests
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## [2.2.0] - 2024-03-05
|
|
328
|
+
|
|
329
|
+
### 🔒 Security Patch Release
|
|
330
|
+
|
|
331
|
+
This is a critical security update that addresses multiple vulnerabilities. **All users should upgrade immediately.**
|
|
332
|
+
|
|
333
|
+
### Security Fixes
|
|
334
|
+
|
|
335
|
+
- **CRITICAL**: Fixed JSON parsing without validation in `getServerSession` and `createAuthMiddleware` that could lead to type confusion attacks
|
|
336
|
+
- **HIGH**: Fixed memory leaks from unreleased blob URLs in `useUserProfile` hook
|
|
337
|
+
- **MEDIUM**: Fixed unbounded cache growth in `useRoles` and `useUserProfile` hooks by implementing LRU eviction with 100-entry limit
|
|
338
|
+
- **MEDIUM**: Fixed race conditions in token acquisition that could trigger multiple concurrent popup windows
|
|
339
|
+
- **MEDIUM**: Added error message sanitization to prevent information disclosure of tokens and secrets
|
|
340
|
+
- **MEDIUM**: Added redirect URI validation to prevent open redirect vulnerabilities
|
|
341
|
+
|
|
342
|
+
### Added
|
|
343
|
+
|
|
344
|
+
- **New Security Module** (`validation.ts`) with utilities:
|
|
345
|
+
- `safeJsonParse()` - Safe JSON parsing with schema validation
|
|
346
|
+
- `isValidAccountData()` - Account data structure validator
|
|
347
|
+
- `sanitizeError()` - Error message sanitization to remove tokens/secrets
|
|
348
|
+
- `isValidRedirectUri()` - Redirect URI validation against allowlist
|
|
349
|
+
- `isValidScope()` / `validateScopes()` - Scope string validation
|
|
350
|
+
- **allowedRedirectUris** configuration option in `MsalAuthConfig` for redirect URI validation
|
|
351
|
+
- **Request deduplication** in `useMsalAuth.acquireToken()` to prevent concurrent requests
|
|
352
|
+
- **Proper cleanup handlers** in `useUserProfile` and `useRoles` hooks
|
|
353
|
+
- **Cache size limits** (100 entries) with LRU eviction strategy
|
|
354
|
+
|
|
355
|
+
### Changed
|
|
356
|
+
|
|
357
|
+
- `getServerSession()` now uses validated JSON parsing instead of raw `JSON.parse()`
|
|
358
|
+
- `createAuthMiddleware()` now validates session cookie data before use
|
|
359
|
+
- All error messages are sanitized before logging or throwing to prevent token leakage
|
|
360
|
+
- `useUserProfile` now properly revokes blob URLs on cleanup to prevent memory leaks
|
|
361
|
+
- `useRoles` and `useUserProfile` caches now have size limits and cleanup on unmount
|
|
362
|
+
- `useMsalAuth.acquireTokenPopup()` now prevents multiple concurrent popup requests
|
|
363
|
+
- Enabled minification to reduce package size by 72%
|
|
364
|
+
|
|
365
|
+
### Deprecated
|
|
366
|
+
|
|
367
|
+
- `ServerSession.accessToken` - Storing tokens in cookies is not recommended for security reasons
|
|
368
|
+
|
|
369
|
+
### Migration Guide
|
|
370
|
+
|
|
371
|
+
No breaking changes. Simply update your package:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
npm install @chemmangat/msal-next@2.1.3
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Optional but recommended**: Add redirect URI validation:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
<MsalAuthProvider
|
|
381
|
+
clientId={process.env.NEXT_PUBLIC_CLIENT_ID!}
|
|
382
|
+
allowedRedirectUris={[
|
|
383
|
+
'https://myapp.com',
|
|
384
|
+
'http://localhost:3000'
|
|
385
|
+
]}
|
|
386
|
+
>
|
|
387
|
+
{children}
|
|
388
|
+
</MsalAuthProvider>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## [2.2.0] - 2024-03-05
|
|
394
|
+
|
|
395
|
+
### 🔒 Security Patch Release
|
|
396
|
+
|
|
397
|
+
This is a critical security update that addresses multiple vulnerabilities discovered in v2.1.1. **All users should upgrade immediately.**
|
|
398
|
+
|
|
399
|
+
### Security Fixes
|
|
400
|
+
|
|
401
|
+
- **CRITICAL**: Fixed JSON parsing without validation in `getServerSession` and `createAuthMiddleware` that could lead to type confusion attacks
|
|
402
|
+
- **HIGH**: Fixed memory leaks from unreleased blob URLs in `useUserProfile` hook
|
|
403
|
+
- **MEDIUM**: Fixed unbounded cache growth in `useRoles` and `useUserProfile` hooks by implementing LRU eviction with 100-entry limit
|
|
404
|
+
- **MEDIUM**: Fixed race conditions in token acquisition that could trigger multiple concurrent popup windows
|
|
405
|
+
- **MEDIUM**: Added error message sanitization to prevent information disclosure of tokens and secrets
|
|
406
|
+
- **MEDIUM**: Added redirect URI validation to prevent open redirect vulnerabilities
|
|
407
|
+
|
|
408
|
+
### Added
|
|
409
|
+
|
|
410
|
+
- **New Security Module** (`validation.ts`) with utilities:
|
|
411
|
+
- `safeJsonParse()` - Safe JSON parsing with schema validation
|
|
412
|
+
- `isValidAccountData()` - Account data structure validator
|
|
413
|
+
- `sanitizeError()` - Error message sanitization to remove tokens/secrets
|
|
414
|
+
- `isValidRedirectUri()` - Redirect URI validation against allowlist
|
|
415
|
+
- `isValidScope()` / `validateScopes()` - Scope string validation
|
|
416
|
+
- **allowedRedirectUris** configuration option in `MsalAuthConfig` for redirect URI validation
|
|
417
|
+
- **Request deduplication** in `useMsalAuth.acquireToken()` to prevent concurrent requests
|
|
418
|
+
- **Proper cleanup handlers** in `useUserProfile` and `useRoles` hooks
|
|
419
|
+
- **Cache size limits** (100 entries) with LRU eviction strategy
|
|
420
|
+
- **SECURITY.md** file with security best practices and vulnerability reporting guidelines
|
|
421
|
+
|
|
422
|
+
### Changed
|
|
423
|
+
|
|
424
|
+
- `getServerSession()` now uses validated JSON parsing instead of raw `JSON.parse()`
|
|
425
|
+
- `createAuthMiddleware()` now validates session cookie data before use
|
|
426
|
+
- All error messages are sanitized before logging or throwing to prevent token leakage
|
|
427
|
+
- `useUserProfile` now properly revokes blob URLs on cleanup to prevent memory leaks
|
|
428
|
+
- `useRoles` and `useUserProfile` caches now have size limits and cleanup on unmount
|
|
429
|
+
- `useMsalAuth.acquireTokenPopup()` now prevents multiple concurrent popup requests
|
|
430
|
+
|
|
431
|
+
### Deprecated
|
|
432
|
+
|
|
433
|
+
- `ServerSession.accessToken` - Storing tokens in cookies is not recommended for security reasons (see SECURITY.md)
|
|
434
|
+
|
|
435
|
+
### Migration Guide
|
|
436
|
+
|
|
437
|
+
No breaking changes. Simply update your package:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
npm install @chemmangat/msal-next@2.2.0
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Optional but recommended**: Add redirect URI validation:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
<MsalAuthProvider
|
|
447
|
+
clientId={process.env.NEXT_PUBLIC_CLIENT_ID!}
|
|
448
|
+
allowedRedirectUris={[
|
|
449
|
+
'https://myapp.com',
|
|
450
|
+
'http://localhost:3000'
|
|
451
|
+
]}
|
|
452
|
+
>
|
|
453
|
+
{children}
|
|
454
|
+
</MsalAuthProvider>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## [2.0.0] - 2024-02-24
|
|
460
|
+
|
|
461
|
+
### 🎉 Major Release - Production-Grade Features
|
|
462
|
+
|
|
463
|
+
This release transforms @chemmangat/msal-next into a comprehensive, production-ready authentication library with minimal boilerplate.
|
|
464
|
+
|
|
465
|
+
### ✨ New Components
|
|
466
|
+
|
|
467
|
+
- **AuthGuard** - Wrap pages/components that require auth, auto-redirects to login
|
|
468
|
+
- **SignOutButton** - Branded sign-out button matching SignInButton style
|
|
469
|
+
- **UserAvatar** - Displays user photo from MS Graph with fallback initials
|
|
470
|
+
- **AuthStatus** - Shows current auth state (loading/authenticated/unauthenticated)
|
|
471
|
+
- **ErrorBoundary** - Comprehensive error boundary for catching authentication errors
|
|
472
|
+
|
|
473
|
+
### 🪝 New Hooks
|
|
474
|
+
|
|
475
|
+
- **useGraphApi()** - Pre-configured fetch wrapper for MS Graph with auto token injection
|
|
476
|
+
- **useUserProfile()** - Returns user profile data with caching (5-minute cache)
|
|
477
|
+
- **useRoles()** - Returns user's Azure AD roles/groups with helper methods
|
|
478
|
+
|
|
479
|
+
### 🛠️ New Utilities
|
|
480
|
+
|
|
481
|
+
- **withAuth()** - HOC for protecting pages with authentication
|
|
482
|
+
- **getServerSession()** - Server-side session helper for App Router
|
|
483
|
+
- **setServerSessionCookie()** - Helper to sync auth state to server cookies
|
|
484
|
+
- **retryWithBackoff()** - Exponential backoff retry utility for token acquisition
|
|
485
|
+
- **createRetryWrapper()** - Create reusable retry wrappers for functions
|
|
486
|
+
- **getDebugLogger()** - Comprehensive debug logger with levels and scoping
|
|
487
|
+
- **createScopedLogger()** - Create loggers with custom prefixes
|
|
488
|
+
|
|
489
|
+
### 🔒 Middleware
|
|
490
|
+
|
|
491
|
+
- **createAuthMiddleware()** - Edge-compatible middleware for protecting routes
|
|
492
|
+
- Support for protected routes and public-only routes
|
|
493
|
+
- Custom authentication checks
|
|
494
|
+
- Automatic redirects with return URLs
|
|
495
|
+
- Debug mode with detailed logging
|
|
496
|
+
|
|
497
|
+
### 🎨 Developer Experience
|
|
498
|
+
|
|
499
|
+
- **Debug Mode** - Clear console logs with troubleshooting hints
|
|
500
|
+
- **Better Error Messages** - Descriptive errors with actionable solutions
|
|
501
|
+
- **TypeScript Generics** - Support for custom token claims via `CustomTokenClaims` interface
|
|
502
|
+
- **JSDoc Comments** - Comprehensive documentation on all exports
|
|
503
|
+
- **Example Code** - API route and middleware examples included
|
|
504
|
+
|
|
505
|
+
### 🏗️ Production Ready
|
|
506
|
+
|
|
507
|
+
- **Error Boundaries** - Graceful error handling with recovery options
|
|
508
|
+
- **Token Refresh Retry** - Exponential backoff with configurable retries
|
|
509
|
+
- **Multiple Account Support** - Handle multiple signed-in accounts
|
|
510
|
+
- **SSR/Hydration Safe** - Proper 'use client' boundaries and SSR guards
|
|
511
|
+
- **Caching** - Built-in caching for user profiles and roles (5-minute TTL)
|
|
512
|
+
|
|
513
|
+
### 🧪 Testing
|
|
514
|
+
|
|
515
|
+
- **Unit Tests** - Comprehensive test suite with >80% coverage target
|
|
516
|
+
- **Vitest Configuration** - Modern testing setup with coverage reporting
|
|
517
|
+
- **Test Utilities** - Mock helpers and test setup included
|
|
518
|
+
|
|
519
|
+
### 📚 Documentation
|
|
520
|
+
|
|
521
|
+
- **Comprehensive README** - Complete guide with examples for all features
|
|
522
|
+
- **Migration Guide** - Backward compatible with v1.x
|
|
523
|
+
- **TypeScript Examples** - Type-safe examples for custom claims
|
|
524
|
+
- **Troubleshooting Guide** - Common issues and solutions
|
|
525
|
+
|
|
526
|
+
### 🔄 Breaking Changes
|
|
527
|
+
|
|
528
|
+
None! This release is fully backward compatible with v1.x.
|
|
529
|
+
|
|
530
|
+
### 🐛 Bug Fixes
|
|
531
|
+
|
|
532
|
+
- Fixed SSR hydration issues with proper client-side guards
|
|
533
|
+
- Improved token refresh reliability with retry logic
|
|
534
|
+
- Better error handling for MS Graph photo fetch failures
|
|
535
|
+
|
|
536
|
+
### ⚡ Performance
|
|
537
|
+
|
|
538
|
+
- Added 5-minute caching for user profiles and roles
|
|
539
|
+
- Optimized token acquisition with silent refresh
|
|
540
|
+
- Reduced bundle size with tree-shakeable exports
|
|
541
|
+
|
|
542
|
+
### 📦 Dependencies
|
|
543
|
+
|
|
544
|
+
- Added `vitest` for testing
|
|
545
|
+
- Added `@testing-library/react` for component testing
|
|
546
|
+
- Updated peer dependencies to support latest MSAL versions
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## [1.2.1] - Previous Release
|
|
551
|
+
|
|
552
|
+
Previous changelog entries...
|
|
553
|
+
|
|
554
|
+
## [1.2.0] - 2024-02-23
|
|
555
|
+
|
|
556
|
+
### Added
|
|
557
|
+
- **onInitialized callback** - Access MSAL instance after initialization for setting up interceptors
|
|
558
|
+
- **getMsalInstance() utility** - Access MSAL instance outside React components (API clients, middleware)
|
|
559
|
+
- **clearSession() method** - Clear MSAL cache without triggering Microsoft logout redirect
|
|
560
|
+
- **SSR safety guards** - Automatic detection and handling of server-side rendering
|
|
561
|
+
- **UseMsalAuthReturn type export** - Type interface for hook return value
|
|
562
|
+
|
|
563
|
+
### Changed
|
|
564
|
+
- **Peer dependencies** - Now supports both v3 and v4 of `@azure/msal-browser` and v2/v3 of `@azure/msal-react`
|
|
565
|
+
- **Logging behavior** - Console logs now respect `enableLogging` config (errors always log)
|
|
566
|
+
- Enhanced README with advanced usage examples (Axios interceptors, API clients, silent logout)
|
|
567
|
+
- Improved TypeScript type exports
|
|
568
|
+
|
|
569
|
+
## [1.1.0] - 2024-02-18
|
|
570
|
+
|
|
571
|
+
### Added
|
|
572
|
+
- **MicrosoftSignInButton** component with official Microsoft branding
|
|
573
|
+
- Dark and light variants
|
|
574
|
+
- Three size options (small, medium, large)
|
|
575
|
+
- Popup and redirect flow support
|
|
576
|
+
- Custom scopes support
|
|
577
|
+
- Success/error callbacks
|
|
578
|
+
- Fully customizable with className and style props
|
|
579
|
+
- Loading and disabled states
|
|
580
|
+
|
|
581
|
+
### Changed
|
|
582
|
+
- Improved TypeScript type exports
|
|
583
|
+
- Enhanced documentation with button component examples
|
|
584
|
+
|
|
585
|
+
## [1.0.0] - 2024-02-18
|
|
586
|
+
|
|
587
|
+
### Added
|
|
588
|
+
- Initial release of `@chemmangat/msal-next`
|
|
589
|
+
- `MsalAuthProvider` component for Next.js App Router
|
|
590
|
+
- `useMsalAuth` hook with comprehensive authentication methods
|
|
591
|
+
- Support for popup and redirect authentication flows
|
|
592
|
+
- Automatic token acquisition with silent refresh
|
|
593
|
+
- Multi-tenant and single-tenant authentication support
|
|
594
|
+
- Configurable cache location (sessionStorage, localStorage, memoryStorage)
|
|
595
|
+
- Custom loading component support
|
|
596
|
+
- Debug logging support
|
|
597
|
+
- TypeScript support with full type definitions
|
|
598
|
+
- Comprehensive documentation and examples
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Release Notes - v3.0.8 (Critical Bug Fix)
|
|
2
|
+
|
|
3
|
+
## 🚨 Critical Update Required
|
|
4
|
+
|
|
5
|
+
If you're using v3.0.6 or v3.0.7, please update immediately. These versions have a critical bug that breaks popup authentication.
|
|
6
|
+
|
|
7
|
+
## What Happened?
|
|
8
|
+
|
|
9
|
+
In v3.0.6, we attempted to fix a popup redirect issue by skipping `handleRedirectPromise()` in popup windows. This was incorrect and broke the authentication flow for popup-based logins.
|
|
10
|
+
|
|
11
|
+
## The Fix
|
|
12
|
+
|
|
13
|
+
v3.0.8 properly handles the popup flow by:
|
|
14
|
+
1. Calling `handleRedirectPromise()` in ALL windows (main and popup)
|
|
15
|
+
2. Only cleaning the URL in the main window
|
|
16
|
+
3. Allowing the popup to close naturally after MSAL processes the redirect
|
|
17
|
+
|
|
18
|
+
## Update Instructions
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @chemmangat/msal-next@3.0.8
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No code changes required. Your existing implementation will work correctly.
|
|
25
|
+
|
|
26
|
+
## Verified Scenarios
|
|
27
|
+
|
|
28
|
+
### ✅ Working
|
|
29
|
+
- Popup login flow
|
|
30
|
+
- Redirect login flow
|
|
31
|
+
- User cancellation
|
|
32
|
+
- Page refresh during auth
|
|
33
|
+
- Multiple tabs
|
|
34
|
+
- Token acquisition
|
|
35
|
+
- Logout flows
|
|
36
|
+
- SSR/hydration
|
|
37
|
+
|
|
38
|
+
### ✅ Fixed Issues
|
|
39
|
+
- Popup not closing after authentication
|
|
40
|
+
- URL showing auth code after login
|
|
41
|
+
- Button staying disabled after popup closes
|
|
42
|
+
- Redirect happening in popup window
|
|
43
|
+
|
|
44
|
+
## For Users on v2.x
|
|
45
|
+
|
|
46
|
+
If you're still on v2.x, you can safely upgrade to v3.0.8. All v2.x functionality is preserved with additional improvements.
|
|
47
|
+
|
|
48
|
+
## Support
|
|
49
|
+
|
|
50
|
+
If you encounter any issues:
|
|
51
|
+
1. Check the [Troubleshooting Guide](./TROUBLESHOOTING.md)
|
|
52
|
+
2. Review [Test Scenarios](./TEST_SCENARIOS.md)
|
|
53
|
+
3. Open an issue on GitHub with:
|
|
54
|
+
- Your version number
|
|
55
|
+
- Browser and OS
|
|
56
|
+
- Steps to reproduce
|
|
57
|
+
- Console errors
|
|
58
|
+
|
|
59
|
+
## Apology
|
|
60
|
+
|
|
61
|
+
We sincerely apologize for the disruption caused by v3.0.6 and v3.0.7. We've implemented additional testing procedures to prevent similar issues in the future.
|
|
62
|
+
|
|
63
|
+
## Next Steps
|
|
64
|
+
|
|
65
|
+
1. Update to v3.0.8
|
|
66
|
+
2. Test your authentication flows
|
|
67
|
+
3. Report any issues immediately
|
|
68
|
+
4. Consider implementing the test scenarios in your own testing
|
|
69
|
+
|
|
70
|
+
Thank you for your patience and continued use of @chemmangat/msal-next.
|
package/dist/index.js
CHANGED
|
@@ -208,15 +208,19 @@ function MsalAuthProvider({ children, loadingComponent, onInitialized, ...config
|
|
|
208
208
|
const msalConfig = createMsalConfig(config);
|
|
209
209
|
const instance = new import_msal_browser2.PublicClientApplication(msalConfig);
|
|
210
210
|
await instance.initialize();
|
|
211
|
+
const isInPopup = window.opener && window.opener !== window;
|
|
211
212
|
try {
|
|
212
213
|
const response = await instance.handleRedirectPromise();
|
|
213
214
|
if (response) {
|
|
214
215
|
if (config.enableLogging) {
|
|
215
|
-
console.log("[MSAL] Redirect authentication successful");
|
|
216
|
+
console.log("[MSAL] Redirect authentication successful", isInPopup ? "(popup)" : "(main)");
|
|
216
217
|
}
|
|
217
218
|
if (response.account) {
|
|
218
219
|
instance.setActiveAccount(response.account);
|
|
219
220
|
}
|
|
221
|
+
if (!isInPopup && window.location.hash) {
|
|
222
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
223
|
+
}
|
|
220
224
|
}
|
|
221
225
|
} catch (redirectError) {
|
|
222
226
|
if (redirectError?.errorCode === "no_token_request_cache_error") {
|
|
@@ -230,6 +234,9 @@ function MsalAuthProvider({ children, loadingComponent, onInitialized, ...config
|
|
|
230
234
|
} else {
|
|
231
235
|
console.error("[MSAL] Redirect handling error:", redirectError);
|
|
232
236
|
}
|
|
237
|
+
if (!isInPopup && window.location.hash && (window.location.hash.includes("code=") || window.location.hash.includes("error="))) {
|
|
238
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
239
|
+
}
|
|
233
240
|
}
|
|
234
241
|
const accounts = instance.getAllAccounts();
|
|
235
242
|
if (accounts.length > 0 && !instance.getActiveAccount()) {
|
|
@@ -481,6 +488,7 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
481
488
|
}
|
|
482
489
|
|
|
483
490
|
// src/components/MicrosoftSignInButton.tsx
|
|
491
|
+
var import_react3 = require("react");
|
|
484
492
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
485
493
|
function MicrosoftSignInButton({
|
|
486
494
|
text = "Sign in with Microsoft",
|
|
@@ -494,7 +502,9 @@ function MicrosoftSignInButton({
|
|
|
494
502
|
onError
|
|
495
503
|
}) {
|
|
496
504
|
const { loginPopup, loginRedirect, inProgress } = useMsalAuth();
|
|
505
|
+
const [isLoading, setIsLoading] = (0, import_react3.useState)(false);
|
|
497
506
|
const handleClick = async () => {
|
|
507
|
+
setIsLoading(true);
|
|
498
508
|
try {
|
|
499
509
|
if (useRedirect) {
|
|
500
510
|
await loginRedirect(scopes);
|
|
@@ -504,6 +514,8 @@ function MicrosoftSignInButton({
|
|
|
504
514
|
onSuccess?.();
|
|
505
515
|
} catch (error) {
|
|
506
516
|
onError?.(error);
|
|
517
|
+
} finally {
|
|
518
|
+
setTimeout(() => setIsLoading(false), 500);
|
|
507
519
|
}
|
|
508
520
|
};
|
|
509
521
|
const sizeStyles = {
|
|
@@ -535,6 +547,7 @@ function MicrosoftSignInButton({
|
|
|
535
547
|
border: "1px solid #8C8C8C"
|
|
536
548
|
}
|
|
537
549
|
};
|
|
550
|
+
const isDisabled = inProgress || isLoading;
|
|
538
551
|
const baseStyles = {
|
|
539
552
|
display: "inline-flex",
|
|
540
553
|
alignItems: "center",
|
|
@@ -543,9 +556,9 @@ function MicrosoftSignInButton({
|
|
|
543
556
|
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
544
557
|
fontWeight: 600,
|
|
545
558
|
borderRadius: "2px",
|
|
546
|
-
cursor:
|
|
559
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
547
560
|
transition: "all 0.2s ease",
|
|
548
|
-
opacity:
|
|
561
|
+
opacity: isDisabled ? 0.6 : 1,
|
|
549
562
|
...variantStyles[variant],
|
|
550
563
|
...sizeStyles[size],
|
|
551
564
|
...style
|
|
@@ -554,7 +567,7 @@ function MicrosoftSignInButton({
|
|
|
554
567
|
"button",
|
|
555
568
|
{
|
|
556
569
|
onClick: handleClick,
|
|
557
|
-
disabled:
|
|
570
|
+
disabled: isDisabled,
|
|
558
571
|
className,
|
|
559
572
|
style: baseStyles,
|
|
560
573
|
"aria-label": text,
|
|
@@ -668,16 +681,16 @@ function MicrosoftLogo2() {
|
|
|
668
681
|
}
|
|
669
682
|
|
|
670
683
|
// src/components/UserAvatar.tsx
|
|
671
|
-
var
|
|
684
|
+
var import_react6 = require("react");
|
|
672
685
|
|
|
673
686
|
// src/hooks/useUserProfile.ts
|
|
674
|
-
var
|
|
687
|
+
var import_react5 = require("react");
|
|
675
688
|
|
|
676
689
|
// src/hooks/useGraphApi.ts
|
|
677
|
-
var
|
|
690
|
+
var import_react4 = require("react");
|
|
678
691
|
function useGraphApi() {
|
|
679
692
|
const { acquireToken } = useMsalAuth();
|
|
680
|
-
const request = (0,
|
|
693
|
+
const request = (0, import_react4.useCallback)(
|
|
681
694
|
async (endpoint, options = {}) => {
|
|
682
695
|
const {
|
|
683
696
|
scopes = ["User.Read"],
|
|
@@ -721,13 +734,13 @@ function useGraphApi() {
|
|
|
721
734
|
},
|
|
722
735
|
[acquireToken]
|
|
723
736
|
);
|
|
724
|
-
const get = (0,
|
|
737
|
+
const get = (0, import_react4.useCallback)(
|
|
725
738
|
(endpoint, options = {}) => {
|
|
726
739
|
return request(endpoint, { ...options, method: "GET" });
|
|
727
740
|
},
|
|
728
741
|
[request]
|
|
729
742
|
);
|
|
730
|
-
const post = (0,
|
|
743
|
+
const post = (0, import_react4.useCallback)(
|
|
731
744
|
(endpoint, body, options = {}) => {
|
|
732
745
|
return request(endpoint, {
|
|
733
746
|
...options,
|
|
@@ -737,7 +750,7 @@ function useGraphApi() {
|
|
|
737
750
|
},
|
|
738
751
|
[request]
|
|
739
752
|
);
|
|
740
|
-
const put = (0,
|
|
753
|
+
const put = (0, import_react4.useCallback)(
|
|
741
754
|
(endpoint, body, options = {}) => {
|
|
742
755
|
return request(endpoint, {
|
|
743
756
|
...options,
|
|
@@ -747,7 +760,7 @@ function useGraphApi() {
|
|
|
747
760
|
},
|
|
748
761
|
[request]
|
|
749
762
|
);
|
|
750
|
-
const patch = (0,
|
|
763
|
+
const patch = (0, import_react4.useCallback)(
|
|
751
764
|
(endpoint, body, options = {}) => {
|
|
752
765
|
return request(endpoint, {
|
|
753
766
|
...options,
|
|
@@ -757,7 +770,7 @@ function useGraphApi() {
|
|
|
757
770
|
},
|
|
758
771
|
[request]
|
|
759
772
|
);
|
|
760
|
-
const deleteRequest = (0,
|
|
773
|
+
const deleteRequest = (0, import_react4.useCallback)(
|
|
761
774
|
(endpoint, options = {}) => {
|
|
762
775
|
return request(endpoint, { ...options, method: "DELETE" });
|
|
763
776
|
},
|
|
@@ -794,10 +807,10 @@ function enforceCacheLimit() {
|
|
|
794
807
|
function useUserProfile() {
|
|
795
808
|
const { isAuthenticated, account } = useMsalAuth();
|
|
796
809
|
const graph = useGraphApi();
|
|
797
|
-
const [profile, setProfile] = (0,
|
|
798
|
-
const [loading, setLoading] = (0,
|
|
799
|
-
const [error, setError] = (0,
|
|
800
|
-
const fetchProfile = (0,
|
|
810
|
+
const [profile, setProfile] = (0, import_react5.useState)(null);
|
|
811
|
+
const [loading, setLoading] = (0, import_react5.useState)(false);
|
|
812
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
813
|
+
const fetchProfile = (0, import_react5.useCallback)(async () => {
|
|
801
814
|
if (!isAuthenticated || !account) {
|
|
802
815
|
setProfile(null);
|
|
803
816
|
return;
|
|
@@ -857,7 +870,7 @@ function useUserProfile() {
|
|
|
857
870
|
setLoading(false);
|
|
858
871
|
}
|
|
859
872
|
}, [isAuthenticated, account, graph]);
|
|
860
|
-
const clearCache = (0,
|
|
873
|
+
const clearCache = (0, import_react5.useCallback)(() => {
|
|
861
874
|
if (account) {
|
|
862
875
|
const cached = profileCache.get(account.homeAccountId);
|
|
863
876
|
if (cached?.data.photo) {
|
|
@@ -870,7 +883,7 @@ function useUserProfile() {
|
|
|
870
883
|
}
|
|
871
884
|
setProfile(null);
|
|
872
885
|
}, [account, profile]);
|
|
873
|
-
(0,
|
|
886
|
+
(0, import_react5.useEffect)(() => {
|
|
874
887
|
fetchProfile();
|
|
875
888
|
return () => {
|
|
876
889
|
if (profile?.photo) {
|
|
@@ -878,7 +891,7 @@ function useUserProfile() {
|
|
|
878
891
|
}
|
|
879
892
|
};
|
|
880
893
|
}, [fetchProfile]);
|
|
881
|
-
(0,
|
|
894
|
+
(0, import_react5.useEffect)(() => {
|
|
882
895
|
return () => {
|
|
883
896
|
if (profile?.photo) {
|
|
884
897
|
URL.revokeObjectURL(profile.photo);
|
|
@@ -904,9 +917,9 @@ function UserAvatar({
|
|
|
904
917
|
fallbackImage
|
|
905
918
|
}) {
|
|
906
919
|
const { profile, loading } = useUserProfile();
|
|
907
|
-
const [photoUrl, setPhotoUrl] = (0,
|
|
908
|
-
const [photoError, setPhotoError] = (0,
|
|
909
|
-
(0,
|
|
920
|
+
const [photoUrl, setPhotoUrl] = (0, import_react6.useState)(null);
|
|
921
|
+
const [photoError, setPhotoError] = (0, import_react6.useState)(false);
|
|
922
|
+
(0, import_react6.useEffect)(() => {
|
|
910
923
|
if (profile?.photo) {
|
|
911
924
|
setPhotoUrl(profile.photo);
|
|
912
925
|
}
|
|
@@ -1072,7 +1085,7 @@ function StatusIndicator({ color }) {
|
|
|
1072
1085
|
}
|
|
1073
1086
|
|
|
1074
1087
|
// src/components/AuthGuard.tsx
|
|
1075
|
-
var
|
|
1088
|
+
var import_react7 = require("react");
|
|
1076
1089
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1077
1090
|
function AuthGuard({
|
|
1078
1091
|
children,
|
|
@@ -1083,7 +1096,7 @@ function AuthGuard({
|
|
|
1083
1096
|
onAuthRequired
|
|
1084
1097
|
}) {
|
|
1085
1098
|
const { isAuthenticated, inProgress, loginRedirect, loginPopup } = useMsalAuth();
|
|
1086
|
-
(0,
|
|
1099
|
+
(0, import_react7.useEffect)(() => {
|
|
1087
1100
|
if (!isAuthenticated && !inProgress) {
|
|
1088
1101
|
onAuthRequired?.();
|
|
1089
1102
|
const login = async () => {
|
|
@@ -1110,9 +1123,9 @@ function AuthGuard({
|
|
|
1110
1123
|
}
|
|
1111
1124
|
|
|
1112
1125
|
// src/components/ErrorBoundary.tsx
|
|
1113
|
-
var
|
|
1126
|
+
var import_react8 = require("react");
|
|
1114
1127
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1115
|
-
var ErrorBoundary = class extends
|
|
1128
|
+
var ErrorBoundary = class extends import_react8.Component {
|
|
1116
1129
|
constructor(props) {
|
|
1117
1130
|
super(props);
|
|
1118
1131
|
this.reset = () => {
|
|
@@ -1188,7 +1201,7 @@ var ErrorBoundary = class extends import_react7.Component {
|
|
|
1188
1201
|
};
|
|
1189
1202
|
|
|
1190
1203
|
// src/hooks/useRoles.ts
|
|
1191
|
-
var
|
|
1204
|
+
var import_react9 = require("react");
|
|
1192
1205
|
var rolesCache = /* @__PURE__ */ new Map();
|
|
1193
1206
|
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
1194
1207
|
var MAX_CACHE_SIZE2 = 100;
|
|
@@ -1210,11 +1223,11 @@ function enforceCacheLimit2() {
|
|
|
1210
1223
|
function useRoles() {
|
|
1211
1224
|
const { isAuthenticated, account } = useMsalAuth();
|
|
1212
1225
|
const graph = useGraphApi();
|
|
1213
|
-
const [roles, setRoles] = (0,
|
|
1214
|
-
const [groups, setGroups] = (0,
|
|
1215
|
-
const [loading, setLoading] = (0,
|
|
1216
|
-
const [error, setError] = (0,
|
|
1217
|
-
const fetchRolesAndGroups = (0,
|
|
1226
|
+
const [roles, setRoles] = (0, import_react9.useState)([]);
|
|
1227
|
+
const [groups, setGroups] = (0, import_react9.useState)([]);
|
|
1228
|
+
const [loading, setLoading] = (0, import_react9.useState)(false);
|
|
1229
|
+
const [error, setError] = (0, import_react9.useState)(null);
|
|
1230
|
+
const fetchRolesAndGroups = (0, import_react9.useCallback)(async () => {
|
|
1218
1231
|
if (!isAuthenticated || !account) {
|
|
1219
1232
|
setRoles([]);
|
|
1220
1233
|
setGroups([]);
|
|
@@ -1257,31 +1270,31 @@ function useRoles() {
|
|
|
1257
1270
|
setLoading(false);
|
|
1258
1271
|
}
|
|
1259
1272
|
}, [isAuthenticated, account, graph]);
|
|
1260
|
-
const hasRole = (0,
|
|
1273
|
+
const hasRole = (0, import_react9.useCallback)(
|
|
1261
1274
|
(role) => {
|
|
1262
1275
|
return roles.includes(role);
|
|
1263
1276
|
},
|
|
1264
1277
|
[roles]
|
|
1265
1278
|
);
|
|
1266
|
-
const hasGroup = (0,
|
|
1279
|
+
const hasGroup = (0, import_react9.useCallback)(
|
|
1267
1280
|
(groupId) => {
|
|
1268
1281
|
return groups.includes(groupId);
|
|
1269
1282
|
},
|
|
1270
1283
|
[groups]
|
|
1271
1284
|
);
|
|
1272
|
-
const hasAnyRole = (0,
|
|
1285
|
+
const hasAnyRole = (0, import_react9.useCallback)(
|
|
1273
1286
|
(checkRoles) => {
|
|
1274
1287
|
return checkRoles.some((role) => roles.includes(role));
|
|
1275
1288
|
},
|
|
1276
1289
|
[roles]
|
|
1277
1290
|
);
|
|
1278
|
-
const hasAllRoles = (0,
|
|
1291
|
+
const hasAllRoles = (0, import_react9.useCallback)(
|
|
1279
1292
|
(checkRoles) => {
|
|
1280
1293
|
return checkRoles.every((role) => roles.includes(role));
|
|
1281
1294
|
},
|
|
1282
1295
|
[roles]
|
|
1283
1296
|
);
|
|
1284
|
-
(0,
|
|
1297
|
+
(0, import_react9.useEffect)(() => {
|
|
1285
1298
|
fetchRolesAndGroups();
|
|
1286
1299
|
return () => {
|
|
1287
1300
|
if (account) {
|
package/dist/index.mjs
CHANGED
|
@@ -155,15 +155,19 @@ function MsalAuthProvider({ children, loadingComponent, onInitialized, ...config
|
|
|
155
155
|
const msalConfig = createMsalConfig(config);
|
|
156
156
|
const instance = new PublicClientApplication(msalConfig);
|
|
157
157
|
await instance.initialize();
|
|
158
|
+
const isInPopup = window.opener && window.opener !== window;
|
|
158
159
|
try {
|
|
159
160
|
const response = await instance.handleRedirectPromise();
|
|
160
161
|
if (response) {
|
|
161
162
|
if (config.enableLogging) {
|
|
162
|
-
console.log("[MSAL] Redirect authentication successful");
|
|
163
|
+
console.log("[MSAL] Redirect authentication successful", isInPopup ? "(popup)" : "(main)");
|
|
163
164
|
}
|
|
164
165
|
if (response.account) {
|
|
165
166
|
instance.setActiveAccount(response.account);
|
|
166
167
|
}
|
|
168
|
+
if (!isInPopup && window.location.hash) {
|
|
169
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
170
|
+
}
|
|
167
171
|
}
|
|
168
172
|
} catch (redirectError) {
|
|
169
173
|
if (redirectError?.errorCode === "no_token_request_cache_error") {
|
|
@@ -177,6 +181,9 @@ function MsalAuthProvider({ children, loadingComponent, onInitialized, ...config
|
|
|
177
181
|
} else {
|
|
178
182
|
console.error("[MSAL] Redirect handling error:", redirectError);
|
|
179
183
|
}
|
|
184
|
+
if (!isInPopup && window.location.hash && (window.location.hash.includes("code=") || window.location.hash.includes("error="))) {
|
|
185
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
186
|
+
}
|
|
180
187
|
}
|
|
181
188
|
const accounts = instance.getAllAccounts();
|
|
182
189
|
if (accounts.length > 0 && !instance.getActiveAccount()) {
|
|
@@ -428,6 +435,7 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
428
435
|
}
|
|
429
436
|
|
|
430
437
|
// src/components/MicrosoftSignInButton.tsx
|
|
438
|
+
import { useState as useState2 } from "react";
|
|
431
439
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
432
440
|
function MicrosoftSignInButton({
|
|
433
441
|
text = "Sign in with Microsoft",
|
|
@@ -441,7 +449,9 @@ function MicrosoftSignInButton({
|
|
|
441
449
|
onError
|
|
442
450
|
}) {
|
|
443
451
|
const { loginPopup, loginRedirect, inProgress } = useMsalAuth();
|
|
452
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
444
453
|
const handleClick = async () => {
|
|
454
|
+
setIsLoading(true);
|
|
445
455
|
try {
|
|
446
456
|
if (useRedirect) {
|
|
447
457
|
await loginRedirect(scopes);
|
|
@@ -451,6 +461,8 @@ function MicrosoftSignInButton({
|
|
|
451
461
|
onSuccess?.();
|
|
452
462
|
} catch (error) {
|
|
453
463
|
onError?.(error);
|
|
464
|
+
} finally {
|
|
465
|
+
setTimeout(() => setIsLoading(false), 500);
|
|
454
466
|
}
|
|
455
467
|
};
|
|
456
468
|
const sizeStyles = {
|
|
@@ -482,6 +494,7 @@ function MicrosoftSignInButton({
|
|
|
482
494
|
border: "1px solid #8C8C8C"
|
|
483
495
|
}
|
|
484
496
|
};
|
|
497
|
+
const isDisabled = inProgress || isLoading;
|
|
485
498
|
const baseStyles = {
|
|
486
499
|
display: "inline-flex",
|
|
487
500
|
alignItems: "center",
|
|
@@ -490,9 +503,9 @@ function MicrosoftSignInButton({
|
|
|
490
503
|
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
491
504
|
fontWeight: 600,
|
|
492
505
|
borderRadius: "2px",
|
|
493
|
-
cursor:
|
|
506
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
494
507
|
transition: "all 0.2s ease",
|
|
495
|
-
opacity:
|
|
508
|
+
opacity: isDisabled ? 0.6 : 1,
|
|
496
509
|
...variantStyles[variant],
|
|
497
510
|
...sizeStyles[size],
|
|
498
511
|
...style
|
|
@@ -501,7 +514,7 @@ function MicrosoftSignInButton({
|
|
|
501
514
|
"button",
|
|
502
515
|
{
|
|
503
516
|
onClick: handleClick,
|
|
504
|
-
disabled:
|
|
517
|
+
disabled: isDisabled,
|
|
505
518
|
className,
|
|
506
519
|
style: baseStyles,
|
|
507
520
|
"aria-label": text,
|
|
@@ -615,10 +628,10 @@ function MicrosoftLogo2() {
|
|
|
615
628
|
}
|
|
616
629
|
|
|
617
630
|
// src/components/UserAvatar.tsx
|
|
618
|
-
import { useEffect as useEffect3, useState as
|
|
631
|
+
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
619
632
|
|
|
620
633
|
// src/hooks/useUserProfile.ts
|
|
621
|
-
import { useState as
|
|
634
|
+
import { useState as useState3, useEffect as useEffect2, useCallback as useCallback3 } from "react";
|
|
622
635
|
|
|
623
636
|
// src/hooks/useGraphApi.ts
|
|
624
637
|
import { useCallback as useCallback2 } from "react";
|
|
@@ -741,9 +754,9 @@ function enforceCacheLimit() {
|
|
|
741
754
|
function useUserProfile() {
|
|
742
755
|
const { isAuthenticated, account } = useMsalAuth();
|
|
743
756
|
const graph = useGraphApi();
|
|
744
|
-
const [profile, setProfile] =
|
|
745
|
-
const [loading, setLoading] =
|
|
746
|
-
const [error, setError] =
|
|
757
|
+
const [profile, setProfile] = useState3(null);
|
|
758
|
+
const [loading, setLoading] = useState3(false);
|
|
759
|
+
const [error, setError] = useState3(null);
|
|
747
760
|
const fetchProfile = useCallback3(async () => {
|
|
748
761
|
if (!isAuthenticated || !account) {
|
|
749
762
|
setProfile(null);
|
|
@@ -851,8 +864,8 @@ function UserAvatar({
|
|
|
851
864
|
fallbackImage
|
|
852
865
|
}) {
|
|
853
866
|
const { profile, loading } = useUserProfile();
|
|
854
|
-
const [photoUrl, setPhotoUrl] =
|
|
855
|
-
const [photoError, setPhotoError] =
|
|
867
|
+
const [photoUrl, setPhotoUrl] = useState4(null);
|
|
868
|
+
const [photoError, setPhotoError] = useState4(false);
|
|
856
869
|
useEffect3(() => {
|
|
857
870
|
if (profile?.photo) {
|
|
858
871
|
setPhotoUrl(profile.photo);
|
|
@@ -1135,7 +1148,7 @@ var ErrorBoundary = class extends Component {
|
|
|
1135
1148
|
};
|
|
1136
1149
|
|
|
1137
1150
|
// src/hooks/useRoles.ts
|
|
1138
|
-
import { useState as
|
|
1151
|
+
import { useState as useState5, useEffect as useEffect5, useCallback as useCallback4 } from "react";
|
|
1139
1152
|
var rolesCache = /* @__PURE__ */ new Map();
|
|
1140
1153
|
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
1141
1154
|
var MAX_CACHE_SIZE2 = 100;
|
|
@@ -1157,10 +1170,10 @@ function enforceCacheLimit2() {
|
|
|
1157
1170
|
function useRoles() {
|
|
1158
1171
|
const { isAuthenticated, account } = useMsalAuth();
|
|
1159
1172
|
const graph = useGraphApi();
|
|
1160
|
-
const [roles, setRoles] =
|
|
1161
|
-
const [groups, setGroups] =
|
|
1162
|
-
const [loading, setLoading] =
|
|
1163
|
-
const [error, setError] =
|
|
1173
|
+
const [roles, setRoles] = useState5([]);
|
|
1174
|
+
const [groups, setGroups] = useState5([]);
|
|
1175
|
+
const [loading, setLoading] = useState5(false);
|
|
1176
|
+
const [error, setError] = useState5(null);
|
|
1164
1177
|
const fetchRolesAndGroups = useCallback4(async () => {
|
|
1165
1178
|
if (!isAuthenticated || !account) {
|
|
1166
1179
|
setRoles([]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemmangat/msal-next",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
4
4
|
"description": "Production-grade MSAL authentication package for Next.js App Router with minimal boilerplate",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"dist",
|
|
22
22
|
"README.md",
|
|
23
23
|
"SECURITY.md",
|
|
24
|
-
"TROUBLESHOOTING.md"
|
|
24
|
+
"TROUBLESHOOTING.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"RELEASE_NOTES_v3.0.8.md"
|
|
25
27
|
],
|
|
26
28
|
"scripts": {
|
|
27
29
|
"build": "tsup",
|