mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: refactor authentication flow and improve token verification process
This commit is contained in:
+1
-2
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 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 ensureStorageSchema(this.db);
|
||||||
|
await saveConfigValue(this.db, STORAGE_SCHEMA_VERSION_KEY, STORAGE_SCHEMA_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
StorageService.schemaVerified = true;
|
StorageService.schemaVerified = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user