@payez/next-mvp 4.4.1 → 4.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/auth-handler.js +13 -4
- package/dist/api-handlers/mfa/totp-verify.d.ts +3 -1
- package/dist/api-handlers/mfa/totp-verify.js +22 -3
- package/dist/api-handlers/session/refresh-viability.js +2 -2
- package/dist/api-handlers/session/viability.js +1 -1
- package/dist/auth/auth-decision.js +27 -8
- package/dist/auth/better-auth.js +123 -1
- package/dist/auth/safe-callback.d.ts +92 -0
- package/dist/auth/safe-callback.js +183 -0
- package/dist/hooks/useProfile.d.ts +0 -1
- package/dist/lib/acr.d.ts +64 -0
- package/dist/lib/acr.js +109 -0
- package/dist/lib/session-store.js +56 -7
- package/dist/middleware/create-middleware.js +45 -13
- package/dist/pages/profile/EnhancedProfilePage.js +38 -13
- package/dist/pages/verify-code/page.js +26 -1
- package/dist/routes/auth/viability.d.ts +2 -2
- package/dist/routes/auth/viability.js +2 -2
- package/dist/server/auth-guard.js +26 -1
- package/package.json +2 -1
- package/scripts/module-reach.mjs +280 -0
- package/src/api/auth-handler.ts +19 -4
- package/src/api-handlers/mfa/totp-verify.ts +24 -3
- package/src/api-handlers/session/refresh-viability.ts +2 -2
- package/src/api-handlers/session/viability.ts +1 -1
- package/src/auth/auth-decision.ts +28 -8
- package/src/auth/better-auth.ts +128 -1
- package/src/auth/safe-callback.ts +185 -0
- package/src/hooks/useProfile.ts +8 -1
- package/src/lib/acr.ts +109 -0
- package/src/lib/session-store.ts +66 -7
- package/src/middleware/create-middleware.ts +45 -13
- package/src/pages/profile/EnhancedProfilePage.tsx +43 -14
- package/src/pages/verify-code/page.tsx +26 -1
- package/src/routes/auth/viability.ts +2 -2
- package/src/server/auth-guard.ts +27 -1
- package/dist/middleware/auth-decision.d.ts +0 -33
- package/dist/middleware/auth-decision.js +0 -65
- package/src/middleware/auth-decision.ts +0 -43
package/src/server/auth-guard.ts
CHANGED
|
@@ -127,7 +127,33 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
if (requires2FA) {
|
|
130
|
-
|
|
130
|
+
// 369858: the legacy `?? (sessionData as any).twoFactorComplete` carry is REMOVED.
|
|
131
|
+
// This is THE access gate - the only one of the 8 carry sites that grants access
|
|
132
|
+
// rather than reporting status - so it reads exactly one source of truth.
|
|
133
|
+
//
|
|
134
|
+
// Removing it is fail-closed. No current writer emits the legacy field alone:
|
|
135
|
+
// session-store.ts:444-445 writes both as a pair from a server-derived stepped-up
|
|
136
|
+
// token, auth/refresh.ts:376 writes it as a MIRROR of mfaVerified, and
|
|
137
|
+
// routes/auth/session.ts:149-157 refuses both on PRESENCE, so a caller cannot
|
|
138
|
+
// assert either. The carry therefore only ever served sessions minted before
|
|
139
|
+
// `mfaVerified` existed, and for those the correct behaviour is re-verify, not
|
|
140
|
+
// grandfather. Cost is one forced re-challenge for a stale legacy session.
|
|
141
|
+
//
|
|
142
|
+
// Severity note, so nobody re-inflates this later: the operator was `??`
|
|
143
|
+
// (nullish), NOT `||`. `false ?? x` is `false`, so an explicitly DENIED session
|
|
144
|
+
// could never be upgraded by a stale flag. This was legacy grandfathering, not a
|
|
145
|
+
// live 2FA bypass. Had it been `||`, it would have been the bypass.
|
|
146
|
+
const mfaVerified = sessionData.mfaVerified ?? false;
|
|
147
|
+
|
|
148
|
+
// 339563 / 369858 §3: THIS IS STILL A FAIL-OPEN AND IS DELIBERATELY LEFT VISIBLE.
|
|
149
|
+
// An absent mfaExpiresAt collapses to 0, so `mfaExpired` is false and the MFA
|
|
150
|
+
// verdict NEVER EXPIRES. That needs no legacy-session hypothesis: it fires on a
|
|
151
|
+
// current `mfaVerified: true` session whose token carried no `mfa_expires` claim -
|
|
152
|
+
// the passwordless/primary-credential lane, named on the write path in the
|
|
153
|
+
// source's own voice at session-store.ts:464-467 ("it IS the case the consumers
|
|
154
|
+
// currently fail OPEN on"). Removing the carry above does NOT touch this.
|
|
155
|
+
// Tracked separately by BAPert; not fixed here because failing closed on absent
|
|
156
|
+
// expiry would deny the passwordless lane outright and that is a product call.
|
|
131
157
|
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
132
158
|
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
133
159
|
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export interface AuthContext {
|
|
2
|
-
pathname: string;
|
|
3
|
-
isPublicRoute: boolean;
|
|
4
|
-
isLoginPage: boolean;
|
|
5
|
-
searchParams: URLSearchParams;
|
|
6
|
-
sessionPointer: {
|
|
7
|
-
exists: boolean;
|
|
8
|
-
sessionToken?: string;
|
|
9
|
-
expired?: boolean;
|
|
10
|
-
hasRefreshToken?: boolean;
|
|
11
|
-
};
|
|
12
|
-
sessionStatus: {
|
|
13
|
-
exists: boolean | null;
|
|
14
|
-
forceInvalid: boolean | null;
|
|
15
|
-
requires2FA: boolean;
|
|
16
|
-
twoFactorComplete: boolean;
|
|
17
|
-
};
|
|
18
|
-
circuitBreakerOpen: boolean;
|
|
19
|
-
}
|
|
20
|
-
export type AuthAction = {
|
|
21
|
-
type: 'allow';
|
|
22
|
-
} | {
|
|
23
|
-
type: 'redirect';
|
|
24
|
-
location: string;
|
|
25
|
-
reason: string;
|
|
26
|
-
clearCookies?: boolean;
|
|
27
|
-
} | {
|
|
28
|
-
type: 'service_error';
|
|
29
|
-
reason: string;
|
|
30
|
-
};
|
|
31
|
-
export declare function makeAuthDecision(context: AuthContext): AuthAction;
|
|
32
|
-
export declare function isLoginPage(pathname: string): boolean;
|
|
33
|
-
export declare function getCallbackUrl(pathname: string, searchParams: URLSearchParams): string;
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.makeAuthDecision = makeAuthDecision;
|
|
4
|
-
exports.isLoginPage = isLoginPage;
|
|
5
|
-
exports.getCallbackUrl = getCallbackUrl;
|
|
6
|
-
function makeAuthDecision(context) {
|
|
7
|
-
const { pathname, isPublicRoute, isLoginPage, searchParams, sessionPointer, sessionStatus, circuitBreakerOpen } = context;
|
|
8
|
-
// SAFEGUARD: Never use auth pages as callback URLs to prevent redirect loops
|
|
9
|
-
const safeCallbackUrl = pathname.startsWith('/account-auth/') ? '/' : pathname;
|
|
10
|
-
if (isPublicRoute && !isLoginPage)
|
|
11
|
-
return { type: 'allow' };
|
|
12
|
-
if (circuitBreakerOpen) {
|
|
13
|
-
if (isLoginPage)
|
|
14
|
-
return { type: 'allow' };
|
|
15
|
-
return { type: 'redirect', location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen', reason: 'CircuitBreakerOpen', clearCookies: true };
|
|
16
|
-
}
|
|
17
|
-
if (!sessionPointer.exists) {
|
|
18
|
-
if (isLoginPage)
|
|
19
|
-
return { type: 'allow' };
|
|
20
|
-
if (pathname === '/') {
|
|
21
|
-
const unauthUrl = process.env.UNAUTHENTICATED_REDIRECT_URL || '/account-auth/login';
|
|
22
|
-
return { type: 'redirect', location: unauthUrl, reason: 'unauthenticated_root_redirect' };
|
|
23
|
-
}
|
|
24
|
-
return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: 'no_session_pointer' };
|
|
25
|
-
}
|
|
26
|
-
if (sessionStatus.exists === null || sessionStatus.forceInvalid === null)
|
|
27
|
-
return { type: 'service_error', reason: 'redis_unavailable' };
|
|
28
|
-
if (sessionStatus.forceInvalid) {
|
|
29
|
-
if (isLoginPage)
|
|
30
|
-
return { type: 'allow' };
|
|
31
|
-
return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=force_invalidated`, reason: 'force_invalidated' };
|
|
32
|
-
}
|
|
33
|
-
if (!sessionStatus.exists) {
|
|
34
|
-
if (isLoginPage)
|
|
35
|
-
return { type: 'allow' };
|
|
36
|
-
return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=stale_session`, reason: 'stale_session' };
|
|
37
|
-
}
|
|
38
|
-
if (sessionStatus.requires2FA && !sessionStatus.twoFactorComplete) {
|
|
39
|
-
if (pathname === '/account-auth/verify-code')
|
|
40
|
-
return { type: 'allow' };
|
|
41
|
-
if (isLoginPage) {
|
|
42
|
-
const callbackUrl = searchParams.get('callbackUrl') || '/';
|
|
43
|
-
const safeCallbackUrl2 = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl;
|
|
44
|
-
return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl2)}`, reason: '2fa_required_login_redirect' };
|
|
45
|
-
}
|
|
46
|
-
return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: '2fa_required' };
|
|
47
|
-
}
|
|
48
|
-
if (sessionPointer.expired) {
|
|
49
|
-
if (isLoginPage)
|
|
50
|
-
return { type: 'allow' };
|
|
51
|
-
return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&error=SessionExpired`, reason: 'token_expired' };
|
|
52
|
-
}
|
|
53
|
-
if (pathname === '/')
|
|
54
|
-
return { type: 'redirect', location: '/', reason: 'authenticated_root_redirect' };
|
|
55
|
-
if (isLoginPage) {
|
|
56
|
-
const callbackUrl = searchParams.get('callbackUrl') || '/';
|
|
57
|
-
const safeCallbackUrl = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl;
|
|
58
|
-
return { type: 'redirect', location: safeCallbackUrl, reason: 'already_authenticated' };
|
|
59
|
-
}
|
|
60
|
-
return { type: 'allow' };
|
|
61
|
-
}
|
|
62
|
-
function isLoginPage(pathname) { return pathname === '/account-auth/login'; }
|
|
63
|
-
function getCallbackUrl(pathname, searchParams) { if (pathname.startsWith('/account-auth/'))
|
|
64
|
-
return '/'; const existingCallback = searchParams.get('callbackUrl'); if (existingCallback && !existingCallback.startsWith('/account-auth/'))
|
|
65
|
-
return existingCallback; return pathname; }
|
|
@@ -1,43 +0,0 @@
|
|
|
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; }
|