@chemmangat/msal-next 5.2.0 → 5.3.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 +61 -1
- package/dist/index.d.mts +23 -43
- package/dist/index.d.ts +23 -43
- package/dist/index.js +43 -114
- package/dist/index.mjs +39 -109
- package/dist/middleware.d.mts +115 -0
- package/dist/middleware.d.ts +115 -0
- package/dist/middleware.js +203 -0
- package/dist/middleware.mjs +201 -0
- package/dist/server.d.mts +24 -5
- package/dist/server.d.ts +24 -5
- package/dist/server.js +16 -14
- package/dist/server.mjs +16 -15
- package/package.json +6 -1
package/dist/index.mjs
CHANGED
|
@@ -614,6 +614,7 @@ Note: Environment variables starting with NEXT_PUBLIC_ are exposed to the browse
|
|
|
614
614
|
|
|
615
615
|
// src/hooks/useTokenRefresh.ts
|
|
616
616
|
import { useEffect, useRef, useCallback as useCallback2 } from "react";
|
|
617
|
+
import { useMsal as useMsal2 } from "@azure/msal-react";
|
|
617
618
|
|
|
618
619
|
// src/hooks/useMsalAuth.ts
|
|
619
620
|
import { useMsal, useAccount } from "@azure/msal-react";
|
|
@@ -792,7 +793,8 @@ function useTokenRefresh(options = {}) {
|
|
|
792
793
|
onRefresh,
|
|
793
794
|
onError
|
|
794
795
|
} = options;
|
|
795
|
-
const { isAuthenticated, account
|
|
796
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
797
|
+
const { instance } = useMsal2();
|
|
796
798
|
const intervalRef = useRef(null);
|
|
797
799
|
const lastRefreshRef = useRef(null);
|
|
798
800
|
const expiresInRef = useRef(null);
|
|
@@ -801,23 +803,27 @@ function useTokenRefresh(options = {}) {
|
|
|
801
803
|
return;
|
|
802
804
|
}
|
|
803
805
|
try {
|
|
804
|
-
await acquireTokenSilent(
|
|
806
|
+
const response = await instance.acquireTokenSilent({
|
|
807
|
+
scopes,
|
|
808
|
+
account,
|
|
809
|
+
forceRefresh: false
|
|
810
|
+
});
|
|
805
811
|
lastRefreshRef.current = /* @__PURE__ */ new Date();
|
|
806
|
-
const expiresIn = 3600;
|
|
812
|
+
const expiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
|
|
807
813
|
expiresInRef.current = expiresIn;
|
|
808
814
|
onRefresh?.(expiresIn);
|
|
809
815
|
} catch (error) {
|
|
810
816
|
console.error("[TokenRefresh] Failed to refresh token:", error);
|
|
811
817
|
onError?.(error);
|
|
812
818
|
}
|
|
813
|
-
}, [isAuthenticated, account,
|
|
819
|
+
}, [isAuthenticated, account, instance, scopes, onRefresh, onError]);
|
|
814
820
|
useEffect(() => {
|
|
815
821
|
if (!enabled || !isAuthenticated) {
|
|
816
822
|
return;
|
|
817
823
|
}
|
|
818
824
|
refresh();
|
|
819
825
|
intervalRef.current = setInterval(() => {
|
|
820
|
-
if (
|
|
826
|
+
if (expiresInRef.current === null) {
|
|
821
827
|
return;
|
|
822
828
|
}
|
|
823
829
|
const timeSinceRefresh = lastRefreshRef.current ? (Date.now() - lastRefreshRef.current.getTime()) / 1e3 : 0;
|
|
@@ -950,6 +956,23 @@ function validateTenantAccess(account, config) {
|
|
|
950
956
|
// src/components/MsalAuthProvider.tsx
|
|
951
957
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
952
958
|
var globalMsalInstance = null;
|
|
959
|
+
function writeMsalSessionCookie(account) {
|
|
960
|
+
try {
|
|
961
|
+
const data = encodeURIComponent(JSON.stringify({
|
|
962
|
+
homeAccountId: account.homeAccountId,
|
|
963
|
+
username: account.username,
|
|
964
|
+
name: account.name ?? ""
|
|
965
|
+
}));
|
|
966
|
+
document.cookie = `msal.account=${data}; path=/; SameSite=Lax`;
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function clearMsalSessionCookie() {
|
|
971
|
+
try {
|
|
972
|
+
document.cookie = "msal.account=; path=/; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
}
|
|
953
976
|
function getMsalInstance() {
|
|
954
977
|
return globalMsalInstance;
|
|
955
978
|
}
|
|
@@ -999,6 +1022,7 @@ function MsalAuthProvider({
|
|
|
999
1022
|
}
|
|
1000
1023
|
if (response.account) {
|
|
1001
1024
|
instance.setActiveAccount(response.account);
|
|
1025
|
+
writeMsalSessionCookie(response.account);
|
|
1002
1026
|
if (config.multiTenant) {
|
|
1003
1027
|
const validation = validateTenantAccess(response.account, config.multiTenant);
|
|
1004
1028
|
if (!validation.allowed) {
|
|
@@ -1044,6 +1068,7 @@ function MsalAuthProvider({
|
|
|
1044
1068
|
const accounts = instance.getAllAccounts();
|
|
1045
1069
|
if (accounts.length > 0 && !instance.getActiveAccount()) {
|
|
1046
1070
|
instance.setActiveAccount(accounts[0]);
|
|
1071
|
+
writeMsalSessionCookie(accounts[0]);
|
|
1047
1072
|
}
|
|
1048
1073
|
const loggingEnabled = config.enableLogging || false;
|
|
1049
1074
|
instance.addEventCallback((event) => {
|
|
@@ -1052,6 +1077,7 @@ function MsalAuthProvider({
|
|
|
1052
1077
|
const account = "account" in payload ? payload.account : payload;
|
|
1053
1078
|
if (account) {
|
|
1054
1079
|
instance.setActiveAccount(account);
|
|
1080
|
+
writeMsalSessionCookie(account);
|
|
1055
1081
|
}
|
|
1056
1082
|
if (loggingEnabled) {
|
|
1057
1083
|
console.log("[MSAL] Login successful:", account?.username);
|
|
@@ -1063,10 +1089,14 @@ function MsalAuthProvider({
|
|
|
1063
1089
|
}
|
|
1064
1090
|
if (event.eventType === EventType.LOGOUT_SUCCESS) {
|
|
1065
1091
|
instance.setActiveAccount(null);
|
|
1092
|
+
clearMsalSessionCookie();
|
|
1066
1093
|
if (loggingEnabled) {
|
|
1067
1094
|
console.log("[MSAL] Logout successful");
|
|
1068
1095
|
}
|
|
1069
1096
|
}
|
|
1097
|
+
if (EventType.LOGOUT_END !== void 0 && event.eventType === EventType.LOGOUT_END) {
|
|
1098
|
+
clearMsalSessionCookie();
|
|
1099
|
+
}
|
|
1070
1100
|
if (event.eventType === EventType.ACQUIRE_TOKEN_SUCCESS) {
|
|
1071
1101
|
const payload = event.payload;
|
|
1072
1102
|
if (payload?.account && !instance.getActiveAccount()) {
|
|
@@ -1868,11 +1898,11 @@ var ErrorBoundary = class extends Component {
|
|
|
1868
1898
|
};
|
|
1869
1899
|
|
|
1870
1900
|
// src/hooks/useMultiAccount.ts
|
|
1871
|
-
import { useMsal as
|
|
1901
|
+
import { useMsal as useMsal3 } from "@azure/msal-react";
|
|
1872
1902
|
import { InteractionStatus as InteractionStatus2 } from "@azure/msal-browser";
|
|
1873
1903
|
import { useCallback as useCallback5, useMemo as useMemo2, useState as useState5, useEffect as useEffect7 } from "react";
|
|
1874
1904
|
function useMultiAccount(defaultScopes = ["User.Read"]) {
|
|
1875
|
-
const { instance, accounts, inProgress } =
|
|
1905
|
+
const { instance, accounts, inProgress } = useMsal3();
|
|
1876
1906
|
const [activeAccount, setActiveAccount] = useState5(
|
|
1877
1907
|
instance.getActiveAccount()
|
|
1878
1908
|
);
|
|
@@ -3106,107 +3136,8 @@ function withPageAuth(Component2, authConfig, globalConfig) {
|
|
|
3106
3136
|
return WrappedComponent;
|
|
3107
3137
|
}
|
|
3108
3138
|
|
|
3109
|
-
// src/middleware/createAuthMiddleware.ts
|
|
3110
|
-
import { NextResponse } from "next/server";
|
|
3111
|
-
function createAuthMiddleware(config = {}) {
|
|
3112
|
-
const {
|
|
3113
|
-
protectedRoutes = [],
|
|
3114
|
-
publicOnlyRoutes = [],
|
|
3115
|
-
loginPath = "/login",
|
|
3116
|
-
redirectAfterLogin = "/",
|
|
3117
|
-
sessionCookie = "msal.account",
|
|
3118
|
-
isAuthenticated: customAuthCheck,
|
|
3119
|
-
tenantConfig,
|
|
3120
|
-
tenantDeniedPath = "/unauthorized",
|
|
3121
|
-
debug = false
|
|
3122
|
-
} = config;
|
|
3123
|
-
return async function authMiddleware(request) {
|
|
3124
|
-
const { pathname } = request.nextUrl;
|
|
3125
|
-
if (debug) {
|
|
3126
|
-
console.log("[AuthMiddleware] Processing:", pathname);
|
|
3127
|
-
}
|
|
3128
|
-
let authenticated = false;
|
|
3129
|
-
if (customAuthCheck) {
|
|
3130
|
-
authenticated = await customAuthCheck(request);
|
|
3131
|
-
} else {
|
|
3132
|
-
const sessionData = request.cookies.get(sessionCookie);
|
|
3133
|
-
authenticated = !!sessionData?.value;
|
|
3134
|
-
}
|
|
3135
|
-
if (debug) {
|
|
3136
|
-
console.log("[AuthMiddleware] Authenticated:", authenticated);
|
|
3137
|
-
}
|
|
3138
|
-
const isProtectedRoute = protectedRoutes.some(
|
|
3139
|
-
(route) => pathname.startsWith(route)
|
|
3140
|
-
);
|
|
3141
|
-
const isPublicOnlyRoute = publicOnlyRoutes.some(
|
|
3142
|
-
(route) => pathname.startsWith(route)
|
|
3143
|
-
);
|
|
3144
|
-
if (isProtectedRoute && !authenticated) {
|
|
3145
|
-
if (debug) {
|
|
3146
|
-
console.log("[AuthMiddleware] Redirecting to login");
|
|
3147
|
-
}
|
|
3148
|
-
const url = request.nextUrl.clone();
|
|
3149
|
-
url.pathname = loginPath;
|
|
3150
|
-
url.searchParams.set("returnUrl", pathname);
|
|
3151
|
-
return NextResponse.redirect(url);
|
|
3152
|
-
}
|
|
3153
|
-
if (isProtectedRoute && authenticated && tenantConfig) {
|
|
3154
|
-
try {
|
|
3155
|
-
const sessionData = request.cookies.get(sessionCookie);
|
|
3156
|
-
if (sessionData?.value) {
|
|
3157
|
-
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
3158
|
-
if (account) {
|
|
3159
|
-
const tenantResult = validateTenantAccess(account, tenantConfig);
|
|
3160
|
-
if (!tenantResult.allowed) {
|
|
3161
|
-
if (debug) {
|
|
3162
|
-
console.log("[AuthMiddleware] Tenant access denied:", tenantResult.reason);
|
|
3163
|
-
}
|
|
3164
|
-
const url = request.nextUrl.clone();
|
|
3165
|
-
url.pathname = tenantDeniedPath;
|
|
3166
|
-
url.searchParams.set("reason", tenantResult.reason || "access_denied");
|
|
3167
|
-
return NextResponse.redirect(url);
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
} catch (error) {
|
|
3172
|
-
if (debug) {
|
|
3173
|
-
console.warn("[AuthMiddleware] Tenant validation error:", error);
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
if (isPublicOnlyRoute && authenticated) {
|
|
3178
|
-
if (debug) {
|
|
3179
|
-
console.log("[AuthMiddleware] Redirecting to home");
|
|
3180
|
-
}
|
|
3181
|
-
const returnUrl = request.nextUrl.searchParams.get("returnUrl");
|
|
3182
|
-
const url = request.nextUrl.clone();
|
|
3183
|
-
url.pathname = returnUrl || redirectAfterLogin;
|
|
3184
|
-
url.searchParams.delete("returnUrl");
|
|
3185
|
-
return NextResponse.redirect(url);
|
|
3186
|
-
}
|
|
3187
|
-
const response = NextResponse.next();
|
|
3188
|
-
if (authenticated) {
|
|
3189
|
-
response.headers.set("x-msal-authenticated", "true");
|
|
3190
|
-
try {
|
|
3191
|
-
const sessionData = request.cookies.get(sessionCookie);
|
|
3192
|
-
if (sessionData?.value) {
|
|
3193
|
-
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
3194
|
-
if (account?.username) {
|
|
3195
|
-
response.headers.set("x-msal-username", account.username);
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
} catch (error) {
|
|
3199
|
-
if (debug) {
|
|
3200
|
-
console.warn("[AuthMiddleware] Failed to parse session data");
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
return response;
|
|
3205
|
-
};
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
3139
|
// src/client.ts
|
|
3209
|
-
import { useMsal as
|
|
3140
|
+
import { useMsal as useMsal4, useIsAuthenticated, useAccount as useAccount2 } from "@azure/msal-react";
|
|
3210
3141
|
export {
|
|
3211
3142
|
AccountList,
|
|
3212
3143
|
AccountSwitcher,
|
|
@@ -3220,7 +3151,6 @@ export {
|
|
|
3220
3151
|
ProtectedPage,
|
|
3221
3152
|
SignOutButton,
|
|
3222
3153
|
UserAvatar,
|
|
3223
|
-
createAuthMiddleware,
|
|
3224
3154
|
createMissingEnvVarError,
|
|
3225
3155
|
createMsalConfig,
|
|
3226
3156
|
createRetryWrapper,
|
|
@@ -3237,7 +3167,7 @@ export {
|
|
|
3237
3167
|
useAccount2 as useAccount,
|
|
3238
3168
|
useGraphApi,
|
|
3239
3169
|
useIsAuthenticated,
|
|
3240
|
-
|
|
3170
|
+
useMsal4 as useMsal,
|
|
3241
3171
|
useMsalAuth,
|
|
3242
3172
|
useMultiAccount,
|
|
3243
3173
|
useRoles,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definitions for @chemmangat/msal-next
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Multi-tenant configuration for v5.1.0
|
|
11
|
+
*/
|
|
12
|
+
interface MultiTenantConfig {
|
|
13
|
+
/**
|
|
14
|
+
* Tenant mode
|
|
15
|
+
* - 'single' — only your own tenant (maps to authorityType 'tenant')
|
|
16
|
+
* - 'multi' — any Azure AD tenant (maps to authorityType 'common')
|
|
17
|
+
* - 'organizations' — any organisational tenant, no personal accounts
|
|
18
|
+
* - 'consumers' — Microsoft personal accounts only
|
|
19
|
+
* - 'common' — alias for 'multi'
|
|
20
|
+
*
|
|
21
|
+
* @defaultValue 'common'
|
|
22
|
+
*/
|
|
23
|
+
type?: 'single' | 'multi' | 'organizations' | 'consumers' | 'common';
|
|
24
|
+
/**
|
|
25
|
+
* Tenant allow-list — only users from these tenant IDs or domains are permitted.
|
|
26
|
+
* Checked after authentication; users from other tenants are shown an error.
|
|
27
|
+
*
|
|
28
|
+
* @example ['contoso.com', '72f988bf-86f1-41af-91ab-2d7cd011db47']
|
|
29
|
+
*/
|
|
30
|
+
allowList?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Tenant block-list — users from these tenant IDs or domains are denied.
|
|
33
|
+
* Takes precedence over allowList.
|
|
34
|
+
*/
|
|
35
|
+
blockList?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Require a specific tenant type ('Member' | 'Guest').
|
|
38
|
+
* Useful to block B2B guests or to allow only guests.
|
|
39
|
+
*/
|
|
40
|
+
requireType?: 'Member' | 'Guest';
|
|
41
|
+
/**
|
|
42
|
+
* Require MFA claim in the token (amr claim must contain 'mfa').
|
|
43
|
+
* @defaultValue false
|
|
44
|
+
*/
|
|
45
|
+
requireMFA?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AuthMiddlewareConfig {
|
|
49
|
+
/**
|
|
50
|
+
* Routes that require authentication
|
|
51
|
+
* @example ['/dashboard', '/profile', '/api/protected']
|
|
52
|
+
*/
|
|
53
|
+
protectedRoutes?: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Routes that should be accessible only when NOT authenticated
|
|
56
|
+
* @example ['/login', '/signup']
|
|
57
|
+
*/
|
|
58
|
+
publicOnlyRoutes?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Login page path
|
|
61
|
+
* @default '/login'
|
|
62
|
+
*/
|
|
63
|
+
loginPath?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Redirect path after login
|
|
66
|
+
* @default '/'
|
|
67
|
+
*/
|
|
68
|
+
redirectAfterLogin?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Cookie name for session
|
|
71
|
+
* @default 'msal.account'
|
|
72
|
+
*/
|
|
73
|
+
sessionCookie?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Custom authentication check function
|
|
76
|
+
*/
|
|
77
|
+
isAuthenticated?: (request: NextRequest) => boolean | Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Tenant access configuration (v5.1.0).
|
|
80
|
+
* Validated against the account stored in the session cookie.
|
|
81
|
+
*/
|
|
82
|
+
tenantConfig?: MultiTenantConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Path to redirect to when tenant access is denied (v5.1.0).
|
|
85
|
+
* @default '/unauthorized'
|
|
86
|
+
*/
|
|
87
|
+
tenantDeniedPath?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Enable debug logging
|
|
90
|
+
* @default false
|
|
91
|
+
*/
|
|
92
|
+
debug?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Creates authentication middleware for Next.js App Router
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* // middleware.ts
|
|
100
|
+
* import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
101
|
+
*
|
|
102
|
+
* export const middleware = createAuthMiddleware({
|
|
103
|
+
* protectedRoutes: ['/dashboard', '/profile'],
|
|
104
|
+
* publicOnlyRoutes: ['/login'],
|
|
105
|
+
* loginPath: '/login',
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* export const config = {
|
|
109
|
+
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
110
|
+
* };
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare function createAuthMiddleware(config?: AuthMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
114
|
+
|
|
115
|
+
export { type AuthMiddlewareConfig, createAuthMiddleware };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definitions for @chemmangat/msal-next
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Multi-tenant configuration for v5.1.0
|
|
11
|
+
*/
|
|
12
|
+
interface MultiTenantConfig {
|
|
13
|
+
/**
|
|
14
|
+
* Tenant mode
|
|
15
|
+
* - 'single' — only your own tenant (maps to authorityType 'tenant')
|
|
16
|
+
* - 'multi' — any Azure AD tenant (maps to authorityType 'common')
|
|
17
|
+
* - 'organizations' — any organisational tenant, no personal accounts
|
|
18
|
+
* - 'consumers' — Microsoft personal accounts only
|
|
19
|
+
* - 'common' — alias for 'multi'
|
|
20
|
+
*
|
|
21
|
+
* @defaultValue 'common'
|
|
22
|
+
*/
|
|
23
|
+
type?: 'single' | 'multi' | 'organizations' | 'consumers' | 'common';
|
|
24
|
+
/**
|
|
25
|
+
* Tenant allow-list — only users from these tenant IDs or domains are permitted.
|
|
26
|
+
* Checked after authentication; users from other tenants are shown an error.
|
|
27
|
+
*
|
|
28
|
+
* @example ['contoso.com', '72f988bf-86f1-41af-91ab-2d7cd011db47']
|
|
29
|
+
*/
|
|
30
|
+
allowList?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Tenant block-list — users from these tenant IDs or domains are denied.
|
|
33
|
+
* Takes precedence over allowList.
|
|
34
|
+
*/
|
|
35
|
+
blockList?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Require a specific tenant type ('Member' | 'Guest').
|
|
38
|
+
* Useful to block B2B guests or to allow only guests.
|
|
39
|
+
*/
|
|
40
|
+
requireType?: 'Member' | 'Guest';
|
|
41
|
+
/**
|
|
42
|
+
* Require MFA claim in the token (amr claim must contain 'mfa').
|
|
43
|
+
* @defaultValue false
|
|
44
|
+
*/
|
|
45
|
+
requireMFA?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AuthMiddlewareConfig {
|
|
49
|
+
/**
|
|
50
|
+
* Routes that require authentication
|
|
51
|
+
* @example ['/dashboard', '/profile', '/api/protected']
|
|
52
|
+
*/
|
|
53
|
+
protectedRoutes?: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Routes that should be accessible only when NOT authenticated
|
|
56
|
+
* @example ['/login', '/signup']
|
|
57
|
+
*/
|
|
58
|
+
publicOnlyRoutes?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Login page path
|
|
61
|
+
* @default '/login'
|
|
62
|
+
*/
|
|
63
|
+
loginPath?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Redirect path after login
|
|
66
|
+
* @default '/'
|
|
67
|
+
*/
|
|
68
|
+
redirectAfterLogin?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Cookie name for session
|
|
71
|
+
* @default 'msal.account'
|
|
72
|
+
*/
|
|
73
|
+
sessionCookie?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Custom authentication check function
|
|
76
|
+
*/
|
|
77
|
+
isAuthenticated?: (request: NextRequest) => boolean | Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Tenant access configuration (v5.1.0).
|
|
80
|
+
* Validated against the account stored in the session cookie.
|
|
81
|
+
*/
|
|
82
|
+
tenantConfig?: MultiTenantConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Path to redirect to when tenant access is denied (v5.1.0).
|
|
85
|
+
* @default '/unauthorized'
|
|
86
|
+
*/
|
|
87
|
+
tenantDeniedPath?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Enable debug logging
|
|
90
|
+
* @default false
|
|
91
|
+
*/
|
|
92
|
+
debug?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Creates authentication middleware for Next.js App Router
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* // middleware.ts
|
|
100
|
+
* import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
101
|
+
*
|
|
102
|
+
* export const middleware = createAuthMiddleware({
|
|
103
|
+
* protectedRoutes: ['/dashboard', '/profile'],
|
|
104
|
+
* publicOnlyRoutes: ['/login'],
|
|
105
|
+
* loginPath: '/login',
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* export const config = {
|
|
109
|
+
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
110
|
+
* };
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare function createAuthMiddleware(config?: AuthMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
114
|
+
|
|
115
|
+
export { type AuthMiddlewareConfig, createAuthMiddleware };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var server = require('next/server');
|
|
4
|
+
|
|
5
|
+
// src/middleware/createAuthMiddleware.ts
|
|
6
|
+
|
|
7
|
+
// src/utils/validation.ts
|
|
8
|
+
function safeJsonParse(jsonString, validator) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(jsonString);
|
|
11
|
+
if (validator(parsed)) {
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
console.warn("[Validation] JSON validation failed");
|
|
15
|
+
return null;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("[Validation] JSON parse error:", error);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isValidAccountData(data) {
|
|
22
|
+
return typeof data === "object" && data !== null && typeof data.homeAccountId === "string" && data.homeAccountId.length > 0 && typeof data.username === "string" && data.username.length > 0 && (data.name === void 0 || typeof data.name === "string");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/utils/tenantValidator.ts
|
|
26
|
+
function getTenantDomain(account) {
|
|
27
|
+
const upn = account.username || account.idTokenClaims?.preferred_username || account.idTokenClaims?.upn || "";
|
|
28
|
+
return upn.includes("@") ? upn.split("@")[1].toLowerCase() : null;
|
|
29
|
+
}
|
|
30
|
+
function getTenantId(account) {
|
|
31
|
+
return account.tenantId || account.idTokenClaims?.tid || null;
|
|
32
|
+
}
|
|
33
|
+
function matchesTenant(value, tenantId, tenantDomain) {
|
|
34
|
+
const v = value.toLowerCase();
|
|
35
|
+
if (tenantId && v === tenantId.toLowerCase()) return true;
|
|
36
|
+
if (tenantDomain && v === tenantDomain.toLowerCase()) return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function isGuestAccount(account) {
|
|
40
|
+
const claims = account.idTokenClaims ?? {};
|
|
41
|
+
const resourceTenantId = account.tenantId || claims["tid"] || null;
|
|
42
|
+
const issuer = claims["iss"] || null;
|
|
43
|
+
if (!issuer || !resourceTenantId) return false;
|
|
44
|
+
const match = issuer.match(
|
|
45
|
+
/https:\/\/login\.microsoftonline\.com\/([^/]+)(?:\/|$)/i
|
|
46
|
+
);
|
|
47
|
+
if (!match) return false;
|
|
48
|
+
const homeTenantId = match[1];
|
|
49
|
+
return homeTenantId.toLowerCase() !== resourceTenantId.toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
function validateTenantAccess(account, config) {
|
|
52
|
+
const tenantId = getTenantId(account);
|
|
53
|
+
const tenantDomain = getTenantDomain(account);
|
|
54
|
+
const claims = account.idTokenClaims ?? {};
|
|
55
|
+
if (config.blockList && config.blockList.length > 0) {
|
|
56
|
+
const blocked = config.blockList.some(
|
|
57
|
+
(entry) => matchesTenant(entry, tenantId, tenantDomain)
|
|
58
|
+
);
|
|
59
|
+
if (blocked) {
|
|
60
|
+
return {
|
|
61
|
+
allowed: false,
|
|
62
|
+
reason: `Tenant "${tenantDomain || tenantId}" is blocked from accessing this application.`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (config.allowList && config.allowList.length > 0) {
|
|
67
|
+
const allowed = config.allowList.some(
|
|
68
|
+
(entry) => matchesTenant(entry, tenantId, tenantDomain)
|
|
69
|
+
);
|
|
70
|
+
if (!allowed) {
|
|
71
|
+
return {
|
|
72
|
+
allowed: false,
|
|
73
|
+
reason: `Tenant "${tenantDomain || tenantId}" is not in the allowed list for this application.`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (config.requireType) {
|
|
78
|
+
const isGuest = isGuestAccount(account);
|
|
79
|
+
if (config.requireType === "Member" && isGuest) {
|
|
80
|
+
return {
|
|
81
|
+
allowed: false,
|
|
82
|
+
reason: "Only member accounts are allowed. Guest (B2B) accounts are not permitted."
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (config.requireType === "Guest" && !isGuest) {
|
|
86
|
+
return {
|
|
87
|
+
allowed: false,
|
|
88
|
+
reason: "Only guest (B2B) accounts are allowed."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (config.requireMFA) {
|
|
93
|
+
const amr = claims["amr"] || [];
|
|
94
|
+
const hasMfa = amr.includes("mfa") || amr.includes("ngcmfa") || amr.includes("hwk") || amr.includes("swk");
|
|
95
|
+
if (!hasMfa) {
|
|
96
|
+
return {
|
|
97
|
+
allowed: false,
|
|
98
|
+
reason: "Multi-factor authentication (MFA) is required to access this application."
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { allowed: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/middleware/createAuthMiddleware.ts
|
|
106
|
+
function createAuthMiddleware(config = {}) {
|
|
107
|
+
const {
|
|
108
|
+
protectedRoutes = [],
|
|
109
|
+
publicOnlyRoutes = [],
|
|
110
|
+
loginPath = "/login",
|
|
111
|
+
redirectAfterLogin = "/",
|
|
112
|
+
sessionCookie = "msal.account",
|
|
113
|
+
isAuthenticated: customAuthCheck,
|
|
114
|
+
tenantConfig,
|
|
115
|
+
tenantDeniedPath = "/unauthorized",
|
|
116
|
+
debug = false
|
|
117
|
+
} = config;
|
|
118
|
+
return async function authMiddleware(request) {
|
|
119
|
+
const { pathname } = request.nextUrl;
|
|
120
|
+
if (debug) {
|
|
121
|
+
console.log("[AuthMiddleware] Processing:", pathname);
|
|
122
|
+
}
|
|
123
|
+
let authenticated = false;
|
|
124
|
+
if (customAuthCheck) {
|
|
125
|
+
authenticated = await customAuthCheck(request);
|
|
126
|
+
} else {
|
|
127
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
128
|
+
authenticated = !!sessionData?.value;
|
|
129
|
+
}
|
|
130
|
+
if (debug) {
|
|
131
|
+
console.log("[AuthMiddleware] Authenticated:", authenticated);
|
|
132
|
+
}
|
|
133
|
+
const isProtectedRoute = protectedRoutes.some(
|
|
134
|
+
(route) => pathname.startsWith(route)
|
|
135
|
+
);
|
|
136
|
+
const isPublicOnlyRoute = publicOnlyRoutes.some(
|
|
137
|
+
(route) => pathname.startsWith(route)
|
|
138
|
+
);
|
|
139
|
+
if (isProtectedRoute && !authenticated) {
|
|
140
|
+
if (debug) {
|
|
141
|
+
console.log("[AuthMiddleware] Redirecting to login");
|
|
142
|
+
}
|
|
143
|
+
const url = request.nextUrl.clone();
|
|
144
|
+
url.pathname = loginPath;
|
|
145
|
+
url.searchParams.set("returnUrl", pathname);
|
|
146
|
+
return server.NextResponse.redirect(url);
|
|
147
|
+
}
|
|
148
|
+
if (isProtectedRoute && authenticated && tenantConfig) {
|
|
149
|
+
try {
|
|
150
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
151
|
+
if (sessionData?.value) {
|
|
152
|
+
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
153
|
+
if (account) {
|
|
154
|
+
const tenantResult = validateTenantAccess(account, tenantConfig);
|
|
155
|
+
if (!tenantResult.allowed) {
|
|
156
|
+
if (debug) {
|
|
157
|
+
console.log("[AuthMiddleware] Tenant access denied:", tenantResult.reason);
|
|
158
|
+
}
|
|
159
|
+
const url = request.nextUrl.clone();
|
|
160
|
+
url.pathname = tenantDeniedPath;
|
|
161
|
+
url.searchParams.set("reason", tenantResult.reason || "access_denied");
|
|
162
|
+
return server.NextResponse.redirect(url);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (debug) {
|
|
168
|
+
console.warn("[AuthMiddleware] Tenant validation error:", error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (isPublicOnlyRoute && authenticated) {
|
|
173
|
+
if (debug) {
|
|
174
|
+
console.log("[AuthMiddleware] Redirecting to home");
|
|
175
|
+
}
|
|
176
|
+
const returnUrl = request.nextUrl.searchParams.get("returnUrl");
|
|
177
|
+
const url = request.nextUrl.clone();
|
|
178
|
+
url.pathname = returnUrl || redirectAfterLogin;
|
|
179
|
+
url.searchParams.delete("returnUrl");
|
|
180
|
+
return server.NextResponse.redirect(url);
|
|
181
|
+
}
|
|
182
|
+
const response = server.NextResponse.next();
|
|
183
|
+
if (authenticated) {
|
|
184
|
+
response.headers.set("x-msal-authenticated", "true");
|
|
185
|
+
try {
|
|
186
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
187
|
+
if (sessionData?.value) {
|
|
188
|
+
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
189
|
+
if (account?.username) {
|
|
190
|
+
response.headers.set("x-msal-username", account.username);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (debug) {
|
|
195
|
+
console.warn("[AuthMiddleware] Failed to parse session data");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return response;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|