@chemmangat/msal-next 5.0.1 → 5.1.0

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 CHANGED
@@ -2,6 +2,92 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [5.1.0] - 2026-03-17
6
+
7
+ ### ✨ New Features
8
+
9
+ #### 1. Multi-Tenant Support — `multiTenant` config
10
+
11
+ Pass a `multiTenant` object to `MSALProvider` to control which tenants can access your app:
12
+
13
+ ```tsx
14
+ <MSALProvider
15
+ clientId="..."
16
+ multiTenant={{
17
+ type: 'multi', // 'single' | 'multi' | 'organizations' | 'consumers' | 'common'
18
+ allowList: ['contoso.com', 'fabrikam.com'],
19
+ blockList: ['competitor.com'],
20
+ requireType: 'Member', // block B2B guests
21
+ requireMFA: true,
22
+ }}
23
+ onTenantDenied={(reason) => router.push(`/denied?reason=${reason}`)}
24
+ >
25
+ ```
26
+
27
+ - `type` maps to the MSAL authority (`single` → tenant, `multi`/`common` → common, etc.)
28
+ - `allowList` / `blockList` accept tenant IDs or domain names
29
+ - `requireType: 'Member'` blocks B2B guests; `'Guest'` allows only guests
30
+ - `requireMFA` checks the `amr` claim for MFA evidence
31
+ - Tenant validation runs automatically after redirect authentication
32
+
33
+ #### 2. `useTenant()` Hook
34
+
35
+ ```tsx
36
+ import { useTenant } from '@chemmangat/msal-next';
37
+
38
+ const { tenantId, tenantDomain, isGuestUser, homeTenantId, resourceTenantId, claims } = useTenant();
39
+ ```
40
+
41
+ Returns tenant context for the current user including B2B guest detection.
42
+
43
+ #### 3. Per-Page Tenant Restrictions
44
+
45
+ ```tsx
46
+ // app/admin/page.tsx
47
+ export const auth = {
48
+ required: true,
49
+ tenant: {
50
+ allowList: ['contoso.com'],
51
+ requireMFA: true,
52
+ },
53
+ };
54
+ ```
55
+
56
+ #### 4. Middleware Tenant Validation
57
+
58
+ ```ts
59
+ // middleware.ts
60
+ export const middleware = createAuthMiddleware({
61
+ protectedRoutes: ['/dashboard'],
62
+ tenantConfig: { allowList: ['contoso.com'] },
63
+ tenantDeniedPath: '/unauthorized',
64
+ });
65
+ ```
66
+
67
+ #### 5. Cross-Tenant Token Acquisition
68
+
69
+ ```tsx
70
+ const { acquireTokenForTenant } = useMsalAuth();
71
+ const token = await acquireTokenForTenant('target-tenant-id', ['User.Read']);
72
+ ```
73
+
74
+ #### 6. `useTenantConfig()` Hook
75
+
76
+ Access the `multiTenant` config from anywhere in the component tree:
77
+
78
+ ```tsx
79
+ import { useTenantConfig } from '@chemmangat/msal-next';
80
+ const config = useTenantConfig();
81
+ ```
82
+
83
+ ### 🔧 Internal
84
+
85
+ - `createMsalConfig` now maps `multiTenant.type` to the correct MSAL authority (takes precedence over legacy `authorityType`)
86
+ - `validateTenantAccess` utility exported for advanced use cases
87
+ - `TenantAuthConfig` type exported for per-page tenant config
88
+
89
+ ---
90
+
5
91
  ## [5.0.0] - 2026-03-16
6
92
 
7
93
  ### ⚠️ Breaking Changes
