@chemmangat/msal-next 5.0.0 → 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/dist/index.js CHANGED
@@ -54,6 +54,8 @@ __export(client_exports, {
54
54
  useMsalAuth: () => useMsalAuth,
55
55
  useMultiAccount: () => useMultiAccount,
56
56
  useRoles: () => useRoles,
57
+ useTenant: () => useTenant,
58
+ useTenantConfig: () => useTenantConfig,
57
59
  useTokenRefresh: () => useTokenRefresh,
58
60
  useUserProfile: () => useUserProfile,
59
61
  validateConfig: () => validateConfig,
@@ -131,19 +133,44 @@ function createMsalConfig(config) {
131
133
  navigateToLoginRequestUrl = false,
132
134
  enableLogging = false,
133
135
  loggerCallback,
134
- allowedRedirectUris
136
+ allowedRedirectUris,
137
+ multiTenant
135
138
  } = config;
136
139
  if (!clientId) {
137
140
  throw new Error("@chemmangat/msal-next: clientId is required");
138
141
  }
139
- const getAuthority = () => {
142
+ const resolveAuthorityType = () => {
143
+ if (multiTenant?.type) {
144
+ switch (multiTenant.type) {
145
+ case "single":
146
+ if (!tenantId) {
147
+ throw new Error(
148
+ '@chemmangat/msal-next: tenantId is required when multiTenant.type is "single"'
149
+ );
150
+ }
151
+ return tenantId;
152
+ case "multi":
153
+ case "common":
154
+ return "common";
155
+ case "organizations":
156
+ return "organizations";
157
+ case "consumers":
158
+ return "consumers";
159
+ }
160
+ }
140
161
  if (authorityType === "tenant") {
141
162
  if (!tenantId) {
142
- throw new Error('@chemmangat/msal-next: tenantId is required when authorityType is "tenant"');
163
+ throw new Error(
164
+ '@chemmangat/msal-next: tenantId is required when authorityType is "tenant"'
165
+ );
143
166
  }
144
- return `https://login.microsoftonline.com/${tenantId}`;
167
+ return tenantId;
145
168
  }
146
- return `https://login.microsoftonline.com/${authorityType}`;
169
+ return authorityType;
170
+ };
171
+ const getAuthority = () => {
172
+ const resolved = resolveAuthorityType();
173
+ return `https://login.microsoftonline.com/${resolved}`;
147
174
  };
148
175
  const defaultRedirectUri = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
149
176
  const finalRedirectUri = redirectUri || defaultRedirectUri;
@@ -764,6 +791,31 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
764
791
  instance.setActiveAccount(null);
765
792
  await instance.clearCache();
766
793
  }, [instance]);
794
+ const acquireTokenForTenant = (0, import_react.useCallback)(
795
+ async (tenantId, scopes) => {
796
+ if (!account) {
797
+ throw new Error("[MSAL] No active account. Please login first.");
798
+ }
799
+ try {
800
+ const response = await instance.acquireTokenSilent({
801
+ scopes,
802
+ account,
803
+ authority: `https://login.microsoftonline.com/${tenantId}`,
804
+ forceRefresh: false
805
+ });
806
+ return response.accessToken;
807
+ } catch (error) {
808
+ const msalError = wrapMsalError(error);
809
+ if (process.env.NODE_ENV === "development") {
810
+ console.error(msalError.toConsoleString());
811
+ } else {
812
+ console.error("[MSAL] Cross-tenant token acquisition failed:", msalError.message);
813
+ }
814
+ throw msalError;
815
+ }
816
+ },
817
+ [instance, account]
818
+ );
767
819
  return {
768
820
  account,
769
821
  accounts,
@@ -774,7 +826,8 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
774
826
  acquireToken,
775
827
  acquireTokenSilent,
776
828
  acquireTokenRedirect,
777
- clearSession
829
+ clearSession,
830
+ acquireTokenForTenant
778
831
  };
779
832
  }
780
833
 
