@chemmangat/msal-next 5.2.0 → 5.2.1
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 +26 -1
- package/dist/index.d.mts +22 -42
- package/dist/index.d.ts +22 -42
- package/dist/index.js +0 -101
- package/dist/index.mjs +0 -100
- 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/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [5.2.
|
|
5
|
+
## [5.2.1] - 2026-04-07
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fix
|
|
8
|
+
|
|
9
|
+
#### `createAuthMiddleware` now importable from `@chemmangat/msal-next/middleware`
|
|
10
|
+
|
|
11
|
+
`createAuthMiddleware` was previously bundled inside `dist/index.mjs` which carries a `"use client"` directive. Importing it in `middleware.ts` caused Next.js to throw:
|
|
12
|
+
|
|
13
|
+
> "Attempted to call createAuthMiddleware() from the server but createAuthMiddleware is on the client."
|
|
14
|
+
|
|
15
|
+
It now has its own edge-compatible entry point with no React, no `@azure/msal-browser`, and no `"use client"` — only `next/server`.
|
|
16
|
+
|
|
17
|
+
**Migration** (update your import):
|
|
18
|
+
```ts
|
|
19
|
+
// Before
|
|
20
|
+
import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
21
|
+
|
|
22
|
+
// After
|
|
23
|
+
import { createAuthMiddleware } from '@chemmangat/msal-next/middleware';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Also fixed `tenantValidator.ts` to use `import type` for its `@azure/msal-browser` and `types.ts` imports, ensuring those never get bundled into the middleware output.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
|
|
6
31
|
|
|
7
32
|
### 🔧 Compatibility
|
|
8
33
|
|
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { Configuration, LogLevel, IPublicClientApplication, PublicClientApplication, AccountInfo } from '@azure/msal-browser';
|
|
3
3
|
export { AccountInfo } from '@azure/msal-browser';
|
|
4
4
|
import { ReactNode, CSSProperties, Component, ErrorInfo, ComponentType } from 'react';
|
|
5
|
-
import { NextRequest
|
|
5
|
+
import { NextRequest } from 'next/server';
|
|
6
6
|
export { useAccount, useIsAuthenticated, useMsal } from '@azure/msal-react';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -1865,6 +1865,26 @@ declare function withPageAuth<P extends object>(Component: ComponentType<P>, aut
|
|
|
1865
1865
|
displayName: string;
|
|
1866
1866
|
};
|
|
1867
1867
|
|
|
1868
|
+
interface ServerSession {
|
|
1869
|
+
/**
|
|
1870
|
+
* Whether user is authenticated
|
|
1871
|
+
*/
|
|
1872
|
+
isAuthenticated: boolean;
|
|
1873
|
+
/**
|
|
1874
|
+
* User's account ID from MSAL cache
|
|
1875
|
+
*/
|
|
1876
|
+
accountId?: string;
|
|
1877
|
+
/**
|
|
1878
|
+
* User's username/email
|
|
1879
|
+
*/
|
|
1880
|
+
username?: string;
|
|
1881
|
+
/**
|
|
1882
|
+
* Access token (if available in cookie)
|
|
1883
|
+
* @deprecated Storing tokens in cookies is not recommended for security reasons
|
|
1884
|
+
*/
|
|
1885
|
+
accessToken?: string;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1868
1888
|
interface AuthMiddlewareConfig {
|
|
1869
1889
|
/**
|
|
1870
1890
|
* Routes that require authentication
|
|
@@ -1911,45 +1931,5 @@ interface AuthMiddlewareConfig {
|
|
|
1911
1931
|
*/
|
|
1912
1932
|
debug?: boolean;
|
|
1913
1933
|
}
|
|
1914
|
-
/**
|
|
1915
|
-
* Creates authentication middleware for Next.js App Router
|
|
1916
|
-
*
|
|
1917
|
-
* @example
|
|
1918
|
-
* ```tsx
|
|
1919
|
-
* // middleware.ts
|
|
1920
|
-
* import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
1921
|
-
*
|
|
1922
|
-
* export const middleware = createAuthMiddleware({
|
|
1923
|
-
* protectedRoutes: ['/dashboard', '/profile'],
|
|
1924
|
-
* publicOnlyRoutes: ['/login'],
|
|
1925
|
-
* loginPath: '/login',
|
|
1926
|
-
* });
|
|
1927
|
-
*
|
|
1928
|
-
* export const config = {
|
|
1929
|
-
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
1930
|
-
* };
|
|
1931
|
-
* ```
|
|
1932
|
-
*/
|
|
1933
|
-
declare function createAuthMiddleware(config?: AuthMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
1934
|
-
|
|
1935
|
-
interface ServerSession {
|
|
1936
|
-
/**
|
|
1937
|
-
* Whether user is authenticated
|
|
1938
|
-
*/
|
|
1939
|
-
isAuthenticated: boolean;
|
|
1940
|
-
/**
|
|
1941
|
-
* User's account ID from MSAL cache
|
|
1942
|
-
*/
|
|
1943
|
-
accountId?: string;
|
|
1944
|
-
/**
|
|
1945
|
-
* User's username/email
|
|
1946
|
-
*/
|
|
1947
|
-
username?: string;
|
|
1948
|
-
/**
|
|
1949
|
-
* Access token (if available in cookie)
|
|
1950
|
-
* @deprecated Storing tokens in cookies is not recommended for security reasons
|
|
1951
|
-
*/
|
|
1952
|
-
accessToken?: string;
|
|
1953
|
-
}
|
|
1954
1934
|
|
|
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,
|
|
1935
|
+
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, 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { Configuration, LogLevel, IPublicClientApplication, PublicClientApplication, AccountInfo } from '@azure/msal-browser';
|
|
3
3
|
export { AccountInfo } from '@azure/msal-browser';
|
|
4
4
|
import { ReactNode, CSSProperties, Component, ErrorInfo, ComponentType } from 'react';
|
|
5
|
-
import { NextRequest
|
|
5
|
+
import { NextRequest } from 'next/server';
|
|
6
6
|
export { useAccount, useIsAuthenticated, useMsal } from '@azure/msal-react';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -1865,6 +1865,26 @@ declare function withPageAuth<P extends object>(Component: ComponentType<P>, aut
|
|
|
1865
1865
|
displayName: string;
|
|
1866
1866
|
};
|
|
1867
1867
|
|
|
1868
|
+
interface ServerSession {
|
|
1869
|
+
/**
|
|
1870
|
+
* Whether user is authenticated
|
|
1871
|
+
*/
|
|
1872
|
+
isAuthenticated: boolean;
|
|
1873
|
+
/**
|
|
1874
|
+
* User's account ID from MSAL cache
|
|
1875
|
+
*/
|
|
1876
|
+
accountId?: string;
|
|
1877
|
+
/**
|
|
1878
|
+
* User's username/email
|
|
1879
|
+
*/
|
|
1880
|
+
username?: string;
|
|
1881
|
+
/**
|
|
1882
|
+
* Access token (if available in cookie)
|
|
1883
|
+
* @deprecated Storing tokens in cookies is not recommended for security reasons
|
|
1884
|
+
*/
|
|
1885
|
+
accessToken?: string;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1868
1888
|
interface AuthMiddlewareConfig {
|
|
1869
1889
|
/**
|
|
1870
1890
|
* Routes that require authentication
|
|
@@ -1911,45 +1931,5 @@ interface AuthMiddlewareConfig {
|
|
|
1911
1931
|
*/
|
|
1912
1932
|
debug?: boolean;
|
|
1913
1933
|
}
|
|
1914
|
-
/**
|
|
1915
|
-
* Creates authentication middleware for Next.js App Router
|
|
1916
|
-
*
|
|
1917
|
-
* @example
|
|
1918
|
-
* ```tsx
|
|
1919
|
-
* // middleware.ts
|
|
1920
|
-
* import { createAuthMiddleware } from '@chemmangat/msal-next';
|
|
1921
|
-
*
|
|
1922
|
-
* export const middleware = createAuthMiddleware({
|
|
1923
|
-
* protectedRoutes: ['/dashboard', '/profile'],
|
|
1924
|
-
* publicOnlyRoutes: ['/login'],
|
|
1925
|
-
* loginPath: '/login',
|
|
1926
|
-
* });
|
|
1927
|
-
*
|
|
1928
|
-
* export const config = {
|
|
1929
|
-
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
1930
|
-
* };
|
|
1931
|
-
* ```
|
|
1932
|
-
*/
|
|
1933
|
-
declare function createAuthMiddleware(config?: AuthMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
1934
|
-
|
|
1935
|
-
interface ServerSession {
|
|
1936
|
-
/**
|
|
1937
|
-
* Whether user is authenticated
|
|
1938
|
-
*/
|
|
1939
|
-
isAuthenticated: boolean;
|
|
1940
|
-
/**
|
|
1941
|
-
* User's account ID from MSAL cache
|
|
1942
|
-
*/
|
|
1943
|
-
accountId?: string;
|
|
1944
|
-
/**
|
|
1945
|
-
* User's username/email
|
|
1946
|
-
*/
|
|
1947
|
-
username?: string;
|
|
1948
|
-
/**
|
|
1949
|
-
* Access token (if available in cookie)
|
|
1950
|
-
* @deprecated Storing tokens in cookies is not recommended for security reasons
|
|
1951
|
-
*/
|
|
1952
|
-
accessToken?: string;
|
|
1953
|
-
}
|
|
1954
1934
|
|
|
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,
|
|
1935
|
+
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, 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 };
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,6 @@ __export(client_exports, {
|
|
|
33
33
|
ProtectedPage: () => ProtectedPage,
|
|
34
34
|
SignOutButton: () => SignOutButton,
|
|
35
35
|
UserAvatar: () => UserAvatar,
|
|
36
|
-
createAuthMiddleware: () => createAuthMiddleware,
|
|
37
36
|
createMissingEnvVarError: () => createMissingEnvVarError,
|
|
38
37
|
createMsalConfig: () => createMsalConfig,
|
|
39
38
|
createRetryWrapper: () => createRetryWrapper,
|
|
@@ -3166,105 +3165,6 @@ function withPageAuth(Component2, authConfig, globalConfig) {
|
|
|
3166
3165
|
return WrappedComponent;
|
|
3167
3166
|
}
|
|
3168
3167
|
|
|
3169
|
-
// src/middleware/createAuthMiddleware.ts
|
|
3170
|
-
var import_server = require("next/server");
|
|
3171
|
-
function createAuthMiddleware(config = {}) {
|
|
3172
|
-
const {
|
|
3173
|
-
protectedRoutes = [],
|
|
3174
|
-
publicOnlyRoutes = [],
|
|
3175
|
-
loginPath = "/login",
|
|
3176
|
-
redirectAfterLogin = "/",
|
|
3177
|
-
sessionCookie = "msal.account",
|
|
3178
|
-
isAuthenticated: customAuthCheck,
|
|
3179
|
-
tenantConfig,
|
|
3180
|
-
tenantDeniedPath = "/unauthorized",
|
|
3181
|
-
debug = false
|
|
3182
|
-
} = config;
|
|
3183
|
-
return async function authMiddleware(request) {
|
|
3184
|
-
const { pathname } = request.nextUrl;
|
|
3185
|
-
if (debug) {
|
|
3186
|
-
console.log("[AuthMiddleware] Processing:", pathname);
|
|
3187
|
-
}
|
|
3188
|
-
let authenticated = false;
|
|
3189
|
-
if (customAuthCheck) {
|
|
3190
|
-
authenticated = await customAuthCheck(request);
|
|
3191
|
-
} else {
|
|
3192
|
-
const sessionData = request.cookies.get(sessionCookie);
|
|
3193
|
-
authenticated = !!sessionData?.value;
|
|
3194
|
-
}
|
|
3195
|
-
if (debug) {
|
|
3196
|
-
console.log("[AuthMiddleware] Authenticated:", authenticated);
|
|
3197
|
-
}
|
|
3198
|
-
const isProtectedRoute = protectedRoutes.some(
|
|
3199
|
-
(route) => pathname.startsWith(route)
|
|
3200
|
-
);
|
|
3201
|
-
const isPublicOnlyRoute = publicOnlyRoutes.some(
|
|
3202
|
-
(route) => pathname.startsWith(route)
|
|
3203
|
-
);
|
|
3204
|
-
if (isProtectedRoute && !authenticated) {
|
|
3205
|
-
if (debug) {
|
|
3206
|
-
console.log("[AuthMiddleware] Redirecting to login");
|
|
3207
|
-
}
|
|
3208
|
-
const url = request.nextUrl.clone();
|
|
3209
|
-
url.pathname = loginPath;
|
|
3210
|
-
url.searchParams.set("returnUrl", pathname);
|
|
3211
|
-
return import_server.NextResponse.redirect(url);
|
|
3212
|
-
}
|
|
3213
|
-
if (isProtectedRoute && authenticated && tenantConfig) {
|
|
3214
|
-
try {
|
|
3215
|
-
const sessionData = request.cookies.get(sessionCookie);
|
|
3216
|
-
if (sessionData?.value) {
|
|
3217
|
-
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
3218
|
-
if (account) {
|
|
3219
|
-
const tenantResult = validateTenantAccess(account, tenantConfig);
|
|
3220
|
-
if (!tenantResult.allowed) {
|
|
3221
|
-
if (debug) {
|
|
3222
|
-
console.log("[AuthMiddleware] Tenant access denied:", tenantResult.reason);
|
|
3223
|
-
}
|
|
3224
|
-
const url = request.nextUrl.clone();
|
|
3225
|
-
url.pathname = tenantDeniedPath;
|
|
3226
|
-
url.searchParams.set("reason", tenantResult.reason || "access_denied");
|
|
3227
|
-
return import_server.NextResponse.redirect(url);
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
} catch (error) {
|
|
3232
|
-
if (debug) {
|
|
3233
|
-
console.warn("[AuthMiddleware] Tenant validation error:", error);
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
if (isPublicOnlyRoute && authenticated) {
|
|
3238
|
-
if (debug) {
|
|
3239
|
-
console.log("[AuthMiddleware] Redirecting to home");
|
|
3240
|
-
}
|
|
3241
|
-
const returnUrl = request.nextUrl.searchParams.get("returnUrl");
|
|
3242
|
-
const url = request.nextUrl.clone();
|
|
3243
|
-
url.pathname = returnUrl || redirectAfterLogin;
|
|
3244
|
-
url.searchParams.delete("returnUrl");
|
|
3245
|
-
return import_server.NextResponse.redirect(url);
|
|
3246
|
-
}
|
|
3247
|
-
const response = import_server.NextResponse.next();
|
|
3248
|
-
if (authenticated) {
|
|
3249
|
-
response.headers.set("x-msal-authenticated", "true");
|
|
3250
|
-
try {
|
|
3251
|
-
const sessionData = request.cookies.get(sessionCookie);
|
|
3252
|
-
if (sessionData?.value) {
|
|
3253
|
-
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
3254
|
-
if (account?.username) {
|
|
3255
|
-
response.headers.set("x-msal-username", account.username);
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
} catch (error) {
|
|
3259
|
-
if (debug) {
|
|
3260
|
-
console.warn("[AuthMiddleware] Failed to parse session data");
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
return response;
|
|
3265
|
-
};
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
3168
|
// src/client.ts
|
|
3269
3169
|
var import_msal_react4 = require("@azure/msal-react");
|
|
3270
3170
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -3281,7 +3181,6 @@ var import_msal_react4 = require("@azure/msal-react");
|
|
|
3281
3181
|
ProtectedPage,
|
|
3282
3182
|
SignOutButton,
|
|
3283
3183
|
UserAvatar,
|
|
3284
|
-
createAuthMiddleware,
|
|
3285
3184
|
createMissingEnvVarError,
|
|
3286
3185
|
createMsalConfig,
|
|
3287
3186
|
createRetryWrapper,
|
package/dist/index.mjs
CHANGED
|
@@ -3106,105 +3106,6 @@ function withPageAuth(Component2, authConfig, globalConfig) {
|
|
|
3106
3106
|
return WrappedComponent;
|
|
3107
3107
|
}
|
|
3108
3108
|
|
|
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
3109
|
// src/client.ts
|
|
3209
3110
|
import { useMsal as useMsal3, useIsAuthenticated, useAccount as useAccount2 } from "@azure/msal-react";
|
|
3210
3111
|
export {
|
|
@@ -3220,7 +3121,6 @@ export {
|
|
|
3220
3121
|
ProtectedPage,
|
|
3221
3122
|
SignOutButton,
|
|
3222
3123
|
UserAvatar,
|
|
3223
|
-
createAuthMiddleware,
|
|
3224
3124
|
createMissingEnvVarError,
|
|
3225
3125
|
createMsalConfig,
|
|
3226
3126
|
createRetryWrapper,
|
|
@@ -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;
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemmangat/msal-next",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.1",
|
|
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": [
|