package/README.md CHANGED
@@ -6,7 +6,7 @@ Microsoft/Azure AD authentication for Next.js App Router. Minimal setup, full Ty
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Security](https://img.shields.io/badge/Security-A+-green.svg)](./SECURITY.md)
8
8
 
9
- **Current version: 5.0.0**
9
+ **Current version: 5.1.0**
10
10
 
11
11
  ---
12
12
 
@@ -223,6 +223,60 @@ Protects content and redirects unauthenticated users to login.
223
223
 
224
224
  ---
225
225
 
226
+ ## Multi-Tenant Support (v5.1.0)
227
+
228
+ ### Provider Configuration
229
+
230
+ ```tsx
231
+ <MSALProvider
232
+ clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
233
+ multiTenant={{
234
+ type: 'multi',
235
+ allowList: ['contoso.com', 'fabrikam.com'],
236
+ blockList: ['competitor.com'],
237
+ requireType: 'Member',
238
+ requireMFA: true,
239
+ }}
240
+ onTenantDenied={(reason) => router.push(`/denied?reason=${encodeURIComponent(reason)}`)}
241
+ >
242
+ ```
243
+
244
+ ### `useTenant()` Hook
245
+
246
+ ```tsx
247
+ import { useTenant } from '@chemmangat/msal-next';
248
+
249
+ const { tenantId, tenantDomain, isGuestUser, homeTenantId, resourceTenantId, claims } = useTenant();
250
+ ```
251
+
252
+ ### Per-Page Tenant Restrictions
253
+
254
+ ```tsx
255
+ export const auth = {
256
+ required: true,
257
+ tenant: { allowList: ['contoso.com'], requireMFA: true },
258
+ };
259
+ ```
260
+
261
+ ### Middleware Tenant Validation
262
+
263
+ ```ts
264
+ export const middleware = createAuthMiddleware({
265
+ protectedRoutes: ['/dashboard'],
266
+ tenantConfig: { allowList: ['contoso.com'] },
267
+ tenantDeniedPath: '/unauthorized',
268
+ });
269
+ ```
270
+
271
+ ### Cross-Tenant Token Acquisition
272
+
273
+ ```tsx
274
+ const { acquireTokenForTenant } = useMsalAuth();
275
+ const token = await acquireTokenForTenant('target-tenant-id', ['User.Read']);
276
+ ```
277
+
278
+ ---
279
+
226
280
  ### Hooks
227
281
 
228
282
  #### useMsalAuth()
@@ -239,6 +293,7 @@ const {
239
293
  acquireTokenSilent, // (scopes: string[]) => Promise<string> — silent only
240
294
  acquireTokenRedirect, // (scopes: string[]) => Promise<void>
241
295
  clearSession, // () => Promise<void> — clears cache without Microsoft logout
296
+ acquireTokenForTenant,// (tenantId: string, scopes: string[]) => Promise<string> — cross-tenant (v5.1.0)
242
297
  } = useMsalAuth();
243
298
  ```
244
299
 
package/dist/index.d.mts CHANGED
@@ -292,13 +292,66 @@ interface MsalAuthConfig {
292
292
  * Only used when autoRefreshToken is enabled.
293
293
  *
294
294
  * @defaultValue 300 (5 minutes)
295
+ */
296
+ refreshBeforeExpiry?: number;
297
+ /**
298
+ * Multi-tenant configuration (v5.1.0)
299
+ *
300
+ * @remarks
301
+ * Controls which tenants are allowed to access the application and how
302
+ * cross-tenant token acquisition is handled.
295
303
  *
296
304
  * @example
297
305
  * ```tsx
298
- * refreshBeforeExpiry={600} // Refresh 10 minutes before expiry
306
+ * <MSALProvider
307
+ * clientId="..."
308
+ * multiTenant={{
309
+ * type: 'multi',
310
+ * allowList: ['contoso.com', 'fabrikam.com'],
311
+ * requireMFA: true,
312
+ * }}
313
+ * >
299
314
  * ```
300
315
  */
301
- refreshBeforeExpiry?: number;
316
+ multiTenant?: MultiTenantConfig;
317
+ }
318
+ /**
319
+ * Multi-tenant configuration for v5.1.0
320
+ */
321
+ interface MultiTenantConfig {
322
+ /**
323
+ * Tenant mode
324
+ * - 'single' — only your own tenant (maps to authorityType 'tenant')
325
+ * - 'multi' — any Azure AD tenant (maps to authorityType 'common')
326
+ * - 'organizations' — any organisational tenant, no personal accounts
327
+ * - 'consumers' — Microsoft personal accounts only
328
+ * - 'common' — alias for 'multi'
329
+ *
330
+ * @defaultValue 'common'
331
+ */
332
+ type?: 'single' | 'multi' | 'organizations' | 'consumers' | 'common';
333
+ /**
334
+ * Tenant allow-list — only users from these tenant IDs or domains are permitted.
335
+ * Checked after authentication; users from other tenants are shown an error.
336
+ *
337
+ * @example ['contoso.com', '72f988bf-86f1-41af-91ab-2d7cd011db47']
338
+ */
339
+ allowList?: string[];
340
+ /**
341
+ * Tenant block-list — users from these tenant IDs or domains are denied.
342
+ * Takes precedence over allowList.
343
+ */
344
+ blockList?: string[];
345
+ /**
346
+ * Require a specific tenant type ('Member' | 'Guest').
347
+ * Useful to block B2B guests or to allow only guests.
348
+ */
349
+ requireType?: 'Member' | 'Guest';
350
+ /**
351
+ * Require MFA claim in the token (amr claim must contain 'mfa').
352
+ * @defaultValue false
353
+ */
354
+ requireMFA?: boolean;
302
355
  }
303
356
  /**
304
357
  * Props for MsalAuthProvider component
@@ -318,13 +371,38 @@ interface MsalAuthProviderProps extends MsalAuthConfig {
318
371
  * @returns The MSAL instance or null if not initialized
319
372
  */
320
373
  declare function getMsalInstance(): PublicClientApplication | null;
321
- declare function MsalAuthProvider({ children, loadingComponent, onInitialized, autoRefreshToken, refreshBeforeExpiry, ...config }: MsalAuthProviderProps): react_jsx_runtime.JSX.Element;
374
+ declare function MsalAuthProvider({ children, loadingComponent, onInitialized, autoRefreshToken, refreshBeforeExpiry, onTenantDenied, ...config }: MsalAuthProviderProps & {
375
+ onTenantDenied?: (reason: string) => void;
376
+ }): react_jsx_runtime.JSX.Element;
322
377
 
323
378
  /**
324
379
  * Zero-Config Protected Routes - Type Definitions
325
380
  * v4.0.0 Killer Feature
326
381
  */
327
382
 
383
+ /**
384
+ * Per-page tenant access configuration (v5.1.0)
385
+ */
386
+ interface TenantAuthConfig {
387
+ /**
388
+ * Only users from these tenant IDs or domains are permitted on this page.
389
+ * @example ['contoso.com', '72f988bf-86f1-41af-91ab-2d7cd011db47']
390
+ */
391
+ allowList?: string[];
392
+ /**
393
+ * Users from these tenant IDs or domains are denied on this page.
394
+ * Takes precedence over allowList.
395
+ */
396
+ blockList?: string[];
397
+ /**
398
+ * Require a specific account type ('Member' | 'Guest').
399
+ */
400
+ requireType?: 'Member' | 'Guest';
401
+ /**
402
+ * Require MFA claim in the token (amr must contain 'mfa').
403
+ */
404
+ requireMFA?: boolean;
405
+ }
328
406
  /**
329
407
  * Page-level auth configuration
330
408
  * Export this from your page to enable protection
@@ -334,109 +412,81 @@ declare function MsalAuthProvider({ children, loadingComponent, onInitialized, a
334
412
  * // app/dashboard/page.tsx
335
413
  * export const auth = { required: true };
336
414
  *
337
- * export default function Dashboard() {
338
- * return <div>Protected content</div>;
339
- * }
415
+ * // With tenant restriction (v5.1.0)
416
+ * export const auth = {
417
+ * required: true,
418
+ * tenant: { allowList: ['contoso.com'], requireMFA: true }
419
+ * };
340
420
  * ```
341
421
  */
342
422
  interface PageAuthConfig {
343
- /**
344
- * Whether authentication is required for this page
345
- * @default false
346
- */
423
+ /** Whether authentication is required for this page */
347
424
  required?: boolean;
348
425
  /**
349
426
  * Required roles for access (checks account.idTokenClaims.roles)
350
427
  * User must have at least one of these roles
351
- *
352
- * @example
353
- * ```tsx
354
- * export const auth = {
355
- * required: true,
356
- * roles: ['admin', 'editor']
357
- * };
358
- * ```
359
428
  */
360
429
  roles?: string[];
361
- /**
362
- * Custom redirect path when auth fails
363
- * @default '/login'
364
- */
430
+ /** Custom redirect path when auth fails */
365
431
  redirectTo?: string;
366
- /**
367
- * Custom loading component while checking auth
368
- */
432
+ /** Custom loading component while checking auth */
369
433
  loading?: ReactNode;
370
- /**
371
- * Custom unauthorized component (shown instead of redirect)
372
- */
434
+ /** Custom unauthorized component (shown instead of redirect) */
373
435
  unauthorized?: ReactNode;
374
436
  /**
375
- * Custom validation function
376
- * Return true to allow access, false to deny
377
- *
378
- * @example
379
- * ```tsx
380
- * export const auth = {
381
- * required: true,
382
- * validate: (account) => account.username.endsWith('@company.com')
383
- * };
384
- * ```
437
+ * Custom validation function.
438
+ * Return true to allow access, false to deny.
385
439
  */
386
440
  validate?: (account: any) => boolean | Promise<boolean>;
441
+ /**
442
+ * Per-page tenant access restrictions (v5.1.0).
443
+ * Checked after role validation.
444
+ */
445
+ tenant?: TenantAuthConfig;
387
446
  }
388
447
  /**
389
448
  * Global auth configuration for the provider
390
449
  */
391
450
  interface AuthProtectionConfig {
392
- /**
393
- * Default redirect path for unauthenticated users
394
- * @default '/login'
395
- */
451
+ /** Default redirect path for unauthenticated users */
396
452
  defaultRedirectTo?: string;
397
- /**
398
- * Default loading component
399
- */
453
+ /** Default loading component */
400
454
  defaultLoading?: ReactNode;
401
- /**
402
- * Default unauthorized component
403
- */
455
+ /** Default unauthorized component */
404
456
  defaultUnauthorized?: ReactNode;
405
- /**
406
- * Enable debug logging
407
- * @default false
408
- */
457
+ /** Enable debug logging */
409
458
  debug?: boolean;
410
459
  }
411
460
 
412
461
  interface MSALProviderProps extends MsalAuthProviderProps {
413
462
  /**
414
463
  * Zero-Config Protected Routes configuration (v4.0.0)
415
- * @example
416
- * ```tsx
417
- * <MSALProvider
418
- * clientId="..."
419
- * protection={{
420
- * defaultRedirectTo: '/login',
421
- * defaultLoading: <Spinner />,
422
- * debug: true
423
- * }}
424
- * >
425
- * ```
426
464
  */
427
465
  protection?: AuthProtectionConfig;
466
+ /**
467
+ * Called when a user's tenant is denied access (v5.1.0)
468
+ */
469
+ onTenantDenied?: (reason: string) => void;
428
470
  }
471
+ /**
472
+ * Access the multi-tenant configuration from anywhere in the tree (v5.1.0).
473
+ *
474
+ * @example
475
+ * ```tsx
476
+ * const tenantConfig = useTenantConfig();
477
+ * console.log(tenantConfig?.allowList);
478
+ * ```
479
+ */
480
+ declare function useTenantConfig(): MultiTenantConfig | undefined;
429
481
  /**
430
482
  * Pre-configured MSALProvider component for Next.js App Router layouts.
431
483
  *
432
484
  * @remarks
433
- * This component is already marked as 'use client' internally, so you can import
434
- * and use it directly in your server-side layout.tsx without adding 'use client'
435
- * to your layout file.
485
+ * Already marked as 'use client' internally safe to import in server layouts.
436
486
  *
437
487
  * @example
438
488
  * ```tsx
439
- * // app/layout.tsx (Server Component - no 'use client' needed!)
489
+ * // app/layout.tsx (Server Component)
440
490
  * import { MSALProvider } from '@chemmangat/msal-next'
441
491
  *
442
492
  * export default function RootLayout({ children }) {
@@ -446,6 +496,7 @@ interface MSALProviderProps extends MsalAuthProviderProps {
446
496
  * <MSALProvider
447
497
  * clientId={process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!}
448
498
  * tenantId={process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID!}
499
+ * multiTenant={{ type: 'multi', allowList: ['contoso.com'] }}
449
500
  * >
450
501
  * {children}
451
502
  * </MSALProvider>
@@ -454,15 +505,8 @@ interface MSALProviderProps extends MsalAuthProviderProps {
454
505
  * )
455
506
  * }
456
507
  * ```
457
- *
458
- * @security
459
- * - All authentication happens client-side (browser)
460
- * - Tokens are never sent to your Next.js server
461
- * - Uses Microsoft's official MSAL library
462
- * - Supports secure token storage (sessionStorage/localStorage)
463
- * - No server-side token handling required
464
508
  */
465
- declare function MSALProvider({ children, protection, ...props }: MSALProviderProps): react_jsx_runtime.JSX.Element;
509
+ declare function MSALProvider({ children, protection, onTenantDenied, ...props }: MSALProviderProps): react_jsx_runtime.JSX.Element;
466
510
 
467
511
  interface MicrosoftSignInButtonProps {
468
512
  /**
@@ -921,9 +965,62 @@ interface UseMsalAuthReturn {
921
965
  * Clear MSAL session without triggering Microsoft logout
922
966
  */
923
967
  clearSession: () => Promise<void>;
968
+ /**
969
+ * Acquire an access token for a specific tenant (cross-tenant, v5.1.0).
970
+ * Uses acquireTokenSilent with an explicit authority for the target tenant.
971
+ */
972
+ acquireTokenForTenant: (tenantId: string, scopes: string[]) => Promise<string>;
924
973
  }
925
974
  declare function useMsalAuth(defaultScopes?: string[]): UseMsalAuthReturn;
926
975
 
976
+ interface TenantInfo {
977
+ /** The tenant ID from the current account's token claims */
978
+ tenantId: string | null;
979
+ /** The tenant domain (e.g. contoso.onmicrosoft.com) derived from the UPN */
980
+ tenantDomain: string | null;
981
+ /**
982
+ * Whether the current user is a B2B guest in this tenant.
983
+ * True when the home tenant (iss claim) differs from the resource tenant (tid claim).
984
+ */
985
+ isGuestUser: boolean;
986
+ /** The user's home tenant ID (where their identity lives) */
987
+ homeTenantId: string | null;
988
+ /** The resource tenant ID (the tenant the token was issued for) */
989
+ resourceTenantId: string | null;
990
+ /** Raw idTokenClaims for advanced usage */
991
+ claims: Record<string, any> | null;
992
+ }
993
+ interface UseTenantReturn extends TenantInfo {
994
+ /** Whether the user is authenticated */
995
+ isAuthenticated: boolean;
996
+ }
997
+ /**
998
+ * Hook that exposes tenant context for the currently authenticated user.
999
+ *
1000
+ * @remarks
1001
+ * Detects B2B guest users by comparing the `iss` (issuer / home tenant) claim
1002
+ * against the `tid` (resource tenant) claim. When they differ the user is a
1003
+ * cross-tenant guest.
1004
+ *
1005
+ * @example
1006
+ * ```tsx
1007
+ * 'use client';
1008
+ * import { useTenant } from '@chemmangat/msal-next';
1009
+ *
1010
+ * export default function TenantBadge() {
1011
+ * const { tenantDomain, isGuestUser, tenantId } = useTenant();
1012
+ *
1013
+ * return (
1014
+ * <div>
1015
+ * <span>{tenantDomain}</span>
1016
+ * {isGuestUser && <span className="badge">Guest</span>}
1017
+ * </div>
1018
+ * );
1019
+ * }
1020
+ * ```
1021
+ */
1022
+ declare function useTenant(): UseTenantReturn;
1023
+
927
1024
  interface GraphApiOptions extends RequestInit {
928
1025
  /**
929
1026
  * Scopes required for the API call
@@ -1798,6 +1895,16 @@ interface AuthMiddlewareConfig {
1798
1895
  * Custom authentication check function
1799
1896
  */
1800
1897
  isAuthenticated?: (request: NextRequest) => boolean | Promise<boolean>;
1898
+ /**
1899
+ * Tenant access configuration (v5.1.0).
1900
+ * Validated against the account stored in the session cookie.
1901
+ */
1902
+ tenantConfig?: MultiTenantConfig;
1903
+ /**
1904
+ * Path to redirect to when tenant access is denied (v5.1.0).
1905
+ * @default '/unauthorized'
1906
+ */
1907
+ tenantDeniedPath?: string;
1801
1908
  /**
1802
1909
  * Enable debug logging
1803
1910
  * @default false
@@ -1845,4 +1952,4 @@ interface ServerSession {
1845
1952
  accessToken?: string;
1846
1953
  }
1847
1954
 
1848
- export { AccountList, type AccountListProps, AccountSwitcher, type AccountSwitcherProps, AuthGuard, type AuthGuardProps, type AuthMiddlewareConfig, type AuthProtectionConfig, AuthStatus, type AuthStatusProps, type CustomTokenClaims, type DebugLoggerConfig, ErrorBoundary, type ErrorBoundaryProps, type GraphApiOptions, MSALProvider, MicrosoftSignInButton, type MicrosoftSignInButtonProps, type MsalAuthConfig, MsalAuthProvider, type MsalAuthProviderProps, MsalError, type PageAuthConfig, ProtectedPage, type RetryConfig, type ServerSession, SignOutButton, type SignOutButtonProps, type UseGraphApiReturn, type UseMsalAuthReturn, type UseMultiAccountReturn, type UseRolesReturn, type UseTokenRefreshOptions, type UseTokenRefreshReturn, type UseUserProfileReturn, UserAvatar, type UserAvatarProps, type UserProfile, type ValidatedAccountData, type ValidationError, type ValidationResult, type ValidationWarning, type WithAuthOptions, createAuthMiddleware, createMissingEnvVarError, createMsalConfig, createRetryWrapper, createScopedLogger, displayValidationResults, getDebugLogger, getMsalInstance, isValidAccountData, isValidRedirectUri, isValidScope, retryWithBackoff, safeJsonParse, sanitizeError, useGraphApi, useMsalAuth, useMultiAccount, useRoles, useTokenRefresh, useUserProfile, validateConfig, validateScopes, withAuth, withPageAuth, wrapMsalError };
1955
+ export { AccountList, type AccountListProps, AccountSwitcher, type AccountSwitcherProps, AuthGuard, type AuthGuardProps, type AuthMiddlewareConfig, type AuthProtectionConfig, AuthStatus, type AuthStatusProps, type CustomTokenClaims, type DebugLoggerConfig, ErrorBoundary, type ErrorBoundaryProps, type GraphApiOptions, MSALProvider, MicrosoftSignInButton, type MicrosoftSignInButtonProps, type MsalAuthConfig, MsalAuthProvider, type MsalAuthProviderProps, MsalError, type MultiTenantConfig, type PageAuthConfig, ProtectedPage, type RetryConfig, type ServerSession, SignOutButton, type SignOutButtonProps, type TenantAuthConfig, type TenantInfo, type UseGraphApiReturn, type UseMsalAuthReturn, type UseMultiAccountReturn, type UseRolesReturn, type UseTenantReturn, type UseTokenRefreshOptions, type UseTokenRefreshReturn, type UseUserProfileReturn, UserAvatar, type UserAvatarProps, type UserProfile, type ValidatedAccountData, type ValidationError, type ValidationResult, type ValidationWarning, type WithAuthOptions, createAuthMiddleware, createMissingEnvVarError, createMsalConfig, createRetryWrapper, createScopedLogger, displayValidationResults, getDebugLogger, getMsalInstance, isValidAccountData, isValidRedirectUri, isValidScope, retryWithBackoff, safeJsonParse, sanitizeError, useGraphApi, useMsalAuth, useMultiAccount, useRoles, useTenant, useTenantConfig, useTokenRefresh, useUserProfile, validateConfig, validateScopes, withAuth, withPageAuth, wrapMsalError };