@@ -863,6 +916,86 @@ function TokenRefreshManager({
863
916
  return null;
864
917
  }
865
918
 
919
+ // src/utils/tenantValidator.ts
920
+ function getTenantDomain(account) {
921
+ const upn = account.username || account.idTokenClaims?.preferred_username || account.idTokenClaims?.upn || "";
922
+ return upn.includes("@") ? upn.split("@")[1].toLowerCase() : null;
923
+ }
924
+ function getTenantId(account) {
925
+ return account.tenantId || account.idTokenClaims?.tid || null;
926
+ }
927
+ function matchesTenant(value, tenantId, tenantDomain) {
928
+ const v = value.toLowerCase();
929
+ if (tenantId && v === tenantId.toLowerCase()) return true;
930
+ if (tenantDomain && v === tenantDomain.toLowerCase()) return true;
931
+ return false;
932
+ }
933
+ function isGuestAccount(account) {
934
+ const claims = account.idTokenClaims ?? {};
935
+ const resourceTenantId = account.tenantId || claims["tid"] || null;
936
+ const issuer = claims["iss"] || null;
937
+ if (!issuer || !resourceTenantId) return false;
938
+ const match = issuer.match(
939
+ /https:\/\/login\.microsoftonline\.com\/([^/]+)(?:\/|$)/i
940
+ );
941
+ if (!match) return false;
942
+ const homeTenantId = match[1];
943
+ return homeTenantId.toLowerCase() !== resourceTenantId.toLowerCase();
944
+ }
945
+ function validateTenantAccess(account, config) {
946
+ const tenantId = getTenantId(account);
947
+ const tenantDomain = getTenantDomain(account);
948
+ const claims = account.idTokenClaims ?? {};
949
+ if (config.blockList && config.blockList.length > 0) {
950
+ const blocked = config.blockList.some(
951
+ (entry) => matchesTenant(entry, tenantId, tenantDomain)
952
+ );
953
+ if (blocked) {
954
+ return {
955
+ allowed: false,
956
+ reason: `Tenant "${tenantDomain || tenantId}" is blocked from accessing this application.`
957
+ };
958
+ }
959
+ }
960
+ if (config.allowList && config.allowList.length > 0) {
961
+ const allowed = config.allowList.some(
962
+ (entry) => matchesTenant(entry, tenantId, tenantDomain)
963
+ );
964
+ if (!allowed) {
965
+ return {
966
+ allowed: false,
967
+ reason: `Tenant "${tenantDomain || tenantId}" is not in the allowed list for this application.`
968
+ };
969
+ }
970
+ }
971
+ if (config.requireType) {
972
+ const isGuest = isGuestAccount(account);
973
+ if (config.requireType === "Member" && isGuest) {
974
+ return {
975
+ allowed: false,
976
+ reason: "Only member accounts are allowed. Guest (B2B) accounts are not permitted."
977
+ };
978
+ }
979
+ if (config.requireType === "Guest" && !isGuest) {
980
+ return {
981
+ allowed: false,
982
+ reason: "Only guest (B2B) accounts are allowed."
983
+ };
984
+ }
985
+ }
986
+ if (config.requireMFA) {
987
+ const amr = claims["amr"] || [];
988
+ const hasMfa = amr.includes("mfa") || amr.includes("ngcmfa") || amr.includes("hwk") || amr.includes("swk");
989
+ if (!hasMfa) {
990
+ return {
991
+ allowed: false,
992
+ reason: "Multi-factor authentication (MFA) is required to access this application."
993
+ };
994
+ }
995
+ }
996
+ return { allowed: true };
997
+ }
998
+
866
999
  // src/components/MsalAuthProvider.tsx
867
1000
  var import_jsx_runtime = require("react/jsx-runtime");
868
1001
  var globalMsalInstance = null;
@@ -875,6 +1008,7 @@ function MsalAuthProvider({
875
1008
  onInitialized,
876
1009
  autoRefreshToken = false,
877
1010
  refreshBeforeExpiry = 300,
1011
+ onTenantDenied,
878
1012
  ...config
879
1013
  }) {
880
1014
  const [msalInstance, setMsalInstance] = (0, import_react3.useState)(null);
@@ -904,6 +1038,22 @@ function MsalAuthProvider({
904
1038
  }
905
1039
  if (response.account) {
906
1040
  instance.setActiveAccount(response.account);
1041
+ if (config.multiTenant) {
1042
+ const validation = validateTenantAccess(response.account, config.multiTenant);
1043
+ if (!validation.allowed) {
1044
+ try {
1045
+ await instance.logoutRedirect({ account: response.account });
1046
+ } catch {
1047
+ }
1048
+ const reason = validation.reason || "Tenant access denied.";
1049
+ if (onTenantDenied) {
1050
+ onTenantDenied(reason);
1051
+ } else {
1052
+ console.error("[MSAL] Tenant access denied:", reason);
1053
+ }
1054
+ return;
1055
+ }
1056
+ }
907
1057
  }
908
1058
  if (window.location.hash) {
909
1059
  window.history.replaceState(null, "", window.location.pathname + window.location.search);
@@ -1003,8 +1153,12 @@ function MsalAuthProvider({
1003
1153
  var import_react4 = require("react");
1004
1154
  var import_jsx_runtime2 = require("react/jsx-runtime");
1005
1155
  var ProtectionConfigContext = (0, import_react4.createContext)(void 0);
1006
- function MSALProvider({ children, protection, ...props }) {
1007
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ProtectionConfigContext.Provider, { value: protection, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MsalAuthProvider, { ...props, children }) });
1156
+ var TenantConfigContext = (0, import_react4.createContext)(void 0);
1157
+ function useTenantConfig() {
1158
+ return (0, import_react4.useContext)(TenantConfigContext);
1159
+ }
1160
+ function MSALProvider({ children, protection, onTenantDenied, ...props }) {
1161
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ProtectionConfigContext.Provider, { value: protection, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TenantConfigContext.Provider, { value: props.multiTenant, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MsalAuthProvider, { ...props, onTenantDenied, children }) }) });
1008
1162
  }
1009
1163
 
1010
1164
  // src/components/MicrosoftSignInButton.tsx
@@ -2419,8 +2573,48 @@ function AccountList({
2419
2573
  }) });
