bitwarden/server · error · BadRequestException
Unable to complete WebAuthn registration.
Error message
Unable to complete WebAuthn registration.
What it means
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.
Source
Thrown at src/Api/Auth/Controllers/TwoFactorController.cs:361
[ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly
public async Task<TwoFactorWebAuthnChallengeResponseModel> GetWebAuthnChallenge(
[FromBody] TwoFactorWebAuthnChallengeRequestModel model)
{
var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);
var options = await _startTwoFactorWebAuthnRegistrationCommand.StartTwoFactorWebAuthnRegistrationAsync(user);
return new TwoFactorWebAuthnChallengeResponseModel { Options = options };
}
[HttpPut("webauthn")]
public async Task<TwoFactorWebAuthnUpdateResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnUpdateRequestModel model)
{
var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);
var success = await _completeTwoFactorWebAuthnRegistrationCommand.CompleteTwoFactorWebAuthnRegistrationAsync(
user, model.Id.Value, model.Name, model.DeviceResponse);
if (!success)
{
throw new BadRequestException("Unable to complete WebAuthn registration.");
}
return new TwoFactorWebAuthnUpdateResponseModel(user);
}
[HttpPost("webauthn")]
[Obsolete("This endpoint is deprecated. Use PUT /webauthn instead.")]
public async Task<TwoFactorWebAuthnUpdateResponseModel> PostWebAuthn([FromBody] TwoFactorWebAuthnUpdateRequestModel model)
{
return await PutWebAuthn(model);
}
[HttpDelete("webauthn")]
public async Task<TwoFactorWebAuthnDeleteResponseModel> DeleteWebAuthn(
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
{
var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);
View on GitHub (pinned to e93b962371)
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.
Example fix
// before: stale challenge id
api.put('/users/two-factor/webauthn', { id: oldChallengeId, deviceResponse })
// after: start then immediately complete
const { id, options } = await api.post('/users/two-factor/webauthn/challenge');
const deviceResponse = await navigator.credentials.create({ publicKey: options });
api.put('/users/two-factor/webauthn', { id, name, deviceResponse }); Defensive patterns
Strategy: validation
Validate before calling
if (!model.id || !model.deviceResponse) throw new Error('Missing challenge id or device response'); Type guard
function isCompleteWebAuthnModel(m): m is WebAuthnUpdateModel { return !!m?.id && !!m?.deviceResponse && typeof m.userVerificationToken === 'string'; } Try / catch
try { await api.put('/users/two-factor/webauthn', model); }
catch (e) {
if (e.response?.status === 400 && /WebAuthn registration/.test(e.response.data?.message)) {
const challenge = await api.post('/users/two-factor/webauthn/challenge');
throw new RetryableError('Restart the WebAuthn flow with a fresh challenge', challenge);
}
throw e;
} Prevention
- Complete the WebAuthn ceremony immediately after starting it.
- Match the origin/RP id to server config.
- Pass the device response untouched.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unable to complete WebAuthn registration.
- Unable to delete WebAuthn credential.
- Cannot edit item. Update to the latest version of Bitwarden
- Unauthorized.
- Resource not found.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/86cfec490420fee1.
Report an issue: GitHub.