From 14dff8ee6a59b741d86a42b25451116b120ac404 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Fri, 10 Jul 2026 13:00:32 +0800 Subject: [PATCH] feat: disable new-device verification and update related logic across services --- migrations/0001_init.sql | 2 +- src/handlers/accounts.ts | 81 ++++++++++++------------------- src/services/backup-import.ts | 2 +- src/services/storage-schema.ts | 4 +- src/services/storage-user-repo.ts | 2 +- src/utils/profile-response.ts | 4 +- 6 files changed, 39 insertions(+), 56 deletions(-) diff --git a/migrations/0001_init.sql b/migrations/0001_init.sql index 4c9c8ef..5416779 100644 --- a/migrations/0001_init.sql +++ b/migrations/0001_init.sql @@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS users ( security_stamp TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', status TEXT NOT NULL DEFAULT 'active', - verify_devices INTEGER NOT NULL DEFAULT 1, + verify_devices INTEGER NOT NULL DEFAULT 0, totp_secret TEXT, totp_recovery_code TEXT, api_key TEXT, diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index c40e27f..6310908 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -352,7 +352,7 @@ export async function handleRegister(request: Request, env: Env): Promise { const storage = new StorageService(env.DB); const auth = new AuthService(env); const user = await storage.getUserById(userId); if (!user) return errorResponse('User not found', 404); - let body: { - secret?: string; - masterPasswordHash?: string; - verifyDevices?: boolean; - VerifyDevices?: boolean; - }; - try { - body = await request.json(); - } catch { - return errorResponse('Invalid JSON', 400); - } - - const verifyDevices = typeof body.verifyDevices === 'boolean' ? body.verifyDevices : body.VerifyDevices; - if (typeof verifyDevices !== 'boolean') { - return errorResponse('verifyDevices must be true or false', 400); - } - - const verified = await verifyUserSecret(auth, user, body.secret || body.masterPasswordHash); - if (!verified) { - return errorResponse('User verification failed.', 400); - } - - user.verifyDevices = verifyDevices; - user.updatedAt = new Date().toISOString(); - await storage.saveUser(user); + // Log the attempt for audit purposes, but do not change state. await writeAuditEvent(storage, { actorUserId: user.id, - action: 'account.verify_devices.update', + action: 'account.verify_devices.update.rejected', category: 'security', - level: 'security', + level: 'info', targetType: 'user', targetId: user.id, metadata: { - verifyDevices: user.verifyDevices, + reason: 'new-device verification is not supported (no email delivery channel)', ...auditRequestMetadata(request), }, }); - return new Response(null, { status: 200 }); + return errorResponse('New device verification is not available on this server. Enable TOTP or WebAuthn two-factor authentication instead.', 400); } // GET /api/accounts/keys @@ -819,13 +799,16 @@ function yubiKeyResponse(user: User): Record { }; } -function deviceVerificationSettingsResponse(user: User): Record { - const enabled = user.verifyDevices !== false; +// New-device verification requires an email delivery channel to send OTP +// challenges to unknown devices. NodeWarden does not integrate with an email +// provider, so this feature is intentionally unavailable. The settings +// response always reports disabled regardless of any legacy DB value. +function deviceVerificationSettingsResponse(_user: User): Record { return { - Enabled: enabled, - enabled, - VerifyDevices: enabled, - verifyDevices: enabled, + Enabled: false, + enabled: false, + VerifyDevices: false, + verifyDevices: false, Object: 'deviceVerificationSettings', object: 'deviceVerificationSettings', }; @@ -915,9 +898,10 @@ export async function handleGetDeviceVerificationSettings(request: Request, env: } // PUT/POST /api/two-factor/device-verification-settings +// New-device verification is not supported (no email delivery channel). +// Reject any attempt to enable it; always return disabled state. export async function handlePutDeviceVerificationSettings(request: Request, env: Env, userId: string): Promise { const storage = new StorageService(env.DB); - const auth = new AuthService(env); const user = await storage.getUserById(userId); if (!user) return errorResponse('User not found', 404); @@ -929,31 +913,28 @@ export async function handlePutDeviceVerificationSettings(request: Request, env: } const rawEnabled = body.enabled ?? body.Enabled ?? body.verifyDevices ?? body.VerifyDevices; - if (typeof rawEnabled !== 'boolean') { - return errorResponse('enabled must be true or false', 400); - } - const secret = readBodyString(body, ['masterPasswordHash', 'MasterPasswordHash', 'secret', 'Secret']); - const verified = await verifyUserSecret(auth, user, secret); - if (!verified) return errorResponse('User verification failed.', 400); - - user.verifyDevices = rawEnabled; - user.updatedAt = new Date().toISOString(); - await storage.saveUser(user); + // Log the attempt for audit purposes — never change state. await writeAuditEvent(storage, { actorUserId: user.id, - action: 'account.verify_devices.update', + action: 'account.verify_devices.update.rejected', category: 'security', - level: 'security', + level: 'info', targetType: 'user', targetId: user.id, metadata: { - verifyDevices: user.verifyDevices, + requested: rawEnabled, + reason: 'new-device verification is not supported (no email delivery channel)', source: 'two-factor.device-verification-settings', ...auditRequestMetadata(request), }, }); + if (rawEnabled === true) { + return errorResponse('New device verification is not available on this server. Enable TOTP or WebAuthn two-factor authentication instead.', 400); + } + + // Setting to false is the only supported state — return it. return jsonResponse(deviceVerificationSettingsResponse(user)); } diff --git a/src/services/backup-import.ts b/src/services/backup-import.ts index 66d2be2..c223d9a 100644 --- a/src/services/backup-import.ts +++ b/src/services/backup-import.ts @@ -301,7 +301,7 @@ async function importPreparedBackupRows(db: D1Database, payload: BackupPayload[' config: await prepareImportedConfigRows(env, payload.config || [], payload.users || []), users: cloneRows(payload.users || []).map((row) => ({ ...row, - verify_devices: row.verify_devices ?? 1, + verify_devices: row.verify_devices ?? 0, yubikey_nfc: row.yubikey_nfc ?? 0, })), domain_settings: cloneRows(payload.domain_settings || []), diff --git a/src/services/storage-schema.ts b/src/services/storage-schema.ts index 140e316..b7a8096 100644 --- a/src/services/storage-schema.ts +++ b/src/services/storage-schema.ts @@ -14,11 +14,11 @@ const SCHEMA_STATEMENTS: readonly string[] = [ 'id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, master_password_hint TEXT, master_password_hash TEXT NOT NULL, ' + 'key TEXT NOT NULL, private_key TEXT, public_key TEXT, kdf_type INTEGER NOT NULL, ' + 'kdf_iterations INTEGER NOT NULL, kdf_memory INTEGER, kdf_parallelism INTEGER, ' + - 'security_stamp TEXT NOT NULL, role TEXT NOT NULL DEFAULT \'user\', status TEXT NOT NULL DEFAULT \'active\', verify_devices INTEGER NOT NULL DEFAULT 1, totp_secret TEXT, totp_recovery_code TEXT, yubikey_key1 TEXT, yubikey_key2 TEXT, yubikey_key3 TEXT, yubikey_key4 TEXT, yubikey_key5 TEXT, yubikey_nfc INTEGER NOT NULL DEFAULT 0, api_key TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)', + 'security_stamp TEXT NOT NULL, role TEXT NOT NULL DEFAULT \'user\', status TEXT NOT NULL DEFAULT \'active\', verify_devices INTEGER NOT NULL DEFAULT 0, totp_secret TEXT, totp_recovery_code TEXT, yubikey_key1 TEXT, yubikey_key2 TEXT, yubikey_key3 TEXT, yubikey_key4 TEXT, yubikey_key5 TEXT, yubikey_nfc INTEGER NOT NULL DEFAULT 0, api_key TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)', 'ALTER TABLE users ADD COLUMN master_password_hint TEXT', 'ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT \'user\'', 'ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT \'active\'', - 'ALTER TABLE users ADD COLUMN verify_devices INTEGER NOT NULL DEFAULT 1', + 'ALTER TABLE users ADD COLUMN verify_devices INTEGER NOT NULL DEFAULT 0', 'ALTER TABLE users ADD COLUMN totp_secret TEXT', 'ALTER TABLE users ADD COLUMN totp_recovery_code TEXT', 'ALTER TABLE users ADD COLUMN yubikey_key1 TEXT', diff --git a/src/services/storage-user-repo.ts b/src/services/storage-user-repo.ts index ed7e9ee..e91380d 100644 --- a/src/services/storage-user-repo.ts +++ b/src/services/storage-user-repo.ts @@ -23,7 +23,7 @@ function mapUserRow(row: any): User { securityStamp: row.security_stamp, role: row.role === 'admin' ? 'admin' : 'user', status: row.status === 'banned' ? 'banned' : 'active', - verifyDevices: row.verify_devices == null ? true : !!row.verify_devices, + verifyDevices: row.verify_devices == null ? false : !!row.verify_devices, totpSecret: row.totp_secret ?? null, totpRecoveryCode: row.totp_recovery_code ?? null, yubikeyKey1: row.yubikey_key1 ?? null, diff --git a/src/utils/profile-response.ts b/src/utils/profile-response.ts index 5329f99..51a3290 100644 --- a/src/utils/profile-response.ts +++ b/src/utils/profile-response.ts @@ -30,7 +30,9 @@ export function buildProfileResponse(user: User, env?: Env): ProfileResponse { forcePasswordReset: false, avatarColor: null, creationDate: user.createdAt, - verifyDevices: user.verifyDevices !== false, + // New-device verification is not supported without an email delivery channel. + // Always report disabled so clients do not present a false security posture. + verifyDevices: false, role: user.role, status: user.status, object: 'profile',