{"record":{"id":"86cfec490420fee1","repo":"bitwarden/server","slug":"unable-to-complete-webauthn-registration","errorCode":null,"errorMessage":"Unable to complete WebAuthn registration.","messagePattern":"Unable to complete WebAuthn registration\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/Auth/Controllers/TwoFactorController.cs","lineNumber":361,"sourceCode":"    [ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly\n    public async Task<TwoFactorWebAuthnChallengeResponseModel> GetWebAuthnChallenge(\n        [FromBody] TwoFactorWebAuthnChallengeRequestModel model)\n    {\n        var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);\n        var options = await _startTwoFactorWebAuthnRegistrationCommand.StartTwoFactorWebAuthnRegistrationAsync(user);\n        return new TwoFactorWebAuthnChallengeResponseModel { Options = options };\n    }\n\n    [HttpPut(\"webauthn\")]\n    public async Task<TwoFactorWebAuthnUpdateResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnUpdateRequestModel model)\n    {\n        var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);\n\n        var success = await _completeTwoFactorWebAuthnRegistrationCommand.CompleteTwoFactorWebAuthnRegistrationAsync(\n            user, model.Id.Value, model.Name, model.DeviceResponse);\n        if (!success)\n        {\n            throw new BadRequestException(\"Unable to complete WebAuthn registration.\");\n        }\n\n        return new TwoFactorWebAuthnUpdateResponseModel(user);\n    }\n\n    [HttpPost(\"webauthn\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /webauthn instead.\")]\n    public async Task<TwoFactorWebAuthnUpdateResponseModel> PostWebAuthn([FromBody] TwoFactorWebAuthnUpdateRequestModel model)\n    {\n        return await PutWebAuthn(model);\n    }\n\n    [HttpDelete(\"webauthn\")]\n    public async Task<TwoFactorWebAuthnDeleteResponseModel> DeleteWebAuthn(\n        [FromBody] TwoFactorWebAuthnDeleteRequestModel model)\n    {\n        var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);\n","sourceCodeStart":343,"sourceCodeEnd":379,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Auth/Controllers/TwoFactorController.cs#L343-L379","documentation":"BadRequestException 'Unable to complete WebAuthn registration.' is thrown in PUT /webauthn (PutWebAuthn) when _completeTwoFactorWebAuthnRegistrationCommand.CompleteTwoFactorWebAuthnRegistrationAsync returns false. The command wraps fido2-net-lib attestation verification; false indicates the device response failed to verify against the stored challenge/options.","triggerScenarios":"PUT /api/users/two-factor/webauthn (TwoFactorController line 361) where model.DeviceResponse fails attestation/challenge verification, or the challenge (model.Id) does not match a pending registration started by the get/challenge endpoint.","commonSituations":"Challenge expired, the DeviceResponse was generated against a different challenge/origin/RP id, a non-passkey-capable authenticator, clock/origin mismatch (origin must match the RP id config), or tampered/truncated device response.","solutions":["Start a fresh challenge via the start-registration endpoint and submit the device response from the same flow.","Ensure the calling origin/RP id matches the server's WebAuthn/fido2 configuration.","Confirm the authenticator supports the requested parameters and the user actually completed the ceremony.","Pass model.Id (the pending challenge id) and model.DeviceResponse exactly as returned by the authenticator."],"exampleFix":"// before: stale challenge id\napi.put('/users/two-factor/webauthn', { id: oldChallengeId, deviceResponse })\n// after: start then immediately complete\nconst { id, options } = await api.post('/users/two-factor/webauthn/challenge');\nconst deviceResponse = await navigator.credentials.create({ publicKey: options });\napi.put('/users/two-factor/webauthn', { id, name, deviceResponse });","handlingStrategy":"validation","validationCode":"if (!model.id || !model.deviceResponse) throw new Error('Missing challenge id or device response');","typeGuard":"function isCompleteWebAuthnModel(m): m is WebAuthnUpdateModel { return !!m?.id && !!m?.deviceResponse && typeof m.userVerificationToken === 'string'; }","tryCatchPattern":"try { await api.put('/users/two-factor/webauthn', model); }\ncatch (e) {\n  if (e.response?.status === 400 && /WebAuthn registration/.test(e.response.data?.message)) {\n    const challenge = await api.post('/users/two-factor/webauthn/challenge');\n    throw new RetryableError('Restart the WebAuthn flow with a fresh challenge', challenge);\n  }\n  throw e;\n}","preventionTips":["Complete the WebAuthn ceremony immediately after starting it.","Match the origin/RP id to server config.","Pass the device response untouched."],"tags":["two-factor","webauthn","fido2","registration"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}