@payez/next-mvp 4.1.20 → 4.4.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/POST-INSTALL.md +408 -0
- package/README.md +7 -0
- package/dist/api-handlers/account/recovery/initiate.js +35 -2
- package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/send-code.js +35 -2
- package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/verify-code.js +35 -2
- package/dist/api-handlers/account/reset-password.js +35 -2
- package/dist/api-handlers/account/validate-password.js +21 -3
- package/dist/api-handlers/admin/analytics.js +2 -1
- package/dist/api-handlers/admin/audit.js +2 -1
- package/dist/api-handlers/admin/sessions.js +2 -1
- package/dist/api-handlers/admin/stats.js +2 -1
- package/dist/api-handlers/admin/users.js +2 -1
- package/dist/api-handlers/admin/vibe-data.js +2 -1
- package/dist/api-handlers/auth/jwks.js +10 -0
- package/dist/api-handlers/auth/login.js +5 -2
- package/dist/api-handlers/auth/passkey-signin.js +16 -1
- package/dist/api-handlers/auth/refresh.d.ts +23 -3
- package/dist/api-handlers/auth/refresh.js +107 -16
- package/dist/api-handlers/auth/signout.js +3 -2
- package/dist/api-handlers/auth/verify-code.js +5 -2
- package/dist/api-handlers/session/viability.js +27 -1
- package/dist/api-handlers/test/force-expire.d.ts +29 -15
- package/dist/api-handlers/test/force-expire.js +109 -14
- package/dist/auth/better-auth.d.ts +263 -6
- package/dist/auth/better-auth.js +437 -59
- package/dist/auth/utils/idp-client.js +1 -1
- package/dist/client/AuthContext.js +49 -17
- package/dist/components/account/UserAvatarMenu.js +12 -0
- package/dist/components/auth/ModeAwareLoginPage.js +10 -8
- package/dist/components/auth/ModeAwareSignupPage.js +5 -2
- package/dist/components/auth/TraditionalAuthSection.js +5 -1
- package/dist/hooks/useAuthSettings.js +14 -2
- package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
- package/dist/hooks/usePublicAuthSettings.js +28 -14
- package/dist/lib/client-key.d.ts +61 -0
- package/dist/lib/client-key.js +72 -0
- package/dist/lib/demo-mode.js +2 -1
- package/dist/lib/ensure-fresh-access-token.js +12 -1
- package/dist/lib/idp-client-config.d.ts +25 -2
- package/dist/lib/idp-client-config.js +381 -36
- package/dist/lib/idp-fetch.d.ts +2 -0
- package/dist/lib/idp-fetch.js +31 -1
- package/dist/lib/idp-url-guard.d.ts +29 -0
- package/dist/lib/idp-url-guard.js +38 -0
- package/dist/lib/redis.js +77 -5
- package/dist/lib/session-cookie-shape.d.ts +69 -0
- package/dist/lib/session-cookie-shape.js +126 -0
- package/dist/lib/session-store.d.ts +35 -2
- package/dist/lib/session-store.js +135 -4
- package/dist/lib/single-flight.d.ts +35 -0
- package/dist/lib/single-flight.js +30 -0
- package/dist/lib/token-lifecycle.js +12 -0
- package/dist/middleware/create-middleware.js +12 -0
- package/dist/models/SessionModel.d.ts +41 -0
- package/dist/models/SessionModel.js +9 -0
- package/dist/pages/admin-login/page.d.ts +1 -1
- package/dist/pages/admin-login/page.js +5 -41
- package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
- package/dist/pages/security/EnhancedSecurityPage.js +42 -2
- package/dist/pages/security/index.d.ts +1 -0
- package/dist/pages/security/index.js +3 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
- package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
- package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
- package/dist/pages/test-env/JwtInspectPage.js +166 -7
- package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
- package/dist/pages/test-env/RefreshTokenPage.js +22 -6
- package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
- package/dist/pages/test-env/TestEnvPage.js +16 -4
- package/dist/pages/test-env/guarded.d.ts +4 -0
- package/dist/pages/test-env/guarded.js +86 -0
- package/dist/pages/test-env/index.d.ts +50 -7
- package/dist/pages/test-env/index.js +58 -12
- package/dist/routes/auth/logout.js +3 -2
- package/dist/routes/auth/refresh.js +27 -1
- package/dist/routes/auth/session.d.ts +12 -3
- package/dist/routes/auth/session.js +92 -9
- package/dist/routes/auth/settings.d.ts +34 -4
- package/dist/routes/auth/settings.js +87 -33
- package/dist/routes/auth/viability.d.ts +1 -1
- package/dist/routes/auth/viability.js +122 -11
- package/dist/server/auth-guard.js +22 -15
- package/dist/server/auth.d.ts +170 -4
- package/dist/server/auth.js +42 -75
- package/dist/server/decode-session.js +84 -10
- package/dist/types/auth.d.ts +18 -0
- package/package.json +969 -958
- package/scripts/check-internal-url-usage.sh +73 -73
- package/scripts/check-publish-health.mjs +120 -0
- package/scripts/dev-broker.ps1 +71 -6
- package/scripts/print-client-config.mjs +162 -0
- package/scripts/server-only-stub.cjs +4 -0
- package/src/api-handlers/account/recovery/initiate.ts +35 -2
- package/src/api-handlers/account/recovery/send-code.ts +36 -2
- package/src/api-handlers/account/recovery/verify-code.ts +36 -2
- package/src/api-handlers/account/reset-password.ts +35 -2
- package/src/api-handlers/account/validate-password.ts +22 -3
- package/src/api-handlers/admin/analytics.ts +2 -1
- package/src/api-handlers/admin/audit.ts +2 -1
- package/src/api-handlers/admin/sessions.ts +2 -1
- package/src/api-handlers/admin/stats.ts +2 -1
- package/src/api-handlers/admin/users.ts +2 -1
- package/src/api-handlers/admin/vibe-data.ts +2 -1
- package/src/api-handlers/auth/jwks.ts +10 -0
- package/src/api-handlers/auth/login.ts +5 -2
- package/src/api-handlers/auth/passkey-signin.ts +112 -98
- package/src/api-handlers/auth/refresh.ts +112 -15
- package/src/api-handlers/auth/signout.ts +4 -3
- package/src/api-handlers/auth/verify-code.ts +5 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
- package/src/api-handlers/mfa/backup-codes.ts +41 -41
- package/src/api-handlers/mfa/methods.ts +46 -46
- package/src/api-handlers/mfa/totp-confirm.ts +42 -42
- package/src/api-handlers/mfa/totp-enroll.ts +42 -42
- package/src/api-handlers/passkeys/list.ts +31 -31
- package/src/api-handlers/passkeys/manage.ts +54 -54
- package/src/api-handlers/passkeys/register-options.ts +42 -42
- package/src/api-handlers/passkeys/register-verify.ts +41 -41
- package/src/api-handlers/session/viability.ts +29 -1
- package/src/api-handlers/test/force-expire.ts +118 -13
- package/src/auth/better-auth.ts +486 -51
- package/src/auth/utils/idp-client.ts +1 -1
- package/src/client/AuthContext.tsx +55 -19
- package/src/components/account/UserAvatarMenu.tsx +11 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
- package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
- package/src/components/auth/TraditionalAuthSection.tsx +5 -1
- package/src/hooks/useAuthSettings.ts +14 -2
- package/src/hooks/usePublicAuthSettings.ts +38 -16
- package/src/lib/client-key.ts +88 -0
- package/src/lib/demo-mode.ts +2 -1
- package/src/lib/ensure-fresh-access-token.ts +12 -1
- package/src/lib/idp-client-config.ts +399 -36
- package/src/lib/idp-fetch.ts +36 -1
- package/src/lib/idp-url-guard.ts +37 -0
- package/src/lib/redis.ts +84 -6
- package/src/lib/session-cookie-shape.ts +184 -0
- package/src/lib/session-store.ts +152 -6
- package/src/lib/single-flight.ts +64 -0
- package/src/lib/token-lifecycle.ts +12 -0
- package/src/middleware/create-middleware.ts +11 -0
- package/src/models/SessionModel.ts +49 -0
- package/src/pages/admin-login/page.tsx +17 -127
- package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
- package/src/pages/security/index.ts +3 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
- package/src/pages/test-env/JwtInspectPage.tsx +235 -20
- package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
- package/src/pages/test-env/TestEnvPage.tsx +16 -4
- package/src/pages/test-env/guarded.tsx +81 -0
- package/src/pages/test-env/index.ts +59 -7
- package/src/routes/auth/logout.ts +4 -3
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +30 -1
- package/src/routes/auth/session.ts +98 -9
- package/src/routes/auth/settings.ts +144 -42
- package/src/routes/auth/viability.ts +129 -11
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/server/auth-guard.ts +24 -17
- package/src/server/auth.ts +54 -74
- package/src/server/decode-session.ts +95 -11
- package/src/templates/instrumentation.ts +21 -2
- package/src/types/auth.ts +20 -0
- package/dist/lib/auth-secret.d.ts +0 -17
- package/dist/lib/auth-secret.js +0 -116
- package/src/lib/auth-secret.ts +0 -137
|
@@ -2,20 +2,53 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.POST = POST;
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
|
+
const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
6
|
+
const client_key_1 = require("../../lib/client-key");
|
|
5
7
|
function getIdpUrl() {
|
|
6
8
|
const url = process.env.IDP_URL;
|
|
7
9
|
if (!url)
|
|
8
10
|
throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
|
|
9
11
|
return url;
|
|
10
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
|
|
15
|
+
*
|
|
16
|
+
* These four proxies populated NONE of PayEzClientIdResolver's six slots, so
|
|
17
|
+
* RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
|
|
18
|
+
* recovery token was ever minted. That is why S12 failed at step 1.
|
|
19
|
+
*
|
|
20
|
+
* NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
|
|
21
|
+
* That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
|
|
22
|
+
* numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
|
|
23
|
+
* while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
|
|
24
|
+
* added four more sites to the 268410 family and made a stale id answer for
|
|
25
|
+
* the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
|
|
26
|
+
* the numeric fallback logged, is the same resolver the other nine sites use.
|
|
27
|
+
*
|
|
28
|
+
* SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
|
|
29
|
+
* These are unauthenticated recovery endpoints, so a browser-supplied
|
|
30
|
+
* client_id would be a tenant-targeting lever. The caller does not get to
|
|
31
|
+
* choose which tenant their password reset is addressed to.
|
|
32
|
+
*/
|
|
33
|
+
async function recoveryClientKey() {
|
|
34
|
+
try {
|
|
35
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
36
|
+
return (0, client_key_1.resolveClientKey)(config, 'account/reset-password').key;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error('[account/reset-password] could not resolve IDP client config for client_id:', e);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
11
43
|
async function POST(req) {
|
|
12
44
|
const IDP_URL = getIdpUrl();
|
|
13
45
|
try {
|
|
14
46
|
const payload = await req.json();
|
|
47
|
+
const clientKey = await recoveryClientKey();
|
|
15
48
|
const upstream = await fetch(`${IDP_URL}/api/Account/reset-password`, {
|
|
16
49
|
method: 'POST',
|
|
17
|
-
headers: { 'Content-Type': 'application/json' },
|
|
18
|
-
body: JSON.stringify(payload),
|
|
50
|
+
headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
|
|
51
|
+
body: JSON.stringify({ ...payload, ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
19
52
|
});
|
|
20
53
|
const data = await upstream.json().catch(() => ({}));
|
|
21
54
|
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
@@ -20,7 +20,7 @@ async function POST(req) {
|
|
|
20
20
|
}
|
|
21
21
|
// Get IDP base URL and client ID from environment
|
|
22
22
|
const idpBaseUrl = process.env.IDP_URL;
|
|
23
|
-
const clientId = process.env.CLIENT_ID
|
|
23
|
+
const clientId = process.env.CLIENT_ID;
|
|
24
24
|
if (!idpBaseUrl) {
|
|
25
25
|
console.error('[VALIDATE_PASSWORD] IDP_URL not configured');
|
|
26
26
|
return server_1.NextResponse.json({
|
|
@@ -61,8 +61,26 @@ async function POST(req) {
|
|
|
61
61
|
headers: { 'Cache-Control': 'no-store' },
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* 283211 - UNWRAP THE IDP ENVELOPE. Approved by BAPert 2026-08-30.
|
|
66
|
+
*
|
|
67
|
+
* The IDP answers this endpoint with an ENVELOPE - { success, data: { is_valid, score,
|
|
68
|
+
* failed_requirements, policy } } - and this line used to hand that straight back. The
|
|
69
|
+
* `as ValidatePasswordResponse` is a COMPILE-TIME ASSERTION AND PERFORMS NO RUNTIME
|
|
70
|
+
* TRANSFORMATION, so nothing ever reshaped it and the type told us it was fine.
|
|
71
|
+
*
|
|
72
|
+
* The consumer is usePasswordValidation, which reads `data.is_valid` at the top level and
|
|
73
|
+
* calls setIsValid with it. Against an envelope that is `undefined`, so isValid never
|
|
74
|
+
* became true; and EVERY error path in this file returns a FLAT { is_valid: false }. So the
|
|
75
|
+
* success shape was unreadable and the failure shape was readable-and-false: setIsValid(true)
|
|
76
|
+
* was UNREACHABLE, and the Change Password button's `canSubmit` could never enable.
|
|
77
|
+
*
|
|
78
|
+
* `responseData?.data ?? responseData` is defensive in both directions BY DESIGN: it unwraps
|
|
79
|
+
* the envelope we get today and passes a flat body through unchanged, so it does not break
|
|
80
|
+
* if the IDP is later made consistent with this file's own declared response type.
|
|
81
|
+
*/
|
|
82
|
+
const unwrapped = responseData?.data ?? responseData;
|
|
83
|
+
return server_1.NextResponse.json(unwrapped, {
|
|
66
84
|
status: 200,
|
|
67
85
|
headers: { 'Cache-Control': 'no-store' },
|
|
68
86
|
});
|
|
@@ -46,6 +46,7 @@ exports.createAnalyticsHandler = createAnalyticsHandler;
|
|
|
46
46
|
const server_1 = require("next/server");
|
|
47
47
|
const auth_1 = require("../../server/auth");
|
|
48
48
|
const startup_init_1 = require("../../lib/startup-init");
|
|
49
|
+
const client_key_1 = require("../../lib/client-key");
|
|
49
50
|
const roles_1 = require("../../lib/roles");
|
|
50
51
|
async function checkAdminRole(request) {
|
|
51
52
|
const session = await (0, auth_1.getSession)(request);
|
|
@@ -82,7 +83,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
82
83
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
83
84
|
// Get the client slug from startup config for multi-client admin support
|
|
84
85
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
85
|
-
const idpClientId = idpConfig
|
|
86
|
+
const idpClientId = (0, client_key_1.resolveClientKey)(idpConfig, 'admin/analytics').key;
|
|
86
87
|
try {
|
|
87
88
|
const res = await fetch(proxyUrl, {
|
|
88
89
|
method: 'POST',
|
|
@@ -45,6 +45,7 @@ exports.createAuditHandler = createAuditHandler;
|
|
|
45
45
|
const server_1 = require("next/server");
|
|
46
46
|
const auth_1 = require("../../server/auth");
|
|
47
47
|
const startup_init_1 = require("../../lib/startup-init");
|
|
48
|
+
const client_key_1 = require("../../lib/client-key");
|
|
48
49
|
const roles_1 = require("../../lib/roles");
|
|
49
50
|
async function checkAdminRole(request) {
|
|
50
51
|
const session = await (0, auth_1.getSession)(request);
|
|
@@ -81,7 +82,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
81
82
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
82
83
|
// Get the client slug from startup config for multi-client admin support
|
|
83
84
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
84
|
-
const idpClientId = idpConfig
|
|
85
|
+
const idpClientId = (0, client_key_1.resolveClientKey)(idpConfig, 'admin/audit').key;
|
|
85
86
|
try {
|
|
86
87
|
const res = await fetch(proxyUrl, {
|
|
87
88
|
method: 'POST',
|
|
@@ -46,6 +46,7 @@ exports.createSessionsHandler = createSessionsHandler;
|
|
|
46
46
|
const server_1 = require("next/server");
|
|
47
47
|
const auth_1 = require("../../server/auth");
|
|
48
48
|
const startup_init_1 = require("../../lib/startup-init");
|
|
49
|
+
const client_key_1 = require("../../lib/client-key");
|
|
49
50
|
const roles_1 = require("../../lib/roles");
|
|
50
51
|
/**
|
|
51
52
|
* Check if the current user has admin role
|
|
@@ -88,7 +89,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
88
89
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
89
90
|
// Get the client slug from startup config for multi-client admin support
|
|
90
91
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
91
|
-
const idpClientId = idpConfig
|
|
92
|
+
const idpClientId = (0, client_key_1.resolveClientKey)(idpConfig, 'admin/sessions').key;
|
|
92
93
|
try {
|
|
93
94
|
const res = await fetch(proxyUrl, {
|
|
94
95
|
method: 'POST',
|
|
@@ -46,6 +46,7 @@ exports.createStatsHandler = createStatsHandler;
|
|
|
46
46
|
const server_1 = require("next/server");
|
|
47
47
|
const auth_1 = require("../../server/auth");
|
|
48
48
|
const startup_init_1 = require("../../lib/startup-init");
|
|
49
|
+
const client_key_1 = require("../../lib/client-key");
|
|
49
50
|
const redis_1 = require("../../lib/redis");
|
|
50
51
|
const roles_1 = require("../../lib/roles");
|
|
51
52
|
async function checkAdminRole(request) {
|
|
@@ -82,7 +83,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
82
83
|
.digest('base64');
|
|
83
84
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
84
85
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
85
|
-
const idpClientId = idpConfig
|
|
86
|
+
const idpClientId = (0, client_key_1.resolveClientKey)(idpConfig, 'admin/stats').key;
|
|
86
87
|
try {
|
|
87
88
|
const res = await fetch(proxyUrl, {
|
|
88
89
|
method: 'POST',
|
|
@@ -45,6 +45,7 @@ exports.createUsersHandler = createUsersHandler;
|
|
|
45
45
|
const server_1 = require("next/server");
|
|
46
46
|
const auth_1 = require("../../server/auth");
|
|
47
47
|
const startup_init_1 = require("../../lib/startup-init");
|
|
48
|
+
const client_key_1 = require("../../lib/client-key");
|
|
48
49
|
const roles_1 = require("../../lib/roles");
|
|
49
50
|
async function checkAdminRole(request) {
|
|
50
51
|
const session = await (0, auth_1.getSession)(request);
|
|
@@ -81,7 +82,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
81
82
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
82
83
|
// Get the client slug from startup config for multi-client admin support
|
|
83
84
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
84
|
-
const idpClientId = idpConfig
|
|
85
|
+
const idpClientId = (0, client_key_1.resolveClientKey)(idpConfig, 'admin/users').key;
|
|
85
86
|
try {
|
|
86
87
|
const res = await fetch(proxyUrl, {
|
|
87
88
|
method: 'POST',
|
|
@@ -52,6 +52,7 @@ exports.createQueryHandler = createQueryHandler;
|
|
|
52
52
|
const server_1 = require("next/server");
|
|
53
53
|
const auth_1 = require("../../server/auth");
|
|
54
54
|
const startup_init_1 = require("../../lib/startup-init");
|
|
55
|
+
const client_key_1 = require("../../lib/client-key");
|
|
55
56
|
const roles_1 = require("../../lib/roles");
|
|
56
57
|
/**
|
|
57
58
|
* Check if the current user has admin role
|
|
@@ -96,7 +97,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
96
97
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
97
98
|
// Get the client slug from startup config for multi-client admin support
|
|
98
99
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
99
|
-
const idpClientId = idpConfig
|
|
100
|
+
const idpClientId = (0, client_key_1.resolveClientKey)(idpConfig, 'admin/vibe-data').key;
|
|
100
101
|
try {
|
|
101
102
|
const res = await fetch(proxyUrl, {
|
|
102
103
|
method: 'POST',
|
|
@@ -4,9 +4,12 @@ exports.GET = GET;
|
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
5
|
const env_1 = require("../../config/env");
|
|
6
6
|
const logger_1 = require("../../config/logger");
|
|
7
|
+
const idp_url_guard_1 = require("../../lib/idp-url-guard");
|
|
7
8
|
async function GET(_req) {
|
|
8
9
|
try {
|
|
9
10
|
const url = `${env_1.ENV_CONFIG.IDP_URL}${env_1.API_ENDPOINTS.externalAuth.jwks}`;
|
|
11
|
+
// Direct fetch - the idpFetchJSON guard is not on this path.
|
|
12
|
+
(0, idp_url_guard_1.assertAbsoluteIdpUrl)(url);
|
|
10
13
|
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
11
14
|
const bodyText = await res.text();
|
|
12
15
|
try {
|
|
@@ -19,6 +22,13 @@ async function GET(_req) {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
catch (error) {
|
|
25
|
+
// This catch DISCARDED `error` entirely and answered a bare 502, so a config
|
|
26
|
+
// fault and a dead upstream were indistinguishable - and the guard's named
|
|
27
|
+
// message above would have been swallowed with them. The 502 body is left
|
|
28
|
+
// unchanged (it is a public response shape); the cause now reaches the log.
|
|
29
|
+
logger_1.logger.error('[AUTH_JWKS] Failed to fetch JWKS', {
|
|
30
|
+
message: error instanceof Error ? error.message : String(error),
|
|
31
|
+
});
|
|
22
32
|
return server_1.NextResponse.json({ error: 'Failed to fetch JWKS' }, { status: 502 });
|
|
23
33
|
}
|
|
24
34
|
}
|
|
@@ -87,10 +87,13 @@ function createLoginHandler(config) {
|
|
|
87
87
|
}
|
|
88
88
|
// Extract client headers for forwarding to IDP
|
|
89
89
|
const clientHeaders = {};
|
|
90
|
-
// Forward client IP if available
|
|
90
|
+
// Forward client IP if available.
|
|
91
|
+
// 269334 / R21: forward the FULL chain, do NOT collapse to [0] - see the same
|
|
92
|
+
// change in lib/idp-fetch.ts. [0] is the forgeable client-supplied entry and
|
|
93
|
+
// collapsing to it destroys the chain the IDP's trusted-hop logic reads.
|
|
91
94
|
const clientIp = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip');
|
|
92
95
|
if (clientIp) {
|
|
93
|
-
clientHeaders['X-Forwarded-For'] = clientIp
|
|
96
|
+
clientHeaders['X-Forwarded-For'] = clientIp;
|
|
94
97
|
}
|
|
95
98
|
// Forward user agent if available
|
|
96
99
|
const userAgent = req.headers.get('user-agent');
|
|
@@ -5,6 +5,8 @@ exports.verifyPOST = verifyPOST;
|
|
|
5
5
|
const server_1 = require("next/server");
|
|
6
6
|
const env_1 = require("../../config/env");
|
|
7
7
|
const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
8
|
+
const client_key_1 = require("../../lib/client-key");
|
|
9
|
+
const idp_url_guard_1 = require("../../lib/idp-url-guard");
|
|
8
10
|
/**
|
|
9
11
|
* Passkey SIGN-IN ceremony (ANONYMOUS — no session exists yet):
|
|
10
12
|
* optionsPOST -> POST /api/ExternalAuth/passwordless/passkey/options
|
|
@@ -21,7 +23,14 @@ const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
|
21
23
|
*/
|
|
22
24
|
async function clientKey() {
|
|
23
25
|
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
24
|
-
|
|
26
|
+
const { key } = (0, client_key_1.resolveClientKey)(config, 'auth/passkey-signin');
|
|
27
|
+
// Refuse rather than send an empty client_id. Card 268410: the graft's own
|
|
28
|
+
// copy of this path learned this the hard way — `|| ''` made an unset config
|
|
29
|
+
// look like a valid value and the IDP answered "client_id is required" in
|
|
30
|
+
// 7-9ms, which read as a request-shape bug for hours.
|
|
31
|
+
if (!key)
|
|
32
|
+
throw new Error('[passkey-signin] no IDP client slug or id resolved - refusing to send an empty client_id');
|
|
33
|
+
return key;
|
|
25
34
|
}
|
|
26
35
|
async function anonCall(req, path) {
|
|
27
36
|
let parsed = {};
|
|
@@ -32,6 +41,10 @@ async function anonCall(req, path) {
|
|
|
32
41
|
}
|
|
33
42
|
catch { }
|
|
34
43
|
const key = await clientKey();
|
|
44
|
+
// This route calls fetch directly, so it does NOT reach the guard inside
|
|
45
|
+
// idpFetchJSON. Same reasoning as the clientKey() throw above: name the missing
|
|
46
|
+
// config rather than sending a request built from it.
|
|
47
|
+
(0, idp_url_guard_1.assertAbsoluteIdpUrl)(`${env_1.ENV_CONFIG.IDP_URL}${path}`);
|
|
35
48
|
const res = await fetch(`${env_1.ENV_CONFIG.IDP_URL}${path}`, {
|
|
36
49
|
method: 'POST',
|
|
37
50
|
headers: { 'Content-Type': 'application/json', 'X-Client-Id': key },
|
|
@@ -86,6 +99,8 @@ async function verifyPOST(req) {
|
|
|
86
99
|
const nested = response && typeof response === 'object' && 'id' in response;
|
|
87
100
|
const assertion = nested ? response : { ...rest, response };
|
|
88
101
|
const key = await clientKey();
|
|
102
|
+
// Direct fetch - see the note in anonCall; the idpFetchJSON guard is not on this path.
|
|
103
|
+
(0, idp_url_guard_1.assertAbsoluteIdpUrl)(`${env_1.ENV_CONFIG.IDP_URL}/api/ExternalAuth/passwordless/passkey/verify`);
|
|
89
104
|
const res = await fetch(`${env_1.ENV_CONFIG.IDP_URL}/api/ExternalAuth/passwordless/passkey/verify`, {
|
|
90
105
|
method: 'POST',
|
|
91
106
|
headers: { 'Content-Type': 'application/json', 'X-Client-Id': key },
|
|
@@ -4,14 +4,34 @@
|
|
|
4
4
|
* ASK BEFORE EDITING - TESTED AND WORKING SYSTEM
|
|
5
5
|
*
|
|
6
6
|
* This handler manages the server-side refresh token cycle with:
|
|
7
|
-
* - Better Auth session extraction
|
|
8
|
-
* - Session token fallback for internal calls
|
|
7
|
+
* - Better Auth session extraction (the ONLY way a session is established here)
|
|
9
8
|
* - PayEz IDP refresh token exchange
|
|
10
9
|
* - Session state updates with new tokens
|
|
11
10
|
* - Proper error handling and logging
|
|
12
11
|
* - Single-use semantics enforcement
|
|
13
12
|
*
|
|
14
|
-
*
|
|
13
|
+
* ── 246007: THE `x-session-token` FALLBACK IS GONE. IT NOW HARD-DENIES. ────
|
|
14
|
+
* This list previously advertised an x-session-token fallback for internal
|
|
15
|
+
* callers as a current capability. That behaviour has been REMOVED: a request presenting `x-session-token` with
|
|
16
|
+
* no cookie-resolved session of its own is rejected 401 UNAUTHORIZED, because
|
|
17
|
+
* there is no session to establish ownership of the token it names. Accepting
|
|
18
|
+
* it let a caller refresh a session they did not own.
|
|
19
|
+
*
|
|
20
|
+
* The line is corrected here rather than merely deleted, and it is called out
|
|
21
|
+
* rather than quietly dropped, because THIS DOCSTRING SHIPS. It is compiled
|
|
22
|
+
* into dist and into the .d.ts, so it reaches every consumer's editor as hover
|
|
23
|
+
* text. QAPert-NightHawk caught that the fixed build was still describing the
|
|
24
|
+
* removed capability as a current feature (34973) — her verifier tests code
|
|
25
|
+
* properties and reads no comments, so a correct build could have published
|
|
26
|
+
* documentation asserting the opposite of what the code does.
|
|
27
|
+
*
|
|
28
|
+
* That matters twice over: an internal caller relying on the fallback gets a
|
|
29
|
+
* silent behavioural break while the docs promise it still works, and a
|
|
30
|
+
* published package that misdescribes its own auth behaviour is the same
|
|
31
|
+
* amplifier that put other defects onto three surfaces — shipped code carrying
|
|
32
|
+
* instructions for using it wrongly.
|
|
33
|
+
*
|
|
34
|
+
* @version 2.1
|
|
15
35
|
*/
|
|
16
36
|
import { NextRequest, NextResponse } from 'next/server';
|
|
17
37
|
interface RefreshConfig {
|
|
@@ -5,19 +5,40 @@
|
|
|
5
5
|
* ASK BEFORE EDITING - TESTED AND WORKING SYSTEM
|
|
6
6
|
*
|
|
7
7
|
* This handler manages the server-side refresh token cycle with:
|
|
8
|
-
* - Better Auth session extraction
|
|
9
|
-
* - Session token fallback for internal calls
|
|
8
|
+
* - Better Auth session extraction (the ONLY way a session is established here)
|
|
10
9
|
* - PayEz IDP refresh token exchange
|
|
11
10
|
* - Session state updates with new tokens
|
|
12
11
|
* - Proper error handling and logging
|
|
13
12
|
* - Single-use semantics enforcement
|
|
14
13
|
*
|
|
15
|
-
*
|
|
14
|
+
* ── 246007: THE `x-session-token` FALLBACK IS GONE. IT NOW HARD-DENIES. ────
|
|
15
|
+
* This list previously advertised an x-session-token fallback for internal
|
|
16
|
+
* callers as a current capability. That behaviour has been REMOVED: a request presenting `x-session-token` with
|
|
17
|
+
* no cookie-resolved session of its own is rejected 401 UNAUTHORIZED, because
|
|
18
|
+
* there is no session to establish ownership of the token it names. Accepting
|
|
19
|
+
* it let a caller refresh a session they did not own.
|
|
20
|
+
*
|
|
21
|
+
* The line is corrected here rather than merely deleted, and it is called out
|
|
22
|
+
* rather than quietly dropped, because THIS DOCSTRING SHIPS. It is compiled
|
|
23
|
+
* into dist and into the .d.ts, so it reaches every consumer's editor as hover
|
|
24
|
+
* text. QAPert-NightHawk caught that the fixed build was still describing the
|
|
25
|
+
* removed capability as a current feature (34973) — her verifier tests code
|
|
26
|
+
* properties and reads no comments, so a correct build could have published
|
|
27
|
+
* documentation asserting the opposite of what the code does.
|
|
28
|
+
*
|
|
29
|
+
* That matters twice over: an internal caller relying on the fallback gets a
|
|
30
|
+
* silent behavioural break while the docs promise it still works, and a
|
|
31
|
+
* published package that misdescribes its own auth behaviour is the same
|
|
32
|
+
* amplifier that put other defects onto three surfaces — shipped code carrying
|
|
33
|
+
* instructions for using it wrongly.
|
|
34
|
+
*
|
|
35
|
+
* @version 2.1
|
|
16
36
|
*/
|
|
17
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
38
|
exports.POST = void 0;
|
|
19
39
|
exports.createRefreshHandler = createRefreshHandler;
|
|
20
40
|
const server_1 = require("next/server");
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
21
42
|
const auth_1 = require("../../server/auth");
|
|
22
43
|
const session_store_1 = require("../../lib/session-store");
|
|
23
44
|
const token_expiry_1 = require("../../lib/token-expiry");
|
|
@@ -49,12 +70,50 @@ function createRefreshHandler(config) {
|
|
|
49
70
|
let sessionToken = (betterAuthSession?.session?.token);
|
|
50
71
|
let userId = betterAuthSession?.user?.id;
|
|
51
72
|
if (!sessionToken) {
|
|
52
|
-
//
|
|
73
|
+
// OWNERSHIP CHECK on the x-session-token fallback (246007). BAPert ruled
|
|
74
|
+
// 34708: ship it, accept the one-time re-login cost, do not gate on 240654.
|
|
75
|
+
//
|
|
76
|
+
// WHAT THIS CLOSED: the fallback took a session token from a CALLER-SUPPLIED
|
|
77
|
+
// header and acted on that session with no check that it belonged to the
|
|
78
|
+
// caller. /api/auth/* is in publicRoutes on the consuming surfaces, so an
|
|
79
|
+
// UNAUTHENTICATED caller who knew any session token could cause that session
|
|
80
|
+
// to be refreshed -- an identity assumption, and the precondition for the
|
|
81
|
+
// forced-logout chain via IDP reuse detection.
|
|
82
|
+
//
|
|
83
|
+
// WHY THE CHECK IS A HARD DENY RATHER THAN A COMPARISON, which is the part
|
|
84
|
+
// worth understanding before anyone "improves" it: this branch is reached
|
|
85
|
+
// ONLY when getBetterAuthSession() failed to resolve a session from the
|
|
86
|
+
// request's own cookie. The natural ownership check -- "the header may only
|
|
87
|
+
// name the session the caller's cookie already resolves to" -- therefore has
|
|
88
|
+
// nothing to compare against by construction. Any correct ownership test at
|
|
89
|
+
// this point evaluates to reject. So the check and outright removal coincide
|
|
90
|
+
// here; a more permissive version is not possible without moving the check
|
|
91
|
+
// somewhere it can actually see an authenticated identity.
|
|
92
|
+
//
|
|
93
|
+
// WHO THIS AFFECTS, and it is a deliberate accepted cost, not an oversight:
|
|
94
|
+
// internal callers (lib/internal-api internalRefresh, from api-handler and
|
|
95
|
+
// idp-fetch) forward the end user's cookie. Where that cookie resolves they
|
|
96
|
+
// never enter this branch and are unaffected. Where it does not -- sessions
|
|
97
|
+
// minted before the graft dual-write (8a5ac21) whose cookie carries only the
|
|
98
|
+
// __Secure- name, which the pre-240654 resolver could not read -- the refresh
|
|
99
|
+
// now fails and the user re-authenticates ONCE. Their next login writes both
|
|
100
|
+
// cookie names and they are permanently fine. That population cannot grow, does
|
|
101
|
+
// not age out (session TTL slides on every write), and does not self-heal on
|
|
102
|
+
// use (this handler sets no cookie) -- so the only ways out were to accept the
|
|
103
|
+
// re-login or leave the defect live. BAPert chose to accept it.
|
|
104
|
+
//
|
|
105
|
+
// If server-to-server refresh must work WITHOUT an end-user cookie, that needs
|
|
106
|
+
// real service authentication -- a shared secret, mTLS, or a network policy --
|
|
107
|
+
// never a header any client can set.
|
|
53
108
|
const headerSessionToken = req.headers.get('x-session-token');
|
|
54
109
|
if (headerSessionToken) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
110
|
+
console.warn('[AUTH_REFRESH] REJECTED x-session-token: no cookie-resolved session to own it. ' +
|
|
111
|
+
'A header-named session is not proof of ownership; the caller must present a ' +
|
|
112
|
+
'session cookie this server can resolve.');
|
|
113
|
+
return server_1.NextResponse.json({
|
|
114
|
+
error: 'Session ownership could not be established',
|
|
115
|
+
code: 'UNAUTHORIZED',
|
|
116
|
+
}, { status: 401 });
|
|
58
117
|
}
|
|
59
118
|
}
|
|
60
119
|
if (!sessionToken || !userId) {
|
|
@@ -102,19 +161,41 @@ function createRefreshHandler(config) {
|
|
|
102
161
|
console.log(`║ 2FA Complete: ${String(!!currentSession.mfaVerified).padEnd(10)} ║`);
|
|
103
162
|
console.log(`║ Auth Level (ACR): ${String(currentSession.authenticationLevel || '1').padEnd(10)} ║`);
|
|
104
163
|
console.log('╚══════════════════════════════════════════════════════════════════════════════╝');
|
|
105
|
-
//
|
|
106
|
-
|
|
164
|
+
// LOCK IDENTITY IS MINTED SERVER-SIDE. It must never come from a header.
|
|
165
|
+
//
|
|
166
|
+
// This was `req.headers.get('x-request-id') ?? \`refresh_${Date.now()}\``, and the
|
|
167
|
+
// value was used as the lock's `acquiredBy`. Below it, a branch let a caller
|
|
168
|
+
// PROCEED WITHOUT THE LOCK when the existing lock's acquiredBy equalled the id
|
|
169
|
+
// they had supplied. Since the caller chose that id, the branch was a lock
|
|
170
|
+
// bypass requiring no guesswork: send N concurrent requests all carrying the
|
|
171
|
+
// same x-request-id, and every one after the first skips the wait.
|
|
172
|
+
//
|
|
173
|
+
// That completed an attacker-controlled forced logout, because the serialisation
|
|
174
|
+
// is what prevents refresh-token REUSE, and the IDP treats reuse as compromise:
|
|
175
|
+
// IdentityRefreshTokenService:166 -> RevokeDescendantTokens ->
|
|
176
|
+
// IdentityRefreshTokenRepository:142 revokes every ACTIVE token for that USER
|
|
177
|
+
// issued after the reused one -- across all devices and all client apps. So a
|
|
178
|
+
// caller who knew one session token could log a user out everywhere by sending
|
|
179
|
+
// a few requests with a header value of their own choosing.
|
|
180
|
+
//
|
|
181
|
+
// Minting server-side removes the bypass by construction: no caller can produce
|
|
182
|
+
// a value that matches an in-flight lock, so the equality branch can never be
|
|
183
|
+
// true and is deleted rather than left inert -- an unreachable branch that
|
|
184
|
+
// trusts a header is a trap for whoever edits this next.
|
|
185
|
+
//
|
|
186
|
+
// The anti-deadlock intent that branch served is preserved by the wait path
|
|
187
|
+
// below, which is the correct handling anyway: a genuine duplicate waits for the
|
|
188
|
+
// in-flight refresh and returns its result instead of starting a second one.
|
|
189
|
+
// The header is still read, for TRACING ONLY, and never influences control flow.
|
|
190
|
+
const traceId = req.headers.get('x-request-id') ?? undefined;
|
|
191
|
+
const requestId = `refresh_${(0, crypto_1.randomUUID)()}`;
|
|
107
192
|
const lockAcquired = await (0, session_store_1.acquireRefreshLock)(sessionToken, requestId, 5000);
|
|
108
193
|
let weAcquiredLock = false;
|
|
109
194
|
let releaseLockVersion;
|
|
110
195
|
if (!lockAcquired.acquired) {
|
|
111
|
-
|
|
112
|
-
if (existingLock && existingLock.acquiredBy === requestId) {
|
|
113
|
-
console.info('[AUTH_REFRESH] Proceeding under existing caller-held lock', { requestId });
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
196
|
+
{
|
|
116
197
|
// Wait for the lock to release, then check if token is now fresh
|
|
117
|
-
console.info('[AUTH_REFRESH] Refresh in progress by another request, waiting for completion...', { requestId });
|
|
198
|
+
console.info('[AUTH_REFRESH] Refresh in progress by another request, waiting for completion...', { requestId, traceId });
|
|
118
199
|
const maxWaitMs = 5000;
|
|
119
200
|
const checkIntervalMs = 200;
|
|
120
201
|
const startWait = Date.now();
|
|
@@ -595,7 +676,17 @@ function createRefreshHandler(config) {
|
|
|
595
676
|
bearerKeyId: newBearerKeyId || currentSession.bearerKeyId,
|
|
596
677
|
authenticationMethods: amrClaims,
|
|
597
678
|
authenticationLevel: acrLevel,
|
|
598
|
-
|
|
679
|
+
// 170007 rule 2 (BAPert ruling on 121350): '||' is the wrong operator
|
|
680
|
+
// for a fact with an expiry. mfaVerified is derived from amrClaims,
|
|
681
|
+
// which is FRESH every exchange (decoded from the token just
|
|
682
|
+
// returned, falling back to the prior session's amr only on a
|
|
683
|
+
// genuine decode failure a few lines up — not on "this token
|
|
684
|
+
// doesn't currently carry mfa"). ORing in currentSession.mfaVerified
|
|
685
|
+
// made the flag MONOTONIC against a claim that DECAYS (mfa_time,
|
|
686
|
+
// 10 minutes): once true, it stayed true forever regardless of
|
|
687
|
+
// what the current token actually says, silently re-opening the
|
|
688
|
+
// 2FA bypass with no bug in the bridge. Re-derive only.
|
|
689
|
+
mfaVerified: amrClaims.includes('mfa'),
|
|
599
690
|
mfaCompletedAt: mfaTime,
|
|
600
691
|
mfaExpiresAt: mfaExpires,
|
|
601
692
|
mfaValidityHours: mfaValidityHours
|
|
@@ -129,10 +129,11 @@ function createSignoutHandler() {
|
|
|
129
129
|
console.warn('[SIGNOUT] Manual JWT decode failed to extract session token');
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
// Delete
|
|
132
|
+
// Delete the session from EVERY store (181922): canonical + BA secondary.
|
|
133
|
+
// Canonical-only deletion let the ba: record resurrect the session.
|
|
133
134
|
if (redisSessionToken) {
|
|
134
135
|
try {
|
|
135
|
-
await (0, session_store_1.
|
|
136
|
+
await (0, session_store_1.deleteSessionEverywhere)(redisSessionToken);
|
|
136
137
|
console.info('[SIGNOUT] Redis session cleanup successful', {
|
|
137
138
|
sessionToken: redisSessionToken.substring(0, 8) + '...'
|
|
138
139
|
});
|
|
@@ -57,8 +57,11 @@ function createVerifyCodeHandler() {
|
|
|
57
57
|
});
|
|
58
58
|
// Update tokens in Redis
|
|
59
59
|
await (0, session_store_1.updateTokens)(sessionToken, accessToken, refreshToken, accessTokenExpires, refreshTokenExpires);
|
|
60
|
-
// Mark 2FA as complete
|
|
61
|
-
|
|
60
|
+
// Mark 2FA as complete.
|
|
61
|
+
// 339563: pass the stepped-up access token so the session gets an
|
|
62
|
+
// mfaExpiresAt alongside mfaVerified. Without it the gate's `> 0` guard
|
|
63
|
+
// treats the missing expiry as "never expires".
|
|
64
|
+
await (0, session_store_1.mark2FAComplete)(sessionToken, accessToken);
|
|
62
65
|
console.info('[VERIFY-CODE] 2FA completion successful', {
|
|
63
66
|
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
64
67
|
userId: betterAuthSession?.user?.id
|
|
@@ -71,7 +71,33 @@ async function GET(req) {
|
|
|
71
71
|
// CRITICAL: OAuth sessions without refresh token always need 2FA completion
|
|
72
72
|
// to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
|
|
73
73
|
// when no refresh_token is present. This must override the client config.
|
|
74
|
-
|
|
74
|
+
//
|
|
75
|
+
// *** R807 (BAPert, card 362550) — SECOND OF THE TWO SITES THE SIGNAL IS FOR. ***
|
|
76
|
+
// The `=== false` below reads as strict but had stopped behaving strictly: every
|
|
77
|
+
// construction site collapses an ABSENT mfaVerified to `false` (`?? false`), so by
|
|
78
|
+
// the time it is read here "the callback said false" and "we never knew" are the
|
|
79
|
+
// same value. This override therefore fired on EVERY session, not on OAuth sessions
|
|
80
|
+
// that genuinely lack a refresh token.
|
|
81
|
+
//
|
|
82
|
+
// The comment at line ~122 of this file already warned that fixing only ONE of the
|
|
83
|
+
// two viability implementations leaves an app mounting the other one blind. That
|
|
84
|
+
// applies here verbatim: routes/auth/viability.ts carries the same guard, and both
|
|
85
|
+
// are publicly exported entry points, so both must gate on the signal or the defect
|
|
86
|
+
// survives one export over.
|
|
87
|
+
//
|
|
88
|
+
// mfaSignalPresent === true -> the producer really emitted a value, so the `false`
|
|
89
|
+
// is a MEASUREMENT. Override.
|
|
90
|
+
// absent / false -> LEGACY session (minted before the signal existed) or
|
|
91
|
+
// a path with no IDP tokens. NOT a signal. Do not
|
|
92
|
+
// force a re-challenge.
|
|
93
|
+
//
|
|
94
|
+
// *** CONFINED, BY RULING. *** This signal is permissive by construction — it can
|
|
95
|
+
// only ever SUPPRESS this override, never grant access. Anything that GRANTS must
|
|
96
|
+
// keep gating on `mfaVerified`, which stays fail-closed at all five sites. R807 is
|
|
97
|
+
// void if this signal reaches an auth decision.
|
|
98
|
+
if (sessionData.mfaSignalPresent === true &&
|
|
99
|
+
sessionData.mfaVerified === false &&
|
|
100
|
+
!sessionData.idpRefreshToken) {
|
|
75
101
|
requires2FA = true;
|
|
76
102
|
}
|
|
77
103
|
// CRITICAL: Check if MFA has expired (2FA TTL enforcement)
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* ⚠️ DESTRUCTIVE TEST-ONLY HANDLER — DO NOT MOUNT IN PRODUCTION. ⚠️
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* which will trigger a refresh on the next
|
|
5
|
+
* Force-expire access token for testing refresh flow. Sets the access token
|
|
6
|
+
* expiry to 2 minutes in the past, which will trigger a refresh on the next
|
|
7
|
+
* API call.
|
|
8
|
+
*
|
|
9
|
+
* THIS MUTATES SESSION STATE. It is a test affordance, not a product feature.
|
|
10
|
+
*
|
|
11
|
+
* ── WHY THIS WARNING EXISTS (QAPert 34983) ────────────────────────────────
|
|
12
|
+
* The usage block below is not decoration: THIS DOCSTRING IS THE SPREAD
|
|
13
|
+
* MECHANISM. It ships compiled into dist and into the .d.ts, so it reaches
|
|
14
|
+
* every consumer's editor as hover text, and it is a one-line copy-paste that
|
|
15
|
+
* mounts a destructive endpoint. Every one of the three surfaces this route
|
|
16
|
+
* ended up on followed exactly these instructions.
|
|
17
|
+
*
|
|
18
|
+
* The handler itself was fixed (244877: default-deny behind ENABLE_TEST_ROUTES,
|
|
19
|
+
* plus the caller-supplied-token IDOR removed). But a fixed handler shipped
|
|
20
|
+
* with unqualified mounting instructions still spreads: the guidance was never
|
|
21
|
+
* corrected alongside the code, and a package that documents a dangerous route
|
|
22
|
+
* as a plain convenience is instructing the weaker configuration.
|
|
23
|
+
*
|
|
24
|
+
* ── IF YOU MOUNT IT, MOUNT IT LIKE THIS ───────────────────────────────────
|
|
25
|
+
* Required: set ENABLE_TEST_ROUTES=true IN THAT ENVIRONMENT ONLY. Absent, empty
|
|
26
|
+
* or any other value default-DENIES (404). Never set it in production.
|
|
7
27
|
*
|
|
8
|
-
* Usage in consuming app:
|
|
9
28
|
* ```typescript
|
|
10
|
-
* // app/api/test/force-expire/route.ts
|
|
29
|
+
* // app/api/test/force-expire/route.ts — TEST/DEV ENVIRONMENTS ONLY
|
|
30
|
+
* // Requires ENABLE_TEST_ROUTES=true in that environment; 404s otherwise.
|
|
11
31
|
* export { POST } from '@payez/next-mvp/api-handlers/test/force-expire';
|
|
12
32
|
* ```
|
|
33
|
+
*
|
|
34
|
+
* If you do not need to exercise the refresh flow, DO NOT MOUNT THIS AT ALL.
|
|
35
|
+
* The safest configuration is the absence of the route.
|
|
13
36
|
*/
|
|
14
|
-
export declare const POST: (req: NextRequest) => Promise<NextResponse<
|
|
15
|
-
success: boolean;
|
|
16
|
-
error: string;
|
|
17
|
-
}> | NextResponse<{
|
|
18
|
-
success: boolean;
|
|
19
|
-
previous: number | null;
|
|
20
|
-
previousIso: string | null;
|
|
21
|
-
newExpiry: number;
|
|
22
|
-
newExpiryIso: string;
|
|
23
|
-
}>>;
|
|
37
|
+
export declare const POST: (req: NextRequest) => Promise<NextResponse<unknown>>;
|