@payez/next-mvp 4.0.19 → 4.0.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/session/viability.js +9 -1
- package/dist/auth/better-auth.js +2 -0
- package/dist/lib/session-store.d.ts +9 -0
- package/dist/lib/session-store.js +65 -21
- package/dist/server/decode-session.js +3 -11
- package/package.json +1 -1
- package/src/api-handlers/session/viability.ts +136 -128
- package/src/auth/better-auth.ts +2 -0
- package/src/lib/session-store.ts +689 -645
- package/src/server/decode-session.ts +3 -12
|
@@ -45,7 +45,15 @@ async function GET(req) {
|
|
|
45
45
|
}
|
|
46
46
|
const sessionToken = betterAuthSession?.session?.token;
|
|
47
47
|
if (betterAuthSession && sessionToken) {
|
|
48
|
-
|
|
48
|
+
// Try legacy session store first, then Better Auth format
|
|
49
|
+
let sessionData = await (0, session_store_1.getSession)(sessionToken);
|
|
50
|
+
if (!sessionData) {
|
|
51
|
+
// Better Auth stores sessions with ba:{appSlug}:{token} prefix
|
|
52
|
+
sessionData = await (0, session_store_1.getBetterAuthSession)(sessionToken);
|
|
53
|
+
if (sessionData) {
|
|
54
|
+
console.log('[VIABILITY] Found session in Better Auth store');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
if (sessionData) {
|
|
50
58
|
// The session exists in Redis
|
|
51
59
|
// Check if access token is expired (for middleware decision-making)
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -160,6 +160,7 @@ function createBetterAuthInstance(idpConfig) {
|
|
|
160
160
|
}
|
|
161
161
|
// Store IDP tokens in the BA Redis session
|
|
162
162
|
if (baData) {
|
|
163
|
+
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
163
164
|
baData.idpTokens = {
|
|
164
165
|
idpAccessToken: result.access_token,
|
|
165
166
|
idpRefreshToken: result.refresh_token,
|
|
@@ -170,6 +171,7 @@ function createBetterAuthInstance(idpConfig) {
|
|
|
170
171
|
email: result.user?.email || result.email || email,
|
|
171
172
|
name: result.user?.full_name || result.user?.name || result.name || name,
|
|
172
173
|
roles: result.user?.roles || result.roles || [],
|
|
174
|
+
mfaVerified: !requiresTwoFactor,
|
|
173
175
|
};
|
|
174
176
|
await (0, redis_1.getRedis)().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
175
177
|
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
@@ -34,6 +34,15 @@ export declare function createSession(data: SessionData): Promise<string>;
|
|
|
34
34
|
* @returns The session data, or null if not found.
|
|
35
35
|
*/
|
|
36
36
|
export declare function getSession(sessionToken: string): Promise<SessionData | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves a Better Auth session from Redis.
|
|
39
|
+
* Better Auth uses key format: ba:{appSlug}:{token}
|
|
40
|
+
*
|
|
41
|
+
* @param sessionToken The session token to look up.
|
|
42
|
+
* @param appSlug The app slug (defaults to 'idealvibe_online' or extracted from env).
|
|
43
|
+
* @returns The session data, or null if not found.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getBetterAuthSession(sessionToken: string, appSlug?: string): Promise<SessionData | null>;
|
|
37
46
|
/**
|
|
38
47
|
* Refresh session TTL without reading/writing data (sliding window expiry).
|
|
39
48
|
*/
|
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.generateSessionToken = generateSessionToken;
|
|
16
16
|
exports.createSession = createSession;
|
|
17
17
|
exports.getSession = getSession;
|
|
18
|
+
exports.getBetterAuthSession = getBetterAuthSession;
|
|
18
19
|
exports.touchSession = touchSession;
|
|
19
20
|
exports.getSessionWithVersion = getSessionWithVersion;
|
|
20
21
|
exports.isAccessTokenFresh = isAccessTokenFresh;
|
|
@@ -42,6 +43,8 @@ const token_utils_1 = require("../auth/utils/token-utils");
|
|
|
42
43
|
const getSessionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}${token}`;
|
|
43
44
|
const getRefreshLockKey = (token) => `${(0, app_slug_1.getRefreshLockPrefix)()}${token}`;
|
|
44
45
|
const getSessionVersionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}ver:${token}`;
|
|
46
|
+
// Better Auth uses a different key format: ba:{appSlug}:{token}
|
|
47
|
+
const getBetterAuthSessionKey = (token, appSlug) => `ba:${appSlug || 'app'}:${token}`;
|
|
45
48
|
const REFRESH_LOCK_TTL = 60; // 60 seconds
|
|
46
49
|
const SESSION_TTL = 3 * 24 * 60 * 60; // 3 days in seconds (matches refresh token lifetime)
|
|
47
50
|
/**
|
|
@@ -96,6 +99,47 @@ async function getSession(sessionToken) {
|
|
|
96
99
|
return null;
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Retrieves a Better Auth session from Redis.
|
|
104
|
+
* Better Auth uses key format: ba:{appSlug}:{token}
|
|
105
|
+
*
|
|
106
|
+
* @param sessionToken The session token to look up.
|
|
107
|
+
* @param appSlug The app slug (defaults to 'idealvibe_online' or extracted from env).
|
|
108
|
+
* @returns The session data, or null if not found.
|
|
109
|
+
*/
|
|
110
|
+
async function getBetterAuthSession(sessionToken, appSlug) {
|
|
111
|
+
if (!sessionToken) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// Try to get appSlug from env if not provided
|
|
115
|
+
const slug = appSlug || process.env.CLIENT_ID || 'idealvibe_online';
|
|
116
|
+
const key = getBetterAuthSessionKey(sessionToken, slug);
|
|
117
|
+
const json = await redis_1.default.get(key);
|
|
118
|
+
if (!json) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const data = JSON.parse(json);
|
|
123
|
+
// Better Auth stores the session differently - extract user data
|
|
124
|
+
if (data.user) {
|
|
125
|
+
return {
|
|
126
|
+
userId: data.user.id || data.user.email,
|
|
127
|
+
email: data.user.email,
|
|
128
|
+
name: data.user.name,
|
|
129
|
+
idpAccessToken: data.idpTokens?.idpAccessToken,
|
|
130
|
+
idpRefreshToken: data.idpTokens?.idpRefreshToken,
|
|
131
|
+
idpAccessTokenExpires: data.idpTokens?.idpAccessTokenExpires,
|
|
132
|
+
mfaVerified: data.idpTokens?.mfaVerified ?? false,
|
|
133
|
+
roles: data.idpTokens?.roles || [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.error('[SESSION-STORE] Failed to parse Better Auth session data');
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
99
143
|
/**
|
|
100
144
|
* Refresh session TTL without reading/writing data (sliding window expiry).
|
|
101
145
|
*/
|
|
@@ -438,27 +482,27 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
438
482
|
const lockKey = getRefreshLockKey(sessionToken);
|
|
439
483
|
try {
|
|
440
484
|
// Lua script for atomic lock validation and release
|
|
441
|
-
const luaScript = `
|
|
442
|
-
local lockKey = KEYS[1]
|
|
443
|
-
local expectedRequestId = ARGV[1]
|
|
444
|
-
local expectedVersion = ARGV[2]
|
|
445
|
-
|
|
446
|
-
local lockData = redis.call('GET', lockKey)
|
|
447
|
-
if not lockData then
|
|
448
|
-
return 0 -- Lock doesn't exist
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
local lockInfo = cjson.decode(lockData)
|
|
452
|
-
if lockInfo.acquiredBy == expectedRequestId then
|
|
453
|
-
if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
|
|
454
|
-
redis.call('DEL', lockKey)
|
|
455
|
-
return 1 -- Successfully released
|
|
456
|
-
else
|
|
457
|
-
return -2 -- Version mismatch
|
|
458
|
-
end
|
|
459
|
-
else
|
|
460
|
-
return -1 -- Wrong owner
|
|
461
|
-
end
|
|
485
|
+
const luaScript = `
|
|
486
|
+
local lockKey = KEYS[1]
|
|
487
|
+
local expectedRequestId = ARGV[1]
|
|
488
|
+
local expectedVersion = ARGV[2]
|
|
489
|
+
|
|
490
|
+
local lockData = redis.call('GET', lockKey)
|
|
491
|
+
if not lockData then
|
|
492
|
+
return 0 -- Lock doesn't exist
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
local lockInfo = cjson.decode(lockData)
|
|
496
|
+
if lockInfo.acquiredBy == expectedRequestId then
|
|
497
|
+
if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
|
|
498
|
+
redis.call('DEL', lockKey)
|
|
499
|
+
return 1 -- Successfully released
|
|
500
|
+
else
|
|
501
|
+
return -2 -- Version mismatch
|
|
502
|
+
end
|
|
503
|
+
else
|
|
504
|
+
return -1 -- Wrong owner
|
|
505
|
+
end
|
|
462
506
|
`;
|
|
463
507
|
const result = await redis_1.default.eval(luaScript, 1, lockKey, requestId, lockVersion ? lockVersion.toString() : '');
|
|
464
508
|
if (result === 1) {
|
|
@@ -53,18 +53,10 @@ const startup_init_1 = require("../lib/startup-init");
|
|
|
53
53
|
*/
|
|
54
54
|
async function tryBetterAuthSession(requestCookies) {
|
|
55
55
|
try {
|
|
56
|
-
const {
|
|
57
|
-
// getBetterAuthHandler initializes the instance; we need the raw instance
|
|
58
|
-
const { default: getBetterAuthInstanceFn } = await Promise.resolve().then(() => __importStar(require('../auth/better-auth'))).then(m => ({ default: m.getBetterAuthInstance || null }))
|
|
59
|
-
.catch(() => ({ default: null }));
|
|
60
|
-
// Access the cached instance via the module's internal getter
|
|
56
|
+
const { getBetterAuthInstance } = await Promise.resolve().then(() => __importStar(require('../auth/better-auth')));
|
|
61
57
|
let auth = null;
|
|
62
58
|
try {
|
|
63
|
-
|
|
64
|
-
await getBetterAuthHandler();
|
|
65
|
-
// The instance is cached in the module — re-import to access it
|
|
66
|
-
const mod = await Promise.resolve().then(() => __importStar(require('../auth/better-auth')));
|
|
67
|
-
auth = mod.__betterAuthInstance;
|
|
59
|
+
auth = await getBetterAuthInstance();
|
|
68
60
|
}
|
|
69
61
|
catch {
|
|
70
62
|
return null;
|
|
@@ -122,7 +114,7 @@ async function tryBetterAuthSession(requestCookies) {
|
|
|
122
114
|
idpRefreshToken: idpTokens?.idpRefreshToken,
|
|
123
115
|
idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
|
|
124
116
|
|| (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
|
|
125
|
-
mfaVerified:
|
|
117
|
+
mfaVerified: idpTokens?.mfaVerified ?? false,
|
|
126
118
|
oauthProvider: 'google',
|
|
127
119
|
};
|
|
128
120
|
// Backwards compat: session.user.email works alongside session.email
|
package/package.json
CHANGED
|
@@ -1,129 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Viability Check API Handler for `@payez/next-mvp`
|
|
3
|
-
*
|
|
4
|
-
* This API route is called by the middleware to securely check if a session is valid.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
-
import { getSession as getRedisSession } from '../../lib/session-store';
|
|
9
|
-
import { getSession } from '../../server/auth';
|
|
10
|
-
import { isInitializationFailed, ensureInitialized } from '../../lib/startup-init';
|
|
11
|
-
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
12
|
-
|
|
13
|
-
export async function GET(req: NextRequest) {
|
|
14
|
-
try {
|
|
15
|
-
// Ensure initialization is complete
|
|
16
|
-
if (!process.env.NEXTAUTH_SECRET) {
|
|
17
|
-
try {
|
|
18
|
-
await ensureInitialized();
|
|
19
|
-
} catch (error) {
|
|
20
|
-
// Initialization failed - return 503
|
|
21
|
-
console.error('[API Viability] Initialization failed - returning 503');
|
|
22
|
-
return NextResponse.json(
|
|
23
|
-
{
|
|
24
|
-
error: 'Service Unavailable',
|
|
25
|
-
message: 'Authentication service is not properly configured',
|
|
26
|
-
code: 'AUTH_NOT_INITIALIZED'
|
|
27
|
-
},
|
|
28
|
-
{ status: 503 }
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Double-check after initialization attempt
|
|
34
|
-
if (isInitializationFailed()) {
|
|
35
|
-
console.error('[API Viability] Initialization failed - returning 503');
|
|
36
|
-
return NextResponse.json(
|
|
37
|
-
{
|
|
38
|
-
error: 'Service Unavailable',
|
|
39
|
-
message: 'Authentication service is not properly configured',
|
|
40
|
-
code: 'AUTH_NOT_INITIALIZED'
|
|
41
|
-
},
|
|
42
|
-
{ status: 503 }
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Get session from Better Auth
|
|
47
|
-
const betterAuthSession = await getSession(req);
|
|
48
|
-
|
|
49
|
-
// Debug logging
|
|
50
|
-
if (!betterAuthSession) {
|
|
51
|
-
console.warn('[VIABILITY] getSession returned null');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const sessionToken = betterAuthSession?.session?.token as string | undefined;
|
|
55
|
-
if (betterAuthSession && sessionToken) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Session Viability Check API Handler for `@payez/next-mvp`
|
|
3
|
+
*
|
|
4
|
+
* This API route is called by the middleware to securely check if a session is valid.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { getSession as getRedisSession, getBetterAuthSession } from '../../lib/session-store';
|
|
9
|
+
import { getSession } from '../../server/auth';
|
|
10
|
+
import { isInitializationFailed, ensureInitialized } from '../../lib/startup-init';
|
|
11
|
+
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
12
|
+
|
|
13
|
+
export async function GET(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
// Ensure initialization is complete
|
|
16
|
+
if (!process.env.NEXTAUTH_SECRET) {
|
|
17
|
+
try {
|
|
18
|
+
await ensureInitialized();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Initialization failed - return 503
|
|
21
|
+
console.error('[API Viability] Initialization failed - returning 503');
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{
|
|
24
|
+
error: 'Service Unavailable',
|
|
25
|
+
message: 'Authentication service is not properly configured',
|
|
26
|
+
code: 'AUTH_NOT_INITIALIZED'
|
|
27
|
+
},
|
|
28
|
+
{ status: 503 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Double-check after initialization attempt
|
|
34
|
+
if (isInitializationFailed()) {
|
|
35
|
+
console.error('[API Viability] Initialization failed - returning 503');
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{
|
|
38
|
+
error: 'Service Unavailable',
|
|
39
|
+
message: 'Authentication service is not properly configured',
|
|
40
|
+
code: 'AUTH_NOT_INITIALIZED'
|
|
41
|
+
},
|
|
42
|
+
{ status: 503 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get session from Better Auth
|
|
47
|
+
const betterAuthSession = await getSession(req);
|
|
48
|
+
|
|
49
|
+
// Debug logging
|
|
50
|
+
if (!betterAuthSession) {
|
|
51
|
+
console.warn('[VIABILITY] getSession returned null');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sessionToken = betterAuthSession?.session?.token as string | undefined;
|
|
55
|
+
if (betterAuthSession && sessionToken) {
|
|
56
|
+
// Try legacy session store first, then Better Auth format
|
|
57
|
+
let sessionData = await getRedisSession(sessionToken);
|
|
58
|
+
if (!sessionData) {
|
|
59
|
+
// Better Auth stores sessions with ba:{appSlug}:{token} prefix
|
|
60
|
+
sessionData = await getBetterAuthSession(sessionToken);
|
|
61
|
+
if (sessionData) {
|
|
62
|
+
console.log('[VIABILITY] Found session in Better Auth store');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (sessionData) {
|
|
66
|
+
// The session exists in Redis
|
|
67
|
+
|
|
68
|
+
// Check if access token is expired (for middleware decision-making)
|
|
69
|
+
const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
|
|
70
|
+
const accessTokenExpired = accessTokenExpires < Date.now();
|
|
71
|
+
|
|
72
|
+
// Get requires2FA from cached client config (not session)
|
|
73
|
+
// This is a client-wide setting from the broker handshake
|
|
74
|
+
let requires2FA = true; // Default to true for security
|
|
75
|
+
try {
|
|
76
|
+
const cachedConfig = await getIDPClientConfig();
|
|
77
|
+
requires2FA = cachedConfig.authSettings?.require2FA ?? true;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// CRITICAL: Check if MFA has expired (2FA TTL enforcement)
|
|
83
|
+
// The session may have mfaVerified=true from days ago, but if mfaExpiresAt
|
|
84
|
+
// has passed, we must treat 2FA as incomplete to force re-verification.
|
|
85
|
+
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
86
|
+
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
87
|
+
// Check both field names for compatibility (mfaVerified is the normalized name)
|
|
88
|
+
const sessionMfaComplete = sessionData.mfaVerified ?? (sessionData as any).twoFactorComplete ?? false;
|
|
89
|
+
const effectiveTwoFactorComplete = sessionMfaComplete && !mfaExpired;
|
|
90
|
+
|
|
91
|
+
console.log('[VIABILITY] Session 2FA check:', {
|
|
92
|
+
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
93
|
+
mfaVerified: sessionData.mfaVerified,
|
|
94
|
+
twoFactorComplete: (sessionData as any).twoFactorComplete,
|
|
95
|
+
sessionMfaComplete,
|
|
96
|
+
mfaExpired,
|
|
97
|
+
effectiveTwoFactorComplete,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (mfaExpired && sessionMfaComplete) {
|
|
101
|
+
console.warn('[API Viability] MFA expired - forcing 2FA re-verification', {
|
|
102
|
+
mfaExpiresAt: new Date(mfaExpiresAt).toISOString(),
|
|
103
|
+
now: new Date().toISOString(),
|
|
104
|
+
hoursExpiredAgo: ((Date.now() - mfaExpiresAt) / (1000 * 60 * 60)).toFixed(1)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = {
|
|
109
|
+
authenticated: true,
|
|
110
|
+
sessionToken, // Include token for middleware tracking
|
|
111
|
+
// 2FA fields - critical for middleware redirect logic
|
|
112
|
+
requires2FA, // From cached client config (client-wide setting)
|
|
113
|
+
twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
|
|
114
|
+
// Token status for refresh decisions
|
|
115
|
+
accessTokenExpired,
|
|
116
|
+
hasRefreshToken: !!sessionData.idpRefreshToken
|
|
117
|
+
};
|
|
118
|
+
return NextResponse.json(response);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// CRITICAL: Cookie exists but Redis session is missing (stale cookie state)
|
|
122
|
+
// Return sessionToken so middleware can detect this and clear the stale cookie
|
|
123
|
+
console.warn('[VIABILITY] Stale cookie detected - session not in Redis');
|
|
124
|
+
return NextResponse.json({
|
|
125
|
+
authenticated: false,
|
|
126
|
+
sessionToken // Include token to enable stale cookie detection
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If there's no token at all, it's not authenticated
|
|
131
|
+
return NextResponse.json({ authenticated: false });
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[API Viability] Error checking session viability:', error);
|
|
135
|
+
return NextResponse.json({ authenticated: false }, { status: 500 });
|
|
136
|
+
}
|
|
129
137
|
}
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -172,6 +172,7 @@ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
|
172
172
|
|
|
173
173
|
// Store IDP tokens in the BA Redis session
|
|
174
174
|
if (baData) {
|
|
175
|
+
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
175
176
|
baData.idpTokens = {
|
|
176
177
|
idpAccessToken: result.access_token,
|
|
177
178
|
idpRefreshToken: result.refresh_token,
|
|
@@ -182,6 +183,7 @@ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
|
182
183
|
email: result.user?.email || result.email || email,
|
|
183
184
|
name: result.user?.full_name || result.user?.name || result.name || name,
|
|
184
185
|
roles: result.user?.roles || result.roles || [],
|
|
186
|
+
mfaVerified: !requiresTwoFactor,
|
|
185
187
|
};
|
|
186
188
|
await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
187
189
|
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|