@payez/next-mvp 4.1.5 → 4.1.7
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.
|
@@ -59,8 +59,7 @@ async function GET(req) {
|
|
|
59
59
|
// Check if access token is expired (for middleware decision-making)
|
|
60
60
|
const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
|
|
61
61
|
const accessTokenExpired = accessTokenExpires < Date.now();
|
|
62
|
-
// Get requires2FA from cached client config (
|
|
63
|
-
// This is a client-wide setting from the broker handshake
|
|
62
|
+
// Get requires2FA from cached client config (client-wide setting)
|
|
64
63
|
let requires2FA = true; // Default to true for security
|
|
65
64
|
try {
|
|
66
65
|
const cachedConfig = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
@@ -69,6 +68,12 @@ async function GET(req) {
|
|
|
69
68
|
catch (e) {
|
|
70
69
|
console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
|
|
71
70
|
}
|
|
71
|
+
// CRITICAL: OAuth sessions without refresh token always need 2FA completion
|
|
72
|
+
// to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
|
|
73
|
+
// when no refresh_token is present. This must override the client config.
|
|
74
|
+
if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
|
|
75
|
+
requires2FA = true;
|
|
76
|
+
}
|
|
72
77
|
// CRITICAL: Check if MFA has expired (2FA TTL enforcement)
|
|
73
78
|
// The session may have mfaVerified=true from days ago, but if mfaExpiresAt
|
|
74
79
|
// has passed, we must treat 2FA as incomplete to force re-verification.
|
|
@@ -43,7 +43,7 @@ function makeAuthDecision(context) {
|
|
|
43
43
|
const safeCallbackUrl2 = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl;
|
|
44
44
|
return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl2)}`, reason: '2fa_required_login_redirect' };
|
|
45
45
|
}
|
|
46
|
-
return { type: '
|
|
46
|
+
return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: '2fa_required' };
|
|
47
47
|
}
|
|
48
48
|
if (sessionPointer.expired) {
|
|
49
49
|
if (isLoginPage)
|
package/package.json
CHANGED
|
@@ -69,8 +69,7 @@ export async function GET(req: NextRequest) {
|
|
|
69
69
|
const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
|
|
70
70
|
const accessTokenExpired = accessTokenExpires < Date.now();
|
|
71
71
|
|
|
72
|
-
// Get requires2FA from cached client config (
|
|
73
|
-
// This is a client-wide setting from the broker handshake
|
|
72
|
+
// Get requires2FA from cached client config (client-wide setting)
|
|
74
73
|
let requires2FA = true; // Default to true for security
|
|
75
74
|
try {
|
|
76
75
|
const cachedConfig = await getIDPClientConfig();
|
|
@@ -79,6 +78,13 @@ export async function GET(req: NextRequest) {
|
|
|
79
78
|
console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
|
|
80
79
|
}
|
|
81
80
|
|
|
81
|
+
// CRITICAL: OAuth sessions without refresh token always need 2FA completion
|
|
82
|
+
// to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
|
|
83
|
+
// when no refresh_token is present. This must override the client config.
|
|
84
|
+
if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
|
|
85
|
+
requires2FA = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
// CRITICAL: Check if MFA has expired (2FA TTL enforcement)
|
|
83
89
|
// The session may have mfaVerified=true from days ago, but if mfaExpiresAt
|
|
84
90
|
// has passed, we must treat 2FA as incomplete to force re-verification.
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth Decision Engine (lifted)
|
|
3
|
-
*/
|
|
4
|
-
import { NextRequest } from 'next/server';
|
|
5
|
-
|
|
6
|
-
export interface AuthContext {
|
|
7
|
-
pathname: string;
|
|
8
|
-
isPublicRoute: boolean;
|
|
9
|
-
isLoginPage: boolean;
|
|
10
|
-
searchParams: URLSearchParams;
|
|
11
|
-
sessionPointer: { exists: boolean; sessionToken?: string; expired?: boolean; hasRefreshToken?: boolean; };
|
|
12
|
-
sessionStatus: { exists: boolean | null; forceInvalid: boolean | null; requires2FA: boolean; twoFactorComplete: boolean; };
|
|
13
|
-
circuitBreakerOpen: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type AuthAction =
|
|
17
|
-
| { type: 'allow' }
|
|
18
|
-
| { type: 'redirect'; location: string; reason: string; clearCookies?: boolean }
|
|
19
|
-
| { type: 'service_error'; reason: string };
|
|
20
|
-
|
|
21
|
-
export function makeAuthDecision(context: AuthContext): AuthAction {
|
|
22
|
-
const { pathname, isPublicRoute, isLoginPage, searchParams, sessionPointer, sessionStatus, circuitBreakerOpen } = context;
|
|
23
|
-
// SAFEGUARD: Never use auth pages as callback URLs to prevent redirect loops
|
|
24
|
-
const safeCallbackUrl = pathname.startsWith('/account-auth/') ? '/' : pathname;
|
|
25
|
-
if (isPublicRoute && !isLoginPage) return { type: 'allow' };
|
|
26
|
-
if (circuitBreakerOpen) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen', reason: 'CircuitBreakerOpen', clearCookies: true }; }
|
|
27
|
-
if (!sessionPointer.exists) { if (isLoginPage) return { type: 'allow' }; if (pathname === '/') { const unauthUrl = process.env.UNAUTHENTICATED_REDIRECT_URL || '/account-auth/login'; return { type: 'redirect', location: unauthUrl, reason: 'unauthenticated_root_redirect' }; } return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: 'no_session_pointer' }; }
|
|
28
|
-
if (sessionStatus.exists === null || sessionStatus.forceInvalid === null) return { type: 'service_error', reason: 'redis_unavailable' };
|
|
29
|
-
if (sessionStatus.forceInvalid) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=force_invalidated`, reason: 'force_invalidated' }; }
|
|
30
|
-
if (!sessionStatus.exists) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=stale_session`, reason: 'stale_session' }; }
|
|
31
|
-
if (sessionStatus.requires2FA && !sessionStatus.twoFactorComplete) {
|
|
32
|
-
if (pathname === '/account-auth/verify-code') return { type: 'allow' };
|
|
33
|
-
if (isLoginPage) { const callbackUrl = searchParams.get('callbackUrl') || '/'; const safeCallbackUrl2 = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl; return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl2)}`, reason: '2fa_required_login_redirect' }; }
|
|
34
|
-
return { type: '
|
|
35
|
-
}
|
|
36
|
-
if (sessionPointer.expired) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&error=SessionExpired`, reason: 'token_expired' }; }
|
|
37
|
-
if (pathname === '/') return { type: 'redirect', location: '/', reason: 'authenticated_root_redirect' };
|
|
38
|
-
if (isLoginPage) { const callbackUrl = searchParams.get('callbackUrl') || '/'; const safeCallbackUrl = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl; return { type: 'redirect', location: safeCallbackUrl, reason: 'already_authenticated' }; }
|
|
39
|
-
return { type: 'allow' };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function isLoginPage(pathname: string): boolean { return pathname === '/account-auth/login'; }
|
|
43
|
-
export function getCallbackUrl(pathname: string, searchParams: URLSearchParams): string { if (pathname.startsWith('/account-auth/')) return '/'; const existingCallback = searchParams.get('callbackUrl'); if (existingCallback && !existingCallback.startsWith('/account-auth/')) return existingCallback; return pathname; }
|
|
1
|
+
/**
|
|
2
|
+
* Auth Decision Engine (lifted)
|
|
3
|
+
*/
|
|
4
|
+
import { NextRequest } from 'next/server';
|
|
5
|
+
|
|
6
|
+
export interface AuthContext {
|
|
7
|
+
pathname: string;
|
|
8
|
+
isPublicRoute: boolean;
|
|
9
|
+
isLoginPage: boolean;
|
|
10
|
+
searchParams: URLSearchParams;
|
|
11
|
+
sessionPointer: { exists: boolean; sessionToken?: string; expired?: boolean; hasRefreshToken?: boolean; };
|
|
12
|
+
sessionStatus: { exists: boolean | null; forceInvalid: boolean | null; requires2FA: boolean; twoFactorComplete: boolean; };
|
|
13
|
+
circuitBreakerOpen: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AuthAction =
|
|
17
|
+
| { type: 'allow' }
|
|
18
|
+
| { type: 'redirect'; location: string; reason: string; clearCookies?: boolean }
|
|
19
|
+
| { type: 'service_error'; reason: string };
|
|
20
|
+
|
|
21
|
+
export function makeAuthDecision(context: AuthContext): AuthAction {
|
|
22
|
+
const { pathname, isPublicRoute, isLoginPage, searchParams, sessionPointer, sessionStatus, circuitBreakerOpen } = context;
|
|
23
|
+
// SAFEGUARD: Never use auth pages as callback URLs to prevent redirect loops
|
|
24
|
+
const safeCallbackUrl = pathname.startsWith('/account-auth/') ? '/' : pathname;
|
|
25
|
+
if (isPublicRoute && !isLoginPage) return { type: 'allow' };
|
|
26
|
+
if (circuitBreakerOpen) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen', reason: 'CircuitBreakerOpen', clearCookies: true }; }
|
|
27
|
+
if (!sessionPointer.exists) { if (isLoginPage) return { type: 'allow' }; if (pathname === '/') { const unauthUrl = process.env.UNAUTHENTICATED_REDIRECT_URL || '/account-auth/login'; return { type: 'redirect', location: unauthUrl, reason: 'unauthenticated_root_redirect' }; } return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: 'no_session_pointer' }; }
|
|
28
|
+
if (sessionStatus.exists === null || sessionStatus.forceInvalid === null) return { type: 'service_error', reason: 'redis_unavailable' };
|
|
29
|
+
if (sessionStatus.forceInvalid) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=force_invalidated`, reason: 'force_invalidated' }; }
|
|
30
|
+
if (!sessionStatus.exists) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=stale_session`, reason: 'stale_session' }; }
|
|
31
|
+
if (sessionStatus.requires2FA && !sessionStatus.twoFactorComplete) {
|
|
32
|
+
if (pathname === '/account-auth/verify-code') return { type: 'allow' };
|
|
33
|
+
if (isLoginPage) { const callbackUrl = searchParams.get('callbackUrl') || '/'; const safeCallbackUrl2 = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl; return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl2)}`, reason: '2fa_required_login_redirect' }; }
|
|
34
|
+
return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: '2fa_required' };
|
|
35
|
+
}
|
|
36
|
+
if (sessionPointer.expired) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&error=SessionExpired`, reason: 'token_expired' }; }
|
|
37
|
+
if (pathname === '/') return { type: 'redirect', location: '/', reason: 'authenticated_root_redirect' };
|
|
38
|
+
if (isLoginPage) { const callbackUrl = searchParams.get('callbackUrl') || '/'; const safeCallbackUrl = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl; return { type: 'redirect', location: safeCallbackUrl, reason: 'already_authenticated' }; }
|
|
39
|
+
return { type: 'allow' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isLoginPage(pathname: string): boolean { return pathname === '/account-auth/login'; }
|
|
43
|
+
export function getCallbackUrl(pathname: string, searchParams: URLSearchParams): string { if (pathname.startsWith('/account-auth/')) return '/'; const existingCallback = searchParams.get('callbackUrl'); if (existingCallback && !existingCallback.startsWith('/account-auth/')) return existingCallback; return pathname; }
|