fix(server): tidy passkey quirks after the DI fold

This commit is contained in:
jubnl
2026-08-02 10:28:22 +02:00
parent 74be789f22
commit c814653f4e
7 changed files with 122 additions and 31 deletions
+2
View File
@@ -18,6 +18,7 @@ import {
passkeyRegisterVerifyRequestSchema,
passkeyLoginVerifyRequestSchema,
passkeyRenameRequestSchema,
passkeyDeleteRequestSchema,
} from '@trek/shared';
/**
@@ -46,3 +47,4 @@ export class PasskeyRegisterOptionsDto extends createZodDto(passkeyRegisterOptio
export class PasskeyRegisterVerifyDto extends createZodDto(passkeyRegisterVerifyRequestSchema) {}
export class PasskeyLoginVerifyDto extends createZodDto(passkeyLoginVerifyRequestSchema) {}
export class PasskeyRenameDto extends createZodDto(passkeyRenameRequestSchema) {}
export class PasskeyDeleteDto extends createZodDto(passkeyDeleteRequestSchema) {}
+2 -2
View File
@@ -8,7 +8,7 @@ import { setAuthCookie } from '../../services/cookie';
import { getClientIp } from '../audit/client-ip';
import { AuditService } from '../audit/audit.service';
import { PasskeyService } from './passkey.service';
import { PasskeyRegisterOptionsDto, PasskeyRegisterVerifyDto, PasskeyLoginVerifyDto, PasskeyRenameDto } from './auth.dto';
import { PasskeyRegisterOptionsDto, PasskeyRegisterVerifyDto, PasskeyLoginVerifyDto, PasskeyRenameDto, PasskeyDeleteDto } from './auth.dto';
import type { User } from '../../types';
const WINDOW = 15 * 60 * 1000;
@@ -110,7 +110,7 @@ export class PasskeyController {
@Delete('credentials/:id')
@UseGuards(JwtAuthGuard)
remove(@CurrentUser() user: User, @Param('id') id: string, @Body() body: { password?: string }, @Req() req: Request) {
remove(@CurrentUser() user: User, @Param('id') id: string, @Body() body: PasskeyDeleteDto, @Req() req: Request) {
this.limit('login', req, 5);
const result = this.passkeys.deletePasskey(user.id, id, body?.password);
if (result.error) throw new HttpException({ error: result.error }, result.status!);
+31 -19
View File
@@ -32,6 +32,11 @@ const NOT_CONFIGURED = { error: 'Passkey login is not configured for this server
// used to tell "no such credential" apart from "bad signature" (CWE-203).
const AUTH_FAILED = { error: 'Authentication failed', status: 401 } as const;
// Reference-compared sentinel (oidc invite_exhausted precedent): thrown inside
// the register transaction to keep the duplicate 409 distinct from the generic
// insert-failure 400 without string-matching SQLite errors.
const DUPLICATE_CREDENTIAL = new Error('duplicate credential');
interface CredentialRow {
id: number;
user_id: number;
@@ -205,27 +210,34 @@ export class PasskeyService {
// from the raw client payload.
const { credential, credentialDeviceType, credentialBackedUp, aaguid } = verification.registrationInfo;
if (this.db.get('SELECT id FROM webauthn_credentials WHERE credential_id = ?', credential.id)) {
return { error: 'This passkey is already registered.', status: 409 };
}
const name = sanitizeName(body?.name) || defaultCredentialName(credentialDeviceType);
// Duplicate check + INSERT in one transaction so the UNIQUE race can't slip
// between them; the sentinel keeps the legacy 409-vs-400 split intact.
try {
this.db.run(
`INSERT INTO webauthn_credentials
(user_id, credential_id, public_key, counter, transports, device_type, backed_up, name, aaguid, last_used_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
userId,
credential.id,
Buffer.from(credential.publicKey),
credential.counter ?? 0,
credential.transports ? JSON.stringify(credential.transports) : null,
credentialDeviceType ?? null,
credentialBackedUp ? 1 : 0,
name,
aaguid ?? null,
);
} catch {
this.db.transaction((conn) => {
if (conn.prepare('SELECT id FROM webauthn_credentials WHERE credential_id = ?').get(credential.id)) {
throw DUPLICATE_CREDENTIAL;
}
conn.prepare(
`INSERT INTO webauthn_credentials
(user_id, credential_id, public_key, counter, transports, device_type, backed_up, name, aaguid, last_used_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
).run(
userId,
credential.id,
Buffer.from(credential.publicKey),
credential.counter ?? 0,
credential.transports ? JSON.stringify(credential.transports) : null,
credentialDeviceType ?? null,
credentialBackedUp ? 1 : 0,
name,
aaguid ?? null,
);
});
} catch (err) {
if (err === DUPLICATE_CREDENTIAL) {
return { error: 'This passkey is already registered.', status: 409 };
}
return { error: 'Could not register this passkey.', status: 400 };
}