From cecb24dd7869be671ecdc5d410faefe71c5a2853 Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Thu, 20 Aug 2026 09:01:52 +0200 Subject: [PATCH] Harden passkey management --- project.inlang/messages/de.json | 1 + project.inlang/messages/en.json | 1 + .../add-passkey-form/add-passkey-form.svelte | 11 +++++++++++ src/routes/api/auth/challenge/+server.ts | 6 ++++++ 4 files changed, 19 insertions(+) diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index abc42cc..122a2a9 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -798,6 +798,7 @@ "action": "Hinzufügen", "success": "Passkey hinzugefügt", "error": "Passkey konnte nicht hinzugefügt werden", + "errorDeleteSuccess": "Teilweise hinzugefügter Passkey gelöscht", "maxReached": { "title": "Maximum erreicht", "description": "Du hast bereits die maximale Anzahl von Passkeys für Deinen Account erreicht" diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index bee80a6..d64adfd 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -806,6 +806,7 @@ "action": "Add Passkey", "success": "Passkey added", "error": "Passkey could not be added", + "errorDeleteSuccess": "Partially added Passkey deleted", "maxReached": { "title": "Maximum reached", "description": "You've reached the maximum amount of Passkeys for this account" diff --git a/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte b/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte index 7ee624c..ab55ad4 100644 --- a/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte +++ b/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte @@ -167,6 +167,17 @@ } catch (error) { console.error("Unable to add passkey", error); toast.error(m["account.passkeys.add.error"]()); + + // Try to delete the partially added passkey if it was created + await fetch(`/api/auth/passkeys/${$formData.id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ tenantId }), + credentials: "same-origin", + }); + toast.info(m["account.passkeys.add.errorDeleteSuccess"]()); } finally { isSubmitting = false; } diff --git a/src/routes/api/auth/challenge/+server.ts b/src/routes/api/auth/challenge/+server.ts index 9a37e8f..db1a7b1 100644 --- a/src/routes/api/auth/challenge/+server.ts +++ b/src/routes/api/auth/challenge/+server.ts @@ -229,10 +229,12 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => { // Try to get user by email - but don't fail if not found let user = null; let isRegistration = false; + let passkeysLength = 0; try { user = await UserService.getUserByEmail(requestEmail); const passkeys = await UserService.getUserPasskeys(user.id); + passkeysLength = passkeys.length; if (passkeys.length === 0) { isRegistration = true; // User exists but has no passphrase - must register } @@ -279,6 +281,10 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => { path: "/", maxAge: 60 * 5, // 5 minutes }); + } else { + if (passkeysLength >= 3) { + throw new BackendError("Maximum number of passkeys reached", 400); + } } let allowCredentials: Array<{