@payez/next-mvp 4.1.14 → 4.1.21
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-handlers/account/profile.d.ts +38 -0
- package/dist/api-handlers/account/profile.js +73 -45
- package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/send-code.js +52 -18
- package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/verify-code.js +52 -18
- package/dist/api-handlers/account/verify-email.js +15 -1
- package/dist/api-handlers/account/verify-sms.js +15 -1
- package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
- package/dist/api-handlers/auth/passkey-signin.js +97 -0
- package/dist/api-handlers/auth/refresh.js +38 -15
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
- package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
- package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
- package/dist/api-handlers/mfa/backup-codes.js +40 -0
- package/dist/api-handlers/mfa/methods.d.ts +3 -0
- package/dist/api-handlers/mfa/methods.js +41 -0
- package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-confirm.js +41 -0
- package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-enroll.js +41 -0
- package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
- package/dist/api-handlers/mfa/totp-verify.js +134 -0
- package/dist/api-handlers/passkeys/list.d.ts +7 -0
- package/dist/api-handlers/passkeys/list.js +29 -0
- package/dist/api-handlers/passkeys/manage.d.ts +3 -0
- package/dist/api-handlers/passkeys/manage.js +54 -0
- package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
- package/dist/api-handlers/passkeys/register-options.js +41 -0
- package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
- package/dist/api-handlers/passkeys/register-verify.js +40 -0
- package/dist/api-handlers/session/viability.js +19 -0
- package/dist/auth/better-auth.d.ts +68 -2
- package/dist/auth/better-auth.js +137 -18
- package/dist/auth/utils/idp-client.d.ts +13 -22
- package/dist/auth/utils/idp-client.js +12 -82
- package/dist/client/better-auth-client.d.ts +62 -62
- package/dist/lib/ensure-fresh-access-token.js +49 -8
- package/dist/lib/session-store.d.ts +1 -0
- package/dist/lib/session-store.js +19 -8
- package/dist/lib/token-lifecycle.d.ts +2 -0
- package/dist/lib/token-lifecycle.js +61 -7
- package/dist/models/SessionModel.d.ts +17 -0
- package/dist/pages/login/page.d.ts +10 -1
- package/dist/pages/login/page.js +69 -80
- package/dist/pages/verify-code/page.d.ts +10 -1
- package/dist/pages/verify-code/page.js +72 -4
- package/dist/routes/auth/passkey-signin.d.ts +13 -0
- package/dist/routes/auth/passkey-signin.js +18 -0
- package/dist/routes/auth/viability.d.ts +2 -0
- package/dist/routes/auth/viability.js +88 -0
- package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
- package/dist/routes/mfa/backup-code-verify.js +5 -0
- package/dist/routes/mfa/backup-codes.d.ts +10 -0
- package/dist/routes/mfa/backup-codes.js +14 -0
- package/dist/routes/mfa/methods.d.ts +21 -0
- package/dist/routes/mfa/methods.js +10 -0
- package/dist/routes/mfa/totp-confirm.d.ts +10 -0
- package/dist/routes/mfa/totp-confirm.js +14 -0
- package/dist/routes/mfa/totp-enroll.d.ts +10 -0
- package/dist/routes/mfa/totp-enroll.js +14 -0
- package/dist/routes/mfa/totp-verify.d.ts +14 -0
- package/dist/routes/mfa/totp-verify.js +18 -0
- package/dist/routes/passkeys/list.d.ts +10 -0
- package/dist/routes/passkeys/list.js +14 -0
- package/dist/routes/passkeys/manage.d.ts +18 -0
- package/dist/routes/passkeys/manage.js +13 -0
- package/dist/routes/passkeys/register-options.d.ts +10 -0
- package/dist/routes/passkeys/register-options.js +14 -0
- package/dist/routes/passkeys/register-verify.d.ts +10 -0
- package/dist/routes/passkeys/register-verify.js +14 -0
- package/dist/server/auth.d.ts +29 -20
- package/dist/server/auth.js +8 -0
- package/dist/server/decode-session.js +37 -3
- package/package.json +958 -894
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/api-handlers/account/profile.ts +76 -57
- package/src/api-handlers/account/recovery/send-code.ts +65 -17
- package/src/api-handlers/account/recovery/verify-code.ts +65 -17
- package/src/api-handlers/account/verify-email.ts +15 -1
- package/src/api-handlers/account/verify-sms.ts +15 -1
- package/src/api-handlers/auth/passkey-signin.ts +98 -0
- package/src/api-handlers/auth/refresh.ts +38 -15
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
- package/src/api-handlers/mfa/backup-codes.ts +41 -0
- package/src/api-handlers/mfa/methods.ts +46 -0
- package/src/api-handlers/mfa/totp-confirm.ts +42 -0
- package/src/api-handlers/mfa/totp-enroll.ts +42 -0
- package/src/api-handlers/mfa/totp-verify.ts +156 -0
- package/src/api-handlers/passkeys/list.ts +31 -0
- package/src/api-handlers/passkeys/manage.ts +54 -0
- package/src/api-handlers/passkeys/register-options.ts +42 -0
- package/src/api-handlers/passkeys/register-verify.ts +41 -0
- package/src/api-handlers/session/viability.ts +19 -0
- package/src/auth/better-auth.ts +158 -18
- package/src/auth/utils/idp-client.ts +13 -95
- package/src/lib/ensure-fresh-access-token.ts +49 -8
- package/src/lib/session-store.ts +11 -1
- package/src/lib/token-lifecycle.ts +70 -7
- package/src/models/SessionModel.ts +18 -0
- package/src/pages/login/page.tsx +123 -228
- package/src/pages/verify-code/page.tsx +71 -5
- package/src/routes/auth/passkey-signin.ts +13 -0
- package/src/routes/auth/viability.ts +89 -0
- package/src/routes/mfa/backup-code-verify.ts +1 -0
- package/src/routes/mfa/backup-codes.ts +10 -0
- package/src/routes/mfa/methods.ts +22 -0
- package/src/routes/mfa/totp-confirm.ts +10 -0
- package/src/routes/mfa/totp-enroll.ts +10 -0
- package/src/routes/mfa/totp-verify.ts +14 -0
- package/src/routes/passkeys/list.ts +10 -0
- package/src/routes/passkeys/manage.ts +23 -0
- package/src/routes/passkeys/register-options.ts +10 -0
- package/src/routes/passkeys/register-verify.ts +10 -0
- package/src/server/auth.ts +17 -1
- package/src/server/decode-session.ts +36 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Verify TOTP 2FA Challenge Handler
|
|
4
|
+
*
|
|
5
|
+
* Verifies an authenticator-app code AT LOGIN and completes the 2FA flow,
|
|
6
|
+
* transitioning the session to MFA-verified.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS EXISTS (2026-08-02)
|
|
9
|
+
* The package shipped TOTP ENROLMENT (totp-enroll, totp-confirm, against
|
|
10
|
+
* /api/Account/mfa/totp/*) but no TOTP CHALLENGE. SMS and email both had one
|
|
11
|
+
* (verify-sms, verify-email, against /api/ExternalAuth/twofa/*), so TOTP could
|
|
12
|
+
* be enrolled and then never used to satisfy a login challenge — mfaVerified
|
|
13
|
+
* stayed false forever for authenticator-app users, even though the IDP
|
|
14
|
+
* advertises totp in allowed2FAMethods and exposes the endpoint.
|
|
15
|
+
*
|
|
16
|
+
* DO NOT "FIX" THIS BY MAKING totp-confirm TRANSITION THE SESSION. Three people
|
|
17
|
+
* proposed exactly that on the grounds that its SMS/email "siblings" do it. They
|
|
18
|
+
* are not siblings: totp-confirm is ENROLMENT. Flipping mfaVerified there would
|
|
19
|
+
* mean enrolling a factor counts as passing a challenge, letting a
|
|
20
|
+
* password-only session self-promote to MFA-verified by adding an
|
|
21
|
+
* authenticator. Enrolment must never set mfaVerified.
|
|
22
|
+
*
|
|
23
|
+
* FIELD NAME: the IDP expects { "code": ... } here
|
|
24
|
+
* (MfaTotpVerifyRequest, [JsonPropertyName("code")]), NOT the
|
|
25
|
+
* { "verification_code": ... } that the SMS endpoint takes. Copying verify-sms
|
|
26
|
+
* and changing only the URL produces a handler that reaches the IDP and is
|
|
27
|
+
* rejected, which reads as "wrong code" rather than "wrong shape".
|
|
28
|
+
*
|
|
29
|
+
* @package @payez/next-mvp
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.POST = POST;
|
|
33
|
+
const server_1 = require("next/server");
|
|
34
|
+
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
35
|
+
const env_1 = require("../../config/env");
|
|
36
|
+
const test_aware_get_token_1 = require("../../lib/test-aware-get-token");
|
|
37
|
+
const session_store_1 = require("../../lib/session-store");
|
|
38
|
+
const jwt_decode_1 = require("../../lib/jwt-decode");
|
|
39
|
+
async function POST(req) {
|
|
40
|
+
try {
|
|
41
|
+
const body = await req.json();
|
|
42
|
+
// Accept the same aliases the client might send; the IDP field is `code`.
|
|
43
|
+
const code = body.code || body.totpCode || body.verificationCode || body.verification_code;
|
|
44
|
+
if (!code) {
|
|
45
|
+
return server_1.NextResponse.json({
|
|
46
|
+
success: false,
|
|
47
|
+
error: 'Authenticator code is required',
|
|
48
|
+
code: 'INVALID_REQUEST',
|
|
49
|
+
}, { status: 400 });
|
|
50
|
+
}
|
|
51
|
+
// idpFetchJSON injects the caller's Bearer from the session store.
|
|
52
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/ExternalAuth/twofa/totp/verify`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ code }),
|
|
56
|
+
});
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
return server_1.NextResponse.json({
|
|
59
|
+
success: false,
|
|
60
|
+
error: result.json?.message || 'Verification failed',
|
|
61
|
+
code: result.json?.code || 'IDP_ERROR',
|
|
62
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
63
|
+
}, { status: result.status });
|
|
64
|
+
}
|
|
65
|
+
// Unwrap IDP envelope { success, data }
|
|
66
|
+
const unwrappedData = result.json?.data || result.json;
|
|
67
|
+
// Only a successful challenge that returns fresh tokens may transition the
|
|
68
|
+
// session. No tokens => no transition: we must not mark 2FA complete on the
|
|
69
|
+
// strength of a 200 alone.
|
|
70
|
+
let sessionTransitioned = false;
|
|
71
|
+
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
72
|
+
const token = await (0, test_aware_get_token_1.getTokenTestAware)(req);
|
|
73
|
+
const sessionToken = (token?.sessionToken || token?.redisSessionId);
|
|
74
|
+
if (sessionToken) {
|
|
75
|
+
let accessTokenExpires = Date.now() + 15 * 60 * 1000;
|
|
76
|
+
try {
|
|
77
|
+
const decoded = (0, jwt_decode_1.jwtDecode)(unwrappedData.access_token);
|
|
78
|
+
if (decoded?.exp)
|
|
79
|
+
accessTokenExpires = decoded.exp * 1000;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
console.warn('[VERIFY_TOTP] Could not decode access token, using default expiration');
|
|
83
|
+
}
|
|
84
|
+
let refreshTokenExpires = Date.now() + 3 * 24 * 60 * 60 * 1000;
|
|
85
|
+
try {
|
|
86
|
+
const decoded = (0, jwt_decode_1.jwtDecode)(unwrappedData.refresh_token);
|
|
87
|
+
if (decoded?.exp)
|
|
88
|
+
refreshTokenExpires = decoded.exp * 1000;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Refresh token may carry no exp claim; default stands.
|
|
92
|
+
}
|
|
93
|
+
await (0, session_store_1.transitionTo2FASession)(sessionToken, {
|
|
94
|
+
accessToken: unwrappedData.access_token,
|
|
95
|
+
refreshToken: unwrappedData.refresh_token,
|
|
96
|
+
accessTokenExpires,
|
|
97
|
+
refreshTokenExpires,
|
|
98
|
+
}, 'totp' // 2FA method, used by the refresh-token flow
|
|
99
|
+
);
|
|
100
|
+
sessionTransitioned = true;
|
|
101
|
+
console.log('[VERIFY_TOTP] Session transitioned to 2FA-verified');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.warn('[VERIFY_TOTP] IDP verified the code but no session token was resolvable — session NOT transitioned');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.warn('[VERIFY_TOTP] IDP returned 2xx without tokens — session NOT transitioned');
|
|
109
|
+
}
|
|
110
|
+
// Do not expose tokens to the client.
|
|
111
|
+
//
|
|
112
|
+
// twoFactorSessionVerified REPORTS WHAT ACTUALLY HAPPENED. It is NOT a copy
|
|
113
|
+
// of `success`. The code can verify at the IDP while the session is not
|
|
114
|
+
// transitioned — no tokens returned, or no resolvable session token — and a
|
|
115
|
+
// client that trusted a hardcoded `true` would show the user as MFA-verified
|
|
116
|
+
// while the session still says otherwise. That divergence between the guard
|
|
117
|
+
// and the report is the defect this field exists to avoid.
|
|
118
|
+
// (verify-sms.ts hardcodes it true and has the same flaw — reported upstream.)
|
|
119
|
+
return server_1.NextResponse.json({
|
|
120
|
+
success: true,
|
|
121
|
+
verificationSuccessful: true,
|
|
122
|
+
twoFactorSessionVerified: sessionTransitioned,
|
|
123
|
+
message: unwrappedData.message || 'Authenticator code verified successfully',
|
|
124
|
+
}, { status: 200 });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('[VERIFY_TOTP] Error:', error);
|
|
128
|
+
return server_1.NextResponse.json({
|
|
129
|
+
success: false,
|
|
130
|
+
error: 'Failed to verify code',
|
|
131
|
+
code: 'INTERNAL_ERROR',
|
|
132
|
+
}, { status: 500 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* List the signed-in user's passkeys (GET /api/Passkeys, user JWT).
|
|
4
|
+
* Probe-verified live 2026-08-01 (anon -> 401 = routed).
|
|
5
|
+
* No session -> upstream 401, passed through, never invented locally.
|
|
6
|
+
*/
|
|
7
|
+
export declare function GET(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const server_1 = require("next/server");
|
|
5
|
+
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
6
|
+
const env_1 = require("../../config/env");
|
|
7
|
+
/**
|
|
8
|
+
* List the signed-in user's passkeys (GET /api/Passkeys, user JWT).
|
|
9
|
+
* Probe-verified live 2026-08-01 (anon -> 401 = routed).
|
|
10
|
+
* No session -> upstream 401, passed through, never invented locally.
|
|
11
|
+
*/
|
|
12
|
+
async function GET(req) {
|
|
13
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Passkeys`, { method: 'GET' });
|
|
14
|
+
if (!result.ok) {
|
|
15
|
+
return server_1.NextResponse.json({
|
|
16
|
+
success: false,
|
|
17
|
+
message: 'Upstream error',
|
|
18
|
+
error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
|
|
19
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
20
|
+
}, { status: result.status });
|
|
21
|
+
}
|
|
22
|
+
const bodyJson = result.json;
|
|
23
|
+
if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
|
|
24
|
+
if (bodyJson.success === true)
|
|
25
|
+
return server_1.NextResponse.json(bodyJson.data, { status: 200 });
|
|
26
|
+
return server_1.NextResponse.json(bodyJson, { status: 200 });
|
|
27
|
+
}
|
|
28
|
+
return server_1.NextResponse.json(bodyJson ?? {}, { status: 200 });
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PATCH = PATCH;
|
|
4
|
+
exports.DELETE = DELETE;
|
|
5
|
+
const server_1 = require("next/server");
|
|
6
|
+
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
7
|
+
const env_1 = require("../../config/env");
|
|
8
|
+
/**
|
|
9
|
+
* Manage ONE passkey (PATCH rename / DELETE revoke, /api/Passkeys/{id},
|
|
10
|
+
* user JWT). The id arrives from the Next dynamic segment via the routes
|
|
11
|
+
* wrapper — it is never read from the body.
|
|
12
|
+
*/
|
|
13
|
+
function upstreamError(result) {
|
|
14
|
+
return server_1.NextResponse.json({
|
|
15
|
+
success: false,
|
|
16
|
+
message: 'Upstream error',
|
|
17
|
+
error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
|
|
18
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
19
|
+
}, { status: result.status });
|
|
20
|
+
}
|
|
21
|
+
function passthrough(json) {
|
|
22
|
+
const bodyJson = json;
|
|
23
|
+
if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
|
|
24
|
+
if (bodyJson.success === true)
|
|
25
|
+
return server_1.NextResponse.json(bodyJson.data ?? { success: true }, { status: 200 });
|
|
26
|
+
return server_1.NextResponse.json(bodyJson, { status: 200 });
|
|
27
|
+
}
|
|
28
|
+
return server_1.NextResponse.json(bodyJson ?? { success: true }, { status: 200 });
|
|
29
|
+
}
|
|
30
|
+
async function PATCH(req, id) {
|
|
31
|
+
let body = '{}';
|
|
32
|
+
try {
|
|
33
|
+
const raw = await req.text();
|
|
34
|
+
if (raw && raw.trim().length > 0)
|
|
35
|
+
body = raw;
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Passkeys/${encodeURIComponent(id)}`, {
|
|
39
|
+
method: 'PATCH',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body,
|
|
42
|
+
});
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
return upstreamError(result);
|
|
45
|
+
return passthrough(result.json);
|
|
46
|
+
}
|
|
47
|
+
async function DELETE(req, id) {
|
|
48
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Passkeys/${encodeURIComponent(id)}`, {
|
|
49
|
+
method: 'DELETE',
|
|
50
|
+
});
|
|
51
|
+
if (!result.ok)
|
|
52
|
+
return upstreamError(result);
|
|
53
|
+
return passthrough(result.json);
|
|
54
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* Begin the passkey REGISTRATION ceremony (POST /api/Passkeys/register/options,
|
|
4
|
+
* user JWT — a credential change; the IDP may demand step-up, surfaced as-is).
|
|
5
|
+
* Returns the WebAuthn PublicKeyCredentialCreationOptions (ride the envelope
|
|
6
|
+
* `data`, per the archived hand-roll contract).
|
|
7
|
+
*/
|
|
8
|
+
export declare function POST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const server_1 = require("next/server");
|
|
5
|
+
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
6
|
+
const env_1 = require("../../config/env");
|
|
7
|
+
/**
|
|
8
|
+
* Begin the passkey REGISTRATION ceremony (POST /api/Passkeys/register/options,
|
|
9
|
+
* user JWT — a credential change; the IDP may demand step-up, surfaced as-is).
|
|
10
|
+
* Returns the WebAuthn PublicKeyCredentialCreationOptions (ride the envelope
|
|
11
|
+
* `data`, per the archived hand-roll contract).
|
|
12
|
+
*/
|
|
13
|
+
async function POST(req) {
|
|
14
|
+
let body = '{}';
|
|
15
|
+
try {
|
|
16
|
+
const raw = await req.text();
|
|
17
|
+
if (raw && raw.trim().length > 0)
|
|
18
|
+
body = raw;
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Passkeys/register/options`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body,
|
|
25
|
+
});
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
return server_1.NextResponse.json({
|
|
28
|
+
success: false,
|
|
29
|
+
message: 'Upstream error',
|
|
30
|
+
error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
|
|
31
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
32
|
+
}, { status: result.status });
|
|
33
|
+
}
|
|
34
|
+
const bodyJson = result.json;
|
|
35
|
+
if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
|
|
36
|
+
if (bodyJson.success === true)
|
|
37
|
+
return server_1.NextResponse.json(bodyJson.data, { status: 200 });
|
|
38
|
+
return server_1.NextResponse.json(bodyJson, { status: 200 });
|
|
39
|
+
}
|
|
40
|
+
return server_1.NextResponse.json(bodyJson ?? {}, { status: 200 });
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* Finish the passkey REGISTRATION ceremony (POST /api/Passkeys/register/verify,
|
|
4
|
+
* user JWT). Body forwarded VERBATIM — it carries the enrolment handle plus the
|
|
5
|
+
* authenticator attestation produced by navigator.credentials.create().
|
|
6
|
+
*/
|
|
7
|
+
export declare function POST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const server_1 = require("next/server");
|
|
5
|
+
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
6
|
+
const env_1 = require("../../config/env");
|
|
7
|
+
/**
|
|
8
|
+
* Finish the passkey REGISTRATION ceremony (POST /api/Passkeys/register/verify,
|
|
9
|
+
* user JWT). Body forwarded VERBATIM — it carries the enrolment handle plus the
|
|
10
|
+
* authenticator attestation produced by navigator.credentials.create().
|
|
11
|
+
*/
|
|
12
|
+
async function POST(req) {
|
|
13
|
+
let body = '{}';
|
|
14
|
+
try {
|
|
15
|
+
const raw = await req.text();
|
|
16
|
+
if (raw && raw.trim().length > 0)
|
|
17
|
+
body = raw;
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Passkeys/register/verify`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body,
|
|
24
|
+
});
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
return server_1.NextResponse.json({
|
|
27
|
+
success: false,
|
|
28
|
+
message: 'Upstream error',
|
|
29
|
+
error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
|
|
30
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
31
|
+
}, { status: result.status });
|
|
32
|
+
}
|
|
33
|
+
const bodyJson = result.json;
|
|
34
|
+
if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
|
|
35
|
+
if (bodyJson.success === true)
|
|
36
|
+
return server_1.NextResponse.json(bodyJson.data, { status: 200 });
|
|
37
|
+
return server_1.NextResponse.json(bodyJson, { status: 200 });
|
|
38
|
+
}
|
|
39
|
+
return server_1.NextResponse.json(bodyJson ?? {}, { status: 200 });
|
|
40
|
+
}
|
|
@@ -103,6 +103,25 @@ async function GET(req) {
|
|
|
103
103
|
// 2FA fields - critical for middleware redirect logic
|
|
104
104
|
requires2FA, // From cached client config (client-wide setting)
|
|
105
105
|
twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
|
|
106
|
+
// Cards 141688/141741 — the IDP's routing decision, relayed verbatim.
|
|
107
|
+
//
|
|
108
|
+
// THIS IS THE SECOND IMPLEMENTATION OF VIABILITY. The other is
|
|
109
|
+
// routes/auth/viability.ts, which is what the admin graft mounts.
|
|
110
|
+
// Both are publicly exported entry points, so both can be a consumer's
|
|
111
|
+
// only source of this answer. Adding the relay to just one would leave
|
|
112
|
+
// an app that mounts THIS one still blind to enroll_mfa — the exact
|
|
113
|
+
// seam these cards are about, reopened one export over.
|
|
114
|
+
//
|
|
115
|
+
// 'enroll_mfa' = no USABLE method, the user cannot complete a
|
|
116
|
+
// challenge. 'mfa_verify' = usable method exists. null = nothing due.
|
|
117
|
+
// NOT a completion signal: twoFactorComplete still owns that.
|
|
118
|
+
//
|
|
119
|
+
// No `?? null`: undefined (never captured — every session minted
|
|
120
|
+
// before this change) must stay distinguishable from null (the IDP
|
|
121
|
+
// said nothing is outstanding). Defaulting to null would tell the
|
|
122
|
+
// gate that every pre-existing session is fully authenticated,
|
|
123
|
+
// including zero-method users who must be routed to enrol.
|
|
124
|
+
nextStep: sessionData.nextStep,
|
|
106
125
|
// Token status for refresh decisions
|
|
107
126
|
accessTokenExpired,
|
|
108
127
|
hasRefreshToken: !!sessionData.idpRefreshToken
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @see BETTER-AUTH-MIGRATION-SPEC.md
|
|
10
10
|
*/
|
|
11
11
|
import 'server-only';
|
|
12
|
+
import { betterAuth } from 'better-auth';
|
|
12
13
|
import { type MagicLinkOptions } from 'better-auth/plugins/magic-link';
|
|
13
14
|
import type { IDPClientConfig } from '../lib/idp-client-config';
|
|
14
15
|
/**
|
|
@@ -34,6 +35,27 @@ export declare function buildBetterAuthProviders(config: IDPClientConfig): Recor
|
|
|
34
35
|
*/
|
|
35
36
|
export interface CreateBetterAuthInstanceOptions {
|
|
36
37
|
magicLink?: MagicLinkOptions;
|
|
38
|
+
/**
|
|
39
|
+
* Extra Better Auth plugins supplied by the consuming app.
|
|
40
|
+
*
|
|
41
|
+
* WHY THIS EXISTS — session minting is not delegable any other way. BA's
|
|
42
|
+
* `setSessionCookie` requires a `GenericEndpointContext`, which only BA's own
|
|
43
|
+
* endpoint machinery constructs, so an app that must mint a session for an
|
|
44
|
+
* identity established OUTSIDE BA's built-in providers (passkey sign-in being
|
|
45
|
+
* the first) cannot do it from a route handler. It needs a real BA endpoint,
|
|
46
|
+
* and endpoints only arrive via plugins.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately GENERIC. The package gains "an app may supply plugins" rather
|
|
49
|
+
* than any knowledge of passkeys — the alternative was teaching this file
|
|
50
|
+
* about a specific auth method, which is how a package accumulates a consumer's
|
|
51
|
+
* feature list.
|
|
52
|
+
*
|
|
53
|
+
* The alternative to this option is an app hand-rolling the session cookie
|
|
54
|
+
* (`{token}.{base64(hmac)}`, standard padding, runtime `authSecret` from
|
|
55
|
+
* Redis). That fails as a session which DECODES BUT DOES NOT AUTHENTICATE —
|
|
56
|
+
* so this option exists specifically to make that shortcut unnecessary.
|
|
57
|
+
*/
|
|
58
|
+
plugins?: Parameters<typeof betterAuth>[0]['plugins'];
|
|
37
59
|
}
|
|
38
60
|
/**
|
|
39
61
|
* Create Better Auth instance from IDP config.
|
|
@@ -78,7 +100,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
|
|
|
78
100
|
handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>;
|
|
79
101
|
}[];
|
|
80
102
|
};
|
|
81
|
-
}, ...{
|
|
103
|
+
}, ...(import("better-auth").BetterAuthPlugin | {
|
|
82
104
|
id: "magic-link";
|
|
83
105
|
endpoints: {
|
|
84
106
|
signInMagicLink: import("better-auth").StrictEndpoint<"/sign-in/magic-link", {
|
|
@@ -173,7 +195,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
|
|
|
173
195
|
max: number;
|
|
174
196
|
}[];
|
|
175
197
|
options: MagicLinkOptions;
|
|
176
|
-
}[]];
|
|
198
|
+
})[]];
|
|
177
199
|
}>;
|
|
178
200
|
/**
|
|
179
201
|
* Better Auth is always enabled (NextAuth removed in 4.0).
|
|
@@ -228,6 +250,50 @@ export declare function getBetterAuthHandler(): Promise<{
|
|
|
228
250
|
* This replaces the old databaseHooks approach which doesn't fire in stateless mode.
|
|
229
251
|
*/
|
|
230
252
|
export declare function exchangeOAuthForIdpTokens(sessionToken: string, provider?: string): Promise<boolean>;
|
|
253
|
+
/**
|
|
254
|
+
* Write IDP tokens into an EXISTING Better Auth session. Storage only — this
|
|
255
|
+
* function never acquires tokens and never decides who the caller is.
|
|
256
|
+
*
|
|
257
|
+
* ── WHY THIS IS SPLIT OUT (BAPert ruling, 2026-08-04) ────────────────────────
|
|
258
|
+
*
|
|
259
|
+
* `exchangeOAuthForIdpTokens` above does TWO things: it ACQUIRES tokens by
|
|
260
|
+
* POSTing to `/api/ExternalAuth/oauth-callback`, and then it STORES them. Only
|
|
261
|
+
* the second half is reusable, and the first half is actively dangerous to
|
|
262
|
+
* reuse:
|
|
263
|
+
*
|
|
264
|
+
* oauth-callback mints an IDP bearer from an ASSERTED EMAIL with no
|
|
265
|
+
* cryptographic verification of any kind (card 138635).
|
|
266
|
+
*
|
|
267
|
+
* Passkey sign-in already holds something strictly better — a grant issued by
|
|
268
|
+
* `/api/ExternalAuth/passwordless/passkey/verify`, which checked a real
|
|
269
|
+
* signature against a registered credential. Routing that through
|
|
270
|
+
* oauth-callback to obtain a session would DOWNGRADE a cryptographically
|
|
271
|
+
* verified assertion into an unverified one, make passkey sign-in weaker than
|
|
272
|
+
* its own ceremony, and inherit 138635 wholesale.
|
|
273
|
+
*
|
|
274
|
+
* That is not hypothetical: "do what the OAuth path does" is the obvious
|
|
275
|
+
* implementation and it is the wrong one. Splitting acquisition from storage is
|
|
276
|
+
* what makes the wrong one hard to write by accident.
|
|
277
|
+
*
|
|
278
|
+
* ── WHAT CALLERS MUST HAVE DONE FIRST ───────────────────────────────────────
|
|
279
|
+
*
|
|
280
|
+
* A Better Auth session must ALREADY EXIST for `sessionToken`. This function
|
|
281
|
+
* enriches; it does not create. Better Auth owns session creation and cookie
|
|
282
|
+
* signing, and nothing in this package should replicate either — the cookie is
|
|
283
|
+
* `{token}.{base64(hmac)}` with standard padding, signed with the runtime
|
|
284
|
+
* `authSecret` from Redis, and hand-rolling that produces a session which
|
|
285
|
+
* decodes but does not authenticate.
|
|
286
|
+
*
|
|
287
|
+
* @param sessionToken the Better Auth session token (from its Set-Cookie)
|
|
288
|
+
* @param idpTokenData tokens + identity, already obtained by the CALLER'S OWN
|
|
289
|
+
* verified route. Built field-by-field by the caller so an
|
|
290
|
+
* unnamed field cannot ride along silently.
|
|
291
|
+
* @param authProvider provenance label recorded on the canonical row. Written
|
|
292
|
+
* to the historical `oauthProvider` key, which predates
|
|
293
|
+
* non-OAuth callers; the NAME is legacy, the VALUE should
|
|
294
|
+
* state what actually authenticated ('passkey', 'google').
|
|
295
|
+
*/
|
|
296
|
+
export declare function storeIdpTokensInSession(sessionToken: string, idpTokenData: Record<string, unknown>, authProvider: string): Promise<boolean>;
|
|
231
297
|
/**
|
|
232
298
|
* Create a production-ready GET handler for the auth catch-all route.
|
|
233
299
|
*
|