2420
2574
  }
2421
2575
 
2422
- // src/hooks/useRoles.ts
2576
+ // src/hooks/useTenant.ts
2423
2577
  var import_react13 = require("react");
2578
+ function useTenant() {
2579
+ const { account, isAuthenticated } = useMsalAuth();
2580
+ const tenantInfo = (0, import_react13.useMemo)(() => {
2581
+ if (!account) {
2582
+ return {
2583
+ tenantId: null,
2584
+ tenantDomain: null,
2585
+ isGuestUser: false,
2586
+ homeTenantId: null,
2587
+ resourceTenantId: null,
2588
+ claims: null
2589
+ };
2590
+ }
2591
+ const claims = account.idTokenClaims ?? {};
2592
+ const resourceTenantId = account.tenantId || claims["tid"] || null;
2593
+ const issuer = claims["iss"] || null;
2594
+ let homeTenantId = null;
2595
+ if (issuer) {
2596
+ const match = issuer.match(
2597
+ /https:\/\/login\.microsoftonline\.com\/([^/]+)(?:\/|$)/i
2598
+ );
2599
+ if (match) homeTenantId = match[1];
2600
+ }
2601
+ const isGuestUser = !!homeTenantId && !!resourceTenantId && homeTenantId.toLowerCase() !== resourceTenantId.toLowerCase();
2602
+ const upn = account.username || claims["preferred_username"] || claims["upn"] || "";
2603
+ const tenantDomain = upn.includes("@") ? upn.split("@")[1] : null;
2604
+ return {
2605
+ tenantId: resourceTenantId,
2606
+ tenantDomain,
2607
+ isGuestUser,
2608
+ homeTenantId,
2609
+ resourceTenantId,
2610
+ claims
2611
+ };
2612
+ }, [account]);
2613
+ return { ...tenantInfo, isAuthenticated };
2614
+ }
2615
+
2616
+ // src/hooks/useRoles.ts
2617
+ var import_react14 = require("react");
2424
2618
  var rolesCache = /* @__PURE__ */ new Map();
