feat(devices): add functionality to delete all authorized devices

This commit is contained in:
shuaiplus
2026-03-08 22:12:01 +08:00
parent 61dac98a12
commit d48e6b6ce5
10 changed files with 230 additions and 41 deletions
+20
View File
@@ -2,6 +2,7 @@ import { Env } from '../types';
import { StorageService } from '../services/storage';
import { errorResponse, jsonResponse } from '../utils/response';
import { readKnownDeviceProbe } from '../utils/device';
import { generateUUID } from '../utils/uuid';
// GET /api/devices/knowndevice
// Compatible with Bitwarden/Vaultwarden behavior:
@@ -133,10 +134,29 @@ export async function handleDeleteDevice(
const storage = new StorageService(env.DB);
await storage.deleteTrustedTwoFactorTokensByDevice(userId, normalized);
await storage.deleteRefreshTokensByDevice(userId, normalized);
const deleted = await storage.deleteDevice(userId, normalized);
return jsonResponse({ success: deleted });
}
// DELETE /api/devices
export async function handleDeleteAllDevices(request: Request, env: Env, userId: string): Promise<Response> {
void request;
const storage = new StorageService(env.DB);
const user = await storage.getUserById(userId);
if (!user) return errorResponse('User not found', 404);
const [removedTrusted, removedSessions, removedDevices] = await Promise.all([
storage.deleteTrustedTwoFactorTokensByUserId(userId),
storage.deleteRefreshTokensByUserId(userId),
storage.deleteDevicesByUserId(userId),
]);
user.securityStamp = generateUUID();
user.updatedAt = new Date().toISOString();
await storage.saveUser(user);
return jsonResponse({ success: true, removedTrusted, removedSessions: removedSessions ?? 0, removedDevices });
}
// PUT /api/devices/identifier/{deviceIdentifier}/token
// Bitwarden mobile reports push token updates to this endpoint.
// NodeWarden does not implement push notifications, so accept and no-op.
+17 -6
View File
@@ -8,6 +8,7 @@ import { isTotpEnabled, verifyTotpToken } from '../utils/totp';
import { createRefreshToken } from '../utils/jwt';
import { readAuthRequestDeviceInfo } from '../utils/device';
import { createRecoveryCode, recoveryCodeEquals } from '../utils/recovery-code';
import { generateUUID } from '../utils/uuid';
import { issueSendAccessToken } from './sends';
import {
buildAccountKeys,
@@ -227,15 +228,25 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
}
// Persist device only after successful password + (optional) 2FA verification.
if (deviceInfo.deviceIdentifier) {
await storage.upsertDevice(user.id, deviceInfo.deviceIdentifier, deviceInfo.deviceName, deviceInfo.deviceType);
const deviceSession =
deviceInfo.deviceIdentifier
? { identifier: deviceInfo.deviceIdentifier, sessionStamp: generateUUID() }
: null;
if (deviceSession) {
await storage.upsertDevice(
user.id,
deviceSession.identifier,
deviceInfo.deviceName,
deviceInfo.deviceType,
deviceSession.sessionStamp
);
}
// Successful login - clear failed attempts
await rateLimit.clearLoginAttempts(loginIdentifier);
const accessToken = await auth.generateAccessToken(user);
const refreshToken = await auth.generateRefreshToken(user.id);
const accessToken = await auth.generateAccessToken(user, deviceSession);
const refreshToken = await auth.generateRefreshToken(user.id, deviceSession);
const response: TokenResponse = {
access_token: accessToken,
@@ -346,8 +357,8 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
Date.now() + LIMITS.auth.refreshTokenOverlapGraceMs
);
const { accessToken, user } = result;
const newRefreshToken = await auth.generateRefreshToken(user.id);
const { accessToken, user, device } = result;
const newRefreshToken = await auth.generateRefreshToken(user.id, device);
const response: TokenResponse = {
access_token: accessToken,