feat: refactor authentication flow and improve token verification process

This commit is contained in:
shuaiplus
2026-03-18 00:24:45 +08:00
parent 011fe15aae
commit 3f7ca52983
4 changed files with 28 additions and 17 deletions
+1 -2
View File
@@ -3,7 +3,7 @@ import { NotificationsHub } from './durable/notifications-hub';
import { handleRequest } from './router'; import { handleRequest } from './router';
import { StorageService } from './services/storage'; import { StorageService } from './services/storage';
import { applyCors, jsonResponse } from './utils/response'; import { applyCors, jsonResponse } from './utils/response';
import { runScheduledBackupIfDue, seedDefaultBackupSettings } from './handlers/backup'; import { runScheduledBackupIfDue } from './handlers/backup';
import { buildWebBootstrapResponse } from './router-public'; import { buildWebBootstrapResponse } from './router-public';
let dbInitialized = false; let dbInitialized = false;
@@ -59,7 +59,6 @@ async function ensureDatabaseInitialized(env: Env): Promise<void> {
dbInitPromise = (async () => { dbInitPromise = (async () => {
const storage = new StorageService(env.DB); const storage = new StorageService(env.DB);
await storage.initializeDatabase(); await storage.initializeDatabase();
await seedDefaultBackupSettings(env);
dbInitialized = true; dbInitialized = true;
dbInitError = null; dbInitError = null;
})() })()
+3 -8
View File
@@ -1,6 +1,5 @@
import { DEFAULT_DEV_SECRET, Env } from './types'; import { DEFAULT_DEV_SECRET, Env } from './types';
import { AuthService } from './services/auth'; import { AuthService } from './services/auth';
import { StorageService } from './services/storage';
import { RateLimitService, getClientIdentifier } from './services/ratelimit'; import { RateLimitService, getClientIdentifier } from './services/ratelimit';
import { handleCors, errorResponse } from './utils/response'; import { handleCors, errorResponse } from './utils/response';
import { LIMITS } from './config/limits'; import { LIMITS } from './config/limits';
@@ -96,10 +95,11 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
const auth = new AuthService(env); const auth = new AuthService(env);
const authHeader = request.headers.get('Authorization'); const authHeader = request.headers.get('Authorization');
const payload = await auth.verifyAccessToken(authHeader); const verified = await auth.verifyAccessTokenWithUser(authHeader);
if (!payload) { if (!verified) {
return errorResponse('Unauthorized', 401); return errorResponse('Unauthorized', 401);
} }
const { payload, user: currentUser } = verified;
const actingDeviceId = String(payload.did || '').trim(); const actingDeviceId = String(payload.did || '').trim();
if (actingDeviceId) { if (actingDeviceId) {
@@ -109,11 +109,6 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
} }
const userId = payload.sub; const userId = payload.sub;
const storage = new StorageService(env.DB);
const currentUser = await storage.getUserById(userId);
if (!currentUser) {
return errorResponse('Unauthorized', 401);
}
if (currentUser.status !== 'active') { if (currentUser.status !== 'active') {
return errorResponse('Account is disabled', 403); return errorResponse('Account is disabled', 403);
} }
+14 -5
View File
@@ -7,6 +7,11 @@ import { StorageService } from './storage';
// This second layer only needs to be non-trivial, not expensive. // This second layer only needs to be non-trivial, not expensive.
const SERVER_HASH_ITERATIONS = 100_000; const SERVER_HASH_ITERATIONS = 100_000;
export interface VerifiedAccessContext {
payload: JWTPayload;
user: User;
}
export class AuthService { export class AuthService {
private storage: StorageService; private storage: StorageService;
@@ -81,8 +86,7 @@ export class AuthService {
return token; return token;
} }
// Verify access token from Authorization header async verifyAccessTokenWithUser(authHeader: string | null): Promise<VerifiedAccessContext | null> {
async verifyAccessToken(authHeader: string | null): Promise<JWTPayload | null> {
if (!authHeader) return null; if (!authHeader) return null;
const parts = authHeader.split(' '); const parts = authHeader.split(' ');
@@ -93,12 +97,11 @@ export class AuthService {
const payload = await verifyJWT(parts[1], this.env.JWT_SECRET); const payload = await verifyJWT(parts[1], this.env.JWT_SECRET);
if (!payload) return null; if (!payload) return null;
// Verify security stamp - ensures token is invalidated after password change
const user = await this.storage.getUserById(payload.sub); const user = await this.storage.getUserById(payload.sub);
if (!user) return null; if (!user) return null;
if (payload.sstamp !== user.securityStamp) { if (payload.sstamp !== user.securityStamp) {
return null; // Token was issued before password change return null;
} }
if (payload.did) { if (payload.did) {
@@ -107,7 +110,13 @@ export class AuthService {
if (!payload.dstamp || payload.dstamp !== device.sessionStamp) return null; if (!payload.dstamp || payload.dstamp !== device.sessionStamp) return null;
} }
return payload; return { payload, user };
}
// Verify access token from Authorization header
async verifyAccessToken(authHeader: string | null): Promise<JWTPayload | null> {
const verified = await this.verifyAccessTokenWithUser(authHeader);
return verified?.payload ?? null;
} }
// Refresh access token // Refresh access token
+9 -1
View File
@@ -101,6 +101,8 @@ import {
} from './storage-revision-repo'; } from './storage-revision-repo';
const TWO_FACTOR_REMEMBER_TTL_MS = 30 * 24 * 60 * 60 * 1000; const TWO_FACTOR_REMEMBER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const STORAGE_SCHEMA_VERSION_KEY = 'schema.version';
const STORAGE_SCHEMA_VERSION = '2026-03-18.1';
// D1-backed storage. // D1-backed storage.
// Contract: // Contract:
@@ -171,7 +173,13 @@ export class StorageService {
// - Keep statements idempotent so updates are safe. // - Keep statements idempotent so updates are safe.
async initializeDatabase(): Promise<void> { async initializeDatabase(): Promise<void> {
if (StorageService.schemaVerified) return; if (StorageService.schemaVerified) return;
await ensureStorageSchema(this.db);
await this.db.prepare('CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run();
const schemaVersion = await getStoredConfigValue(this.db, STORAGE_SCHEMA_VERSION_KEY);
if (schemaVersion !== STORAGE_SCHEMA_VERSION) {
await ensureStorageSchema(this.db);
await saveConfigValue(this.db, STORAGE_SCHEMA_VERSION_KEY, STORAGE_SCHEMA_VERSION);
}
StorageService.schemaVerified = true; StorageService.schemaVerified = true;
} }