2425
2619
  var CACHE_DURATION2 = 5 * 60 * 1e3;
2426
2620
  var MAX_CACHE_SIZE2 = 100;
@@ -2442,11 +2636,11 @@ function enforceCacheLimit2() {
2442
2636
  function useRoles() {
2443
2637
  const { isAuthenticated, account } = useMsalAuth();
2444
2638
  const graph = useGraphApi();
2445
- const [roles, setRoles] = (0, import_react13.useState)([]);
2446
- const [groups, setGroups] = (0, import_react13.useState)([]);
2447
- const [loading, setLoading] = (0, import_react13.useState)(false);
2448
- const [error, setError] = (0, import_react13.useState)(null);
2449
- const fetchRolesAndGroups = (0, import_react13.useCallback)(async () => {
2639
+ const [roles, setRoles] = (0, import_react14.useState)([]);
2640
+ const [groups, setGroups] = (0, import_react14.useState)([]);
2641
+ const [loading, setLoading] = (0, import_react14.useState)(false);
2642
+ const [error, setError] = (0, import_react14.useState)(null);
2643
+ const fetchRolesAndGroups = (0, import_react14.useCallback)(async () => {
2450
2644
  if (!isAuthenticated || !account) {
2451
2645
  setRoles([]);
2452
2646
  setGroups([]);
@@ -2489,31 +2683,31 @@ function useRoles() {
2489
2683
  setLoading(false);
2490
2684
  }
2491
2685
  }, [isAuthenticated, account, graph]);
2492
- const hasRole = (0, import_react13.useCallback)(
2686
+ const hasRole = (0, import_react14.useCallback)(
2493
2687
  (role) => {
2494
2688
  return roles.includes(role);
2495
2689
  },
2496
2690
  [roles]
2497
2691
  );
2498
- const hasGroup = (0, import_react13.useCallback)(
2692
+ const hasGroup = (0, import_react14.useCallback)(
2499
2693
  (groupId) => {
2500
2694
  return groups.includes(groupId);
2501
2695
  },
2502
2696
  [groups]
2503
2697
  );
2504
- const hasAnyRole = (0, import_react13.useCallback)(
2698
+ const hasAnyRole = (0, import_react14.useCallback)(
2505
2699
  (checkRoles) => {
2506
2700
  return checkRoles.some((role) => roles.includes(role));
2507
2701
  },
2508
2702
  [roles]
2509
2703
  );
2510
- const hasAllRoles = (0, import_react13.useCallback)(
2704
+ const hasAllRoles = (0, import_react14.useCallback)(
2511
2705
  (checkRoles) => {
2512
2706
  return checkRoles.every((role) => roles.includes(role));
2513
2707
  },
2514
2708
  [roles]
2515
2709
  );
2516
- (0, import_react13.useEffect)(() => {
2710
+ (0, import_react14.useEffect)(() => {
2517
2711
  fetchRolesAndGroups();
2518
2712
  return () => {
2519
2713
  if (account) {
@@ -2811,7 +3005,7 @@ function createScopedLogger(scope, config) {
2811
3005
  }
2812
3006
 
2813
3007
  // src/protection/ProtectedPage.tsx
2814
- var import_react14 = require("react");
3008
+ var import_react15 = require("react");
2815
3009
  var import_navigation = require("next/navigation");
2816
3010
  var import_jsx_runtime12 = require("react/jsx-runtime");
2817
3011
  function ProtectedPage({
@@ -2824,9 +3018,10 @@ function ProtectedPage({
2824
3018
  }) {
2825
3019
  const router = (0, import_navigation.useRouter)();
2826
3020
  const { isAuthenticated, account, inProgress } = useMsalAuth();
2827
- const [isValidating, setIsValidating] = (0, import_react14.useState)(true);
2828
- const [isAuthorized, setIsAuthorized] = (0, import_react14.useState)(false);
2829
- (0, import_react14.useEffect)(() => {
3021
+ const tenantInfo = useTenant();
3022
+ const [isValidating, setIsValidating] = (0, import_react15.useState)(true);
3023
+ const [isAuthorized, setIsAuthorized] = (0, import_react15.useState)(false);
3024
+ (0, import_react15.useEffect)(() => {
2830
3025
  async function checkAuth() {
2831
3026
  if (debug) {
2832
3027
  console.log("[ProtectedPage] Checking auth...", {
@@ -2867,6 +3062,17 @@ function ProtectedPage({
2867
3062
  return;
2868
3063
  }
2869
3064
  }
3065
+ if (config.tenant && account) {
3066
+ const tenantResult = validateTenantAccess(account, config.tenant);
3067
+ if (!tenantResult.allowed) {
3068
+ if (debug) {
3069
+ console.log("[ProtectedPage] Tenant validation failed:", tenantResult.reason);
3070
+ }
3071
+ setIsAuthorized(false);
3072
+ setIsValidating(false);
3073
+ return;
3074
+ }
3075
+ }
2870
3076
  if (config.validate) {
2871
3077
  try {
2872
3078
  const isValid = await config.validate(account);
@@ -2892,7 +3098,7 @@ function ProtectedPage({
2892
3098
  setIsValidating(false);
2893
3099
  }
2894
3100
  checkAuth();
2895
- }, [isAuthenticated, account, inProgress, config, router, defaultRedirectTo, debug]);
3101
+ }, [isAuthenticated, account, inProgress, config, router, defaultRedirectTo, debug, tenantInfo]);
2896
3102
  if (isValidating || inProgress) {
2897
3103
  if (config.loading) {
2898
3104
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_jsx_runtime12.Fragment, { children: config.loading });
@@ -2947,6 +3153,8 @@ function createAuthMiddleware(config = {}) {
2947
3153
  redirectAfterLogin = "/",
2948
3154
  sessionCookie = "msal.account",
2949
3155
  isAuthenticated: customAuthCheck,
3156
+ tenantConfig,
3157
+ tenantDeniedPath = "/unauthorized",
2950
3158
  debug = false
2951
3159
  } = config;
2952
3160
  return async function authMiddleware(request) {
@@ -2979,6 +3187,30 @@ function createAuthMiddleware(config = {}) {
2979
3187
  url.searchParams.set("returnUrl", pathname);
2980
3188
  return import_server.NextResponse.redirect(url);
2981
3189
  }
3190
+ if (isProtectedRoute && authenticated && tenantConfig) {
3191
+ try {
3192
+ const sessionData = request.cookies.get(sessionCookie);
3193
+ if (sessionData?.value) {
3194
+ const account = safeJsonParse(sessionData.value, isValidAccountData);
3195
+ if (account) {
3196
+ const tenantResult = validateTenantAccess(account, tenantConfig);
3197
+ if (!tenantResult.allowed) {
3198
+ if (debug) {
3199
+ console.log("[AuthMiddleware] Tenant access denied:", tenantResult.reason);
3200
+ }
3201
+ const url = request.nextUrl.clone();
3202
+ url.pathname = tenantDeniedPath;
3203
+ url.searchParams.set("reason", tenantResult.reason || "access_denied");
3204
+ return import_server.NextResponse.redirect(url);
3205
+ }
3206
+ }
3207
+ }
3208
+ } catch (error) {
3209
+ if (debug) {
3210
+ console.warn("[AuthMiddleware] Tenant validation error:", error);
3211
+ }
3212
+ }
3213
+ }
2982
3214
  if (isPublicOnlyRoute && authenticated) {
2983
3215
  if (debug) {
2984
3216
  console.log("[AuthMiddleware] Redirecting to home");
@@ -3047,6 +3279,8 @@ var import_msal_react4 = require("@azure/msal-react");
3047
3279
  useMsalAuth,
3048
3280
  useMultiAccount,
3049
3281
  useRoles,
3282
+ useTenant,
3283
+ useTenantConfig,
3050
3284
  useTokenRefresh,
3051
3285
  useUserProfile,
3052
3286
  validateConfig,