diff --git a/src/config/limits.ts b/src/config/limits.ts index 589d4ce..f917170 100644 --- a/src/config/limits.ts +++ b/src/config/limits.ts @@ -5,10 +5,10 @@ accessTokenTtlSeconds: 7200, // Refresh token lifetime in milliseconds. // 刷新令牌有效期(毫秒)。 - refreshTokenTtlMs: 30 * 24 * 60 * 60 * 1000, + refreshTokenTtlMs: 365 * 24 * 60 * 60 * 1000, // Grace window for previous refresh token after rotation (ms). // 刷新令牌轮换后的旧令牌宽限窗口(毫秒)。 - refreshTokenOverlapGraceMs: 60 * 1000, + refreshTokenOverlapGraceMs: 30 * 60 * 1000, // Refresh token random byte length. // 刷新令牌随机字节长度。 refreshTokenRandomBytes: 32, diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index 5d9f8d9..d0822af 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -526,6 +526,7 @@ export async function handleChangePassword(request: Request, env: Env, userId: s user.updatedAt = new Date().toISOString(); await storage.saveUser(user); await storage.deleteRefreshTokensByUserId(user.id); + AuthService.invalidateUserCache(user.id); await storage.createAuditLog({ id: generateUUID(), actorUserId: user.id, @@ -587,6 +588,7 @@ export async function handleSetTotpStatus(request: Request, env: Env, userId: st user.updatedAt = new Date().toISOString(); await storage.saveUser(user); await storage.deleteRefreshTokensByUserId(user.id); + AuthService.invalidateUserCache(user.id); return jsonResponse({ enabled: true, recoveryCode: user.totpRecoveryCode, object: 'twoFactor' }); } @@ -601,6 +603,7 @@ export async function handleSetTotpStatus(request: Request, env: Env, userId: st user.updatedAt = new Date().toISOString(); await storage.saveUser(user); await storage.deleteRefreshTokensByUserId(user.id); + AuthService.invalidateUserCache(user.id); return jsonResponse({ enabled: false, object: 'twoFactor' }); } @@ -708,6 +711,7 @@ export async function handleRecoverTwoFactor(request: Request, env: Env): Promis user.updatedAt = new Date().toISOString(); await storage.saveUser(user); await storage.deleteRefreshTokensByUserId(user.id); + AuthService.invalidateUserCache(user.id); await rateLimit.clearLoginAttempts(recoverLimitKey); return jsonResponse({ @@ -801,6 +805,7 @@ async function apiKey(request: Request, env: Env, userId: string, rotate: boolea } user.updatedAt = new Date().toISOString(); await storage.saveUser(user); + AuthService.invalidateUserCache(user.id); } return jsonResponse({ diff --git a/src/handlers/admin.ts b/src/handlers/admin.ts index a2b1ab1..bee0951 100644 --- a/src/handlers/admin.ts +++ b/src/handlers/admin.ts @@ -1,4 +1,5 @@ import { Env, User, Invite } from '../types'; +import { AuthService } from '../services/auth'; import { StorageService } from '../services/storage'; import { jsonResponse, errorResponse } from '../utils/response'; import { generateUUID } from '../utils/uuid'; @@ -222,6 +223,7 @@ export async function handleAdminSetUserStatus( if (nextStatus === 'banned') { await storage.deleteRefreshTokensByUserId(target.id); } + AuthService.invalidateUserCache(target.id); await writeAuditLog(storage, actorUser.id, 'admin.user.status', 'user', target.id, { status: nextStatus, }); @@ -280,6 +282,7 @@ export async function handleAdminDeleteUser( await storage.deleteRefreshTokensByUserId(target.id); await storage.deleteUserById(target.id); + AuthService.invalidateUserCache(target.id); await writeAuditLog(storage, actorUser.id, 'admin.user.delete', 'user', target.id, { email: target.email, }); diff --git a/src/handlers/devices.ts b/src/handlers/devices.ts index e62f950..1499b39 100644 --- a/src/handlers/devices.ts +++ b/src/handlers/devices.ts @@ -1,6 +1,7 @@ import type { Device, DevicePendingAuthRequest, DeviceResponse, ProtectedDeviceResponse as ProtectedDeviceWireResponse } from '../types'; import { Env } from '../types'; import { getOnlineUserDevices, notifyUserLogout } from '../durable/notifications-hub'; +import { AuthService } from '../services/auth'; import { StorageService } from '../services/storage'; import { errorResponse, jsonResponse } from '../utils/response'; import { readKnownDeviceProbe } from '../utils/device'; @@ -309,6 +310,7 @@ export async function handleDeleteDevice( await storage.deleteRefreshTokensByDevice(userId, normalized); const deleted = await storage.deleteDevice(userId, normalized); if (deleted) { + AuthService.invalidateDeviceCache(userId, normalized); notifyUserLogout(env, userId, normalized); } return jsonResponse({ success: deleted }); @@ -352,6 +354,7 @@ export async function handleDeleteAllDevices(request: Request, env: Env, userId: user.securityStamp = generateUUID(); user.updatedAt = new Date().toISOString(); await storage.saveUser(user); + AuthService.invalidateUserCache(userId); notifyUserLogout(env, userId, null); return jsonResponse({ success: true, removedTrusted, removedSessions: removedSessions ?? 0, removedDevices }); } @@ -483,6 +486,7 @@ export async function handleDeactivateDevice( await storage.deleteRefreshTokensByDevice(userId, normalized); const deleted = await storage.deleteDevice(userId, normalized); if (deleted) { + AuthService.invalidateDeviceCache(userId, normalized); notifyUserLogout(env, userId, normalized); } return jsonResponse({ success: deleted }); diff --git a/src/handlers/identity.ts b/src/handlers/identity.ts index e8e1bf1..e4cdb4b 100644 --- a/src/handlers/identity.ts +++ b/src/handlers/identity.ts @@ -32,6 +32,17 @@ function resolveTotpSecret(userSecret: string | null): string | null { return null; } +async function resolveDeviceSession( + storage: StorageService, + userId: string, + deviceInfo: ReturnType +): Promise<{ identifier: string; sessionStamp: string } | null> { + if (!deviceInfo.deviceIdentifier) return null; + const existingDevice = await storage.getDevice(userId, deviceInfo.deviceIdentifier); + const sessionStamp = String(existingDevice?.sessionStamp || '').trim() || generateUUID(); + return { identifier: deviceInfo.deviceIdentifier, sessionStamp }; +} + function shouldUseWebSession(request: Request): boolean { return String(request.headers.get('X-NodeWarden-Web-Session') || '').trim() === '1'; } @@ -320,10 +331,7 @@ export async function handleToken(request: Request, env: Env): Promise } // Persist device only after successful password + (optional) 2FA verification. - const deviceSession = - deviceInfo.deviceIdentifier - ? { identifier: deviceInfo.deviceIdentifier, sessionStamp: generateUUID() } - : null; + const deviceSession = await resolveDeviceSession(storage, user.id, deviceInfo); if (deviceSession) { await storage.upsertDevice( user.id, @@ -413,10 +421,7 @@ export async function handleToken(request: Request, env: Env): Promise } // Persist device only after successful client credential verification. - const deviceSession = - deviceInfo.deviceIdentifier - ? { identifier: deviceInfo.deviceIdentifier, sessionStamp: generateUUID() } - : null; + const deviceSession = await resolveDeviceSession(storage, user.id, deviceInfo); if (deviceSession) { await storage.upsertDevice( user.id, diff --git a/src/services/auth.ts b/src/services/auth.ts index 755dd2f..0ad7487 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -32,6 +32,25 @@ export class AuthService { this.storage = new StorageService(env.DB); } + static invalidateUserCache(userId: string): void { + const normalizedUserId = String(userId || '').trim(); + if (!normalizedUserId) return; + AuthService.userCache.delete(normalizedUserId); + const prefix = `${normalizedUserId}:`; + for (const key of AuthService.deviceCache.keys()) { + if (key.startsWith(prefix)) { + AuthService.deviceCache.delete(key); + } + } + } + + static invalidateDeviceCache(userId: string, deviceId: string): void { + const normalizedUserId = String(userId || '').trim(); + const normalizedDeviceId = String(deviceId || '').trim(); + if (!normalizedUserId || !normalizedDeviceId) return; + AuthService.deviceCache.delete(`${normalizedUserId}:${normalizedDeviceId}`); + } + private readCachedUser(userId: string): User | null | undefined { const cached = AuthService.userCache.get(userId); if (!cached) return undefined;