@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
// src/middleware/createAuthMiddleware.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/validation.ts
|
|
6
|
+
function safeJsonParse(jsonString, validator) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(jsonString);
|
|
9
|
+
if (validator(parsed)) {
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
console.warn("[Validation] JSON validation failed");
|
|
13
|
+
return null;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("[Validation] JSON parse error:", error);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isValidAccountData(data) {
|
|
20
|
+
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");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/utils/tenantValidator.ts
|
|
24
|
+
function getTenantDomain(account) {
|
|
25
|
+
const upn = account.username || account.idTokenClaims?.preferred_username || account.idTokenClaims?.upn || "";
|
|
26
|
+
return upn.includes("@") ? upn.split("@")[1].toLowerCase() : null;
|
|
27
|
+
}
|
|
28
|
+
function getTenantId(account) {
|
|
29
|
+
return account.tenantId || account.idTokenClaims?.tid || null;
|
|
30
|
+
}
|
|
31
|
+
function matchesTenant(value, tenantId, tenantDomain) {
|
|
32
|
+
const v = value.toLowerCase();
|
|
33
|
+
if (tenantId && v === tenantId.toLowerCase()) return true;
|
|
34
|
+
if (tenantDomain && v === tenantDomain.toLowerCase()) return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function isGuestAccount(account) {
|
|
38
|
+
const claims = account.idTokenClaims ?? {};
|
|
39
|
+
const resourceTenantId = account.tenantId || claims["tid"] || null;
|
|
40
|
+
const issuer = claims["iss"] || null;
|
|
41
|
+
if (!issuer || !resourceTenantId) return false;
|
|
42
|
+
const match = issuer.match(
|
|
43
|
+
/https:\/\/login\.microsoftonline\.com\/([^/]+)(?:\/|$)/i
|
|
44
|
+
);
|
|
45
|
+
if (!match) return false;
|
|
46
|
+
const homeTenantId = match[1];
|
|
47
|
+
return homeTenantId.toLowerCase() !== resourceTenantId.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
function validateTenantAccess(account, config) {
|
|
50
|
+
const tenantId = getTenantId(account);
|
|
51
|
+
const tenantDomain = getTenantDomain(account);
|
|
52
|
+
const claims = account.idTokenClaims ?? {};
|
|
53
|
+
if (config.blockList && config.blockList.length > 0) {
|
|
54
|
+
const blocked = config.blockList.some(
|
|
55
|
+
(entry) => matchesTenant(entry, tenantId, tenantDomain)
|
|
56
|
+
);
|
|
57
|
+
if (blocked) {
|
|
58
|
+
return {
|
|
59
|
+
allowed: false,
|
|
60
|
+
reason: `Tenant "${tenantDomain || tenantId}" is blocked from accessing this application.`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (config.allowList && config.allowList.length > 0) {
|
|
65
|
+
const allowed = config.allowList.some(
|
|
66
|
+
(entry) => matchesTenant(entry, tenantId, tenantDomain)
|
|
67
|
+
);
|
|
68
|
+
if (!allowed) {
|
|
69
|
+
return {
|
|
70
|
+
allowed: false,
|
|
71
|
+
reason: `Tenant "${tenantDomain || tenantId}" is not in the allowed list for this application.`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (config.requireType) {
|
|
76
|
+
const isGuest = isGuestAccount(account);
|
|
77
|
+
if (config.requireType === "Member" && isGuest) {
|
|
78
|
+
return {
|
|
79
|
+
allowed: false,
|
|
80
|
+
reason: "Only member accounts are allowed. Guest (B2B) accounts are not permitted."
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (config.requireType === "Guest" && !isGuest) {
|
|
84
|
+
return {
|
|
85
|
+
allowed: false,
|
|
86
|
+
reason: "Only guest (B2B) accounts are allowed."
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (config.requireMFA) {
|
|
91
|
+
const amr = claims["amr"] || [];
|
|
92
|
+
const hasMfa = amr.includes("mfa") || amr.includes("ngcmfa") || amr.includes("hwk") || amr.includes("swk");
|
|
93
|
+
if (!hasMfa) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
reason: "Multi-factor authentication (MFA) is required to access this application."
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { allowed: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/middleware/createAuthMiddleware.ts
|
|
104
|
+
function createAuthMiddleware(config = {}) {
|
|
105
|
+
const {
|
|
106
|
+
protectedRoutes = [],
|
|
107
|
+
publicOnlyRoutes = [],
|
|
108
|
+
loginPath = "/login",
|
|
109
|
+
redirectAfterLogin = "/",
|
|
110
|
+
sessionCookie = "msal.account",
|
|
111
|
+
isAuthenticated: customAuthCheck,
|
|
112
|
+
tenantConfig,
|
|
113
|
+
tenantDeniedPath = "/unauthorized",
|
|
114
|
+
debug = false
|
|
115
|
+
} = config;
|
|
116
|
+
return async function authMiddleware(request) {
|
|
117
|
+
const { pathname } = request.nextUrl;
|
|
118
|
+
if (debug) {
|
|
119
|
+
console.log("[AuthMiddleware] Processing:", pathname);
|
|
120
|
+
}
|
|
121
|
+
let authenticated = false;
|
|
122
|
+
if (customAuthCheck) {
|
|
123
|
+
authenticated = await customAuthCheck(request);
|
|
124
|
+
} else {
|
|
125
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
126
|
+
authenticated = !!sessionData?.value;
|
|
127
|
+
}
|
|
128
|
+
if (debug) {
|
|
129
|
+
console.log("[AuthMiddleware] Authenticated:", authenticated);
|
|
130
|
+
}
|
|
131
|
+
const isProtectedRoute = protectedRoutes.some(
|
|
132
|
+
(route) => pathname.startsWith(route)
|
|
133
|
+
);
|
|
134
|
+
const isPublicOnlyRoute = publicOnlyRoutes.some(
|
|
135
|
+
(route) => pathname.startsWith(route)
|
|
136
|
+
);
|
|
137
|
+
if (isProtectedRoute && !authenticated) {
|
|
138
|
+
if (debug) {
|
|
139
|
+
console.log("[AuthMiddleware] Redirecting to login");
|
|
140
|
+
}
|
|
141
|
+
const url = request.nextUrl.clone();
|
|
142
|
+
url.pathname = loginPath;
|
|
143
|
+
url.searchParams.set("returnUrl", pathname);
|
|
144
|
+
return NextResponse.redirect(url);
|
|
145
|
+
}
|
|
146
|
+
if (isProtectedRoute && authenticated && tenantConfig) {
|
|
147
|
+
try {
|
|
148
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
149
|
+
if (sessionData?.value) {
|
|
150
|
+
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
151
|
+
if (account) {
|
|
152
|
+
const tenantResult = validateTenantAccess(account, tenantConfig);
|
|
153
|
+
if (!tenantResult.allowed) {
|
|
154
|
+
if (debug) {
|
|
155
|
+
console.log("[AuthMiddleware] Tenant access denied:", tenantResult.reason);
|
|
156
|
+
}
|
|
157
|
+
const url = request.nextUrl.clone();
|
|
158
|
+
url.pathname = tenantDeniedPath;
|
|
159
|
+
url.searchParams.set("reason", tenantResult.reason || "access_denied");
|
|
160
|
+
return NextResponse.redirect(url);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (debug) {
|
|
166
|
+
console.warn("[AuthMiddleware] Tenant validation error:", error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (isPublicOnlyRoute && authenticated) {
|
|
171
|
+
if (debug) {
|
|
172
|
+
console.log("[AuthMiddleware] Redirecting to home");
|
|
173
|
+
}
|
|
174
|
+
const returnUrl = request.nextUrl.searchParams.get("returnUrl");
|
|
175
|
+
const url = request.nextUrl.clone();
|
|
176
|
+
url.pathname = returnUrl || redirectAfterLogin;
|
|
177
|
+
url.searchParams.delete("returnUrl");
|
|
178
|
+
return NextResponse.redirect(url);
|
|
179
|
+
}
|
|
180
|
+
const response = NextResponse.next();
|
|
181
|
+
if (authenticated) {
|
|
182
|
+
response.headers.set("x-msal-authenticated", "true");
|
|
183
|
+
try {
|
|
184
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
185
|
+
if (sessionData?.value) {
|
|
186
|
+
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
187
|
+
if (account?.username) {
|
|
188
|
+
response.headers.set("x-msal-username", account.username);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (debug) {
|
|
193
|
+
console.warn("[AuthMiddleware] Failed to parse session data");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return response;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { createAuthMiddleware };
|
package/dist/server.d.mts
CHANGED
|
@@ -41,14 +41,33 @@ interface ServerSession {
|
|
|
41
41
|
*/
|
|
42
42
|
declare function getServerSession(): Promise<ServerSession>;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Writes the `msal.account` session cookie directly via `document.cookie`.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* **Must be called from a Client Component** (browser context only).
|
|
48
|
+
*
|
|
49
|
+
* As of v5.3.0 this is no longer necessary for most apps — `MsalAuthProvider`
|
|
50
|
+
* automatically writes and clears the cookie on every login/logout event.
|
|
51
|
+
* Only call this manually if you need to set the cookie outside of the normal
|
|
52
|
+
* MSAL auth flow (e.g. after a silent SSO check in a custom component).
|
|
45
53
|
*
|
|
46
54
|
* @example
|
|
47
55
|
* ```tsx
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* 'use client';
|
|
57
|
+
* import { setServerSessionCookie } from '@chemmangat/msal-next/server';
|
|
58
|
+
*
|
|
59
|
+
* // After a custom auth event:
|
|
60
|
+
* setServerSessionCookie(account);
|
|
50
61
|
* ```
|
|
51
62
|
*/
|
|
52
|
-
declare function setServerSessionCookie(account: any
|
|
63
|
+
declare function setServerSessionCookie(account: any): void;
|
|
64
|
+
/**
|
|
65
|
+
* Clears the `msal.account` session cookie.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* **Must be called from a Client Component** (browser context only).
|
|
69
|
+
* As of v5.3.0 this is handled automatically by `MsalAuthProvider` on logout.
|
|
70
|
+
*/
|
|
71
|
+
declare function clearServerSessionCookie(): void;
|
|
53
72
|
|
|
54
|
-
export { type ServerSession, getServerSession, setServerSessionCookie };
|
|
73
|
+
export { type ServerSession, clearServerSessionCookie, getServerSession, setServerSessionCookie };
|
package/dist/server.d.ts
CHANGED
|
@@ -41,14 +41,33 @@ interface ServerSession {
|
|
|
41
41
|
*/
|
|
42
42
|
declare function getServerSession(): Promise<ServerSession>;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Writes the `msal.account` session cookie directly via `document.cookie`.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* **Must be called from a Client Component** (browser context only).
|
|
48
|
+
*
|
|
49
|
+
* As of v5.3.0 this is no longer necessary for most apps — `MsalAuthProvider`
|
|
50
|
+
* automatically writes and clears the cookie on every login/logout event.
|
|
51
|
+
* Only call this manually if you need to set the cookie outside of the normal
|
|
52
|
+
* MSAL auth flow (e.g. after a silent SSO check in a custom component).
|
|
45
53
|
*
|
|
46
54
|
* @example
|
|
47
55
|
* ```tsx
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* 'use client';
|
|
57
|
+
* import { setServerSessionCookie } from '@chemmangat/msal-next/server';
|
|
58
|
+
*
|
|
59
|
+
* // After a custom auth event:
|
|
60
|
+
* setServerSessionCookie(account);
|
|
50
61
|
* ```
|
|
51
62
|
*/
|
|
52
|
-
declare function setServerSessionCookie(account: any
|
|
63
|
+
declare function setServerSessionCookie(account: any): void;
|
|
64
|
+
/**
|
|
65
|
+
* Clears the `msal.account` session cookie.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* **Must be called from a Client Component** (browser context only).
|
|
69
|
+
* As of v5.3.0 this is handled automatically by `MsalAuthProvider` on logout.
|
|
70
|
+
*/
|
|
71
|
+
declare function clearServerSessionCookie(): void;
|
|
53
72
|
|
|
54
|
-
export { type ServerSession, getServerSession, setServerSessionCookie };
|
|
73
|
+
export { type ServerSession, clearServerSessionCookie, getServerSession, setServerSessionCookie };
|
package/dist/server.js
CHANGED
|
@@ -63,27 +63,29 @@ async function getServerSession() {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
function setServerSessionCookie(account) {
|
|
67
|
+
if (typeof document === "undefined") {
|
|
68
|
+
console.warn("[ServerSession] setServerSessionCookie must be called in a browser (Client Component) context.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
67
71
|
try {
|
|
68
|
-
const
|
|
72
|
+
const data = encodeURIComponent(JSON.stringify({
|
|
69
73
|
homeAccountId: account.homeAccountId,
|
|
70
74
|
username: account.username,
|
|
71
|
-
name: account.name
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
method: "POST",
|
|
75
|
-
headers: {
|
|
76
|
-
"Content-Type": "application/json"
|
|
77
|
-
},
|
|
78
|
-
body: JSON.stringify({
|
|
79
|
-
account: accountData,
|
|
80
|
-
token: accessToken
|
|
81
|
-
})
|
|
82
|
-
});
|
|
75
|
+
name: account.name ?? ""
|
|
76
|
+
}));
|
|
77
|
+
document.cookie = `msal.account=${data}; path=/; SameSite=Lax`;
|
|
83
78
|
} catch (error) {
|
|
84
79
|
console.error("[ServerSession] Failed to set session cookie:", error);
|
|
85
80
|
}
|
|
86
81
|
}
|
|
82
|
+
function clearServerSessionCookie() {
|
|
83
|
+
if (typeof document === "undefined") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
document.cookie = "msal.account=; path=/; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
|
87
|
+
}
|
|
87
88
|
|
|
89
|
+
exports.clearServerSessionCookie = clearServerSessionCookie;
|
|
88
90
|
exports.getServerSession = getServerSession;
|
|
89
91
|
exports.setServerSessionCookie = setServerSessionCookie;
|
package/dist/server.mjs
CHANGED
|
@@ -61,26 +61,27 @@ async function getServerSession() {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
function setServerSessionCookie(account) {
|
|
65
|
+
if (typeof document === "undefined") {
|
|
66
|
+
console.warn("[ServerSession] setServerSessionCookie must be called in a browser (Client Component) context.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
65
69
|
try {
|
|
66
|
-
const
|
|
70
|
+
const data = encodeURIComponent(JSON.stringify({
|
|
67
71
|
homeAccountId: account.homeAccountId,
|
|
68
72
|
username: account.username,
|
|
69
|
-
name: account.name
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers: {
|
|
74
|
-
"Content-Type": "application/json"
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify({
|
|
77
|
-
account: accountData,
|
|
78
|
-
token: accessToken
|
|
79
|
-
})
|
|
80
|
-
});
|
|
73
|
+
name: account.name ?? ""
|
|
74
|
+
}));
|
|
75
|
+
document.cookie = `msal.account=${data}; path=/; SameSite=Lax`;
|
|
81
76
|
} catch (error) {
|
|
82
77
|
console.error("[ServerSession] Failed to set session cookie:", error);
|
|
83
78
|
}
|
|
84
79
|
}
|
|
80
|
+
function clearServerSessionCookie() {
|
|
81
|
+
if (typeof document === "undefined") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
document.cookie = "msal.account=; path=/; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
|
85
|
+
}
|
|
85
86
|
|
|
86
|
-
export { getServerSession, setServerSessionCookie };
|
|
87
|
+
export { clearServerSessionCookie, getServerSession, setServerSessionCookie };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemmangat/msal-next",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "Production-ready Microsoft/Azure AD authentication for Next.js App Router. Zero-config setup, TypeScript-first, multi-account support, auto token refresh. The easiest way to add Microsoft login to your Next.js app.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"types": "./dist/server.d.ts",
|
|
16
16
|
"import": "./dist/server.mjs",
|
|
17
17
|
"require": "./dist/server.js"
|
|
18
|
+
},
|
|
19
|
+
"./middleware": {
|
|
20
|
+
"types": "./dist/middleware.d.ts",
|
|
21
|
+
"import": "./dist/middleware.mjs",
|
|
22
|
+
"require": "./dist/middleware.js"
|
|
18
23
|
}
|
|
19
24
|
},
|
|
20
25
|
"files": [
|