spring-projects/spring-security · error · BadCredentialsException
Unable to authenticate the PublicKeyCredential
Error message
Unable to authenticate the PublicKeyCredential
What it means
WebAuthnAuthenticationFilter.attemptAuthentication() wraps any exception raised while reading the request body into a PublicKeyCredential<AuthenticatorAssertionResponse> and rethrows it as BadCredentialsException. It means the HTTP POST body was not a parseable/valid WebAuthn assertion credential (bad JSON, wrong shape, invalid fields).
Solutions
- Log the wrapped cause (ex) of the BadCredentialsException to see the actual deserialization error, then fix the payload accordingly.
- Ensure the client POSTs the PublicKeyCredential JSON (from navigator.credentials.get()) with Content-Type: application/json and the exact field names the converter expects (id, rawId, type, response{authenticatorData, clientDataJSON, signature, userHandle}).
- Verify the configured converter (PublicKeyCredentialHttpMessageConverter) is registered and the request goes through the WebAuthn filter path, not a custom controller.
Example fix
// before
fetch('/webauthn/authenticate', { method: 'POST', body: assertion })
// after
fetch('/webauthn/authenticate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: assertion.id, rawId: assertion.rawId, type: assertion.type,
response: { clientDataJSON: assertion.response.clientDataJSON,
authenticatorData: assertion.response.authenticatorData,
signature: assertion.response.signature,
userHandle: assertion.response.userHandle } })
}) Defensive patterns
Strategy: try-catch
Validate before calling
function isValidAssertionPayload(body) {
return body && typeof body === 'object' &&
typeof body.id === 'string' && typeof body.rawId === 'string' &&
body.type === 'public-key' && body.response &&
typeof body.response.clientDataJSON === 'string' &&
typeof body.response.authenticatorData === 'string' &&
typeof body.response.signature === 'string';
}
Type guard
public static boolean isPublicKeyCredentialMap(Object body) {
return body instanceof Map<?, ?> m
&& m.get("id") instanceof String
&& m.get("rawId") instanceof String
&& m.get("response") instanceof Map<?, ?> r
&& r.get("clientDataJSON") instanceof String;
}
Try / catch
try {
authenticationManager.authenticate(token);
} catch (BadCredentialsException e) {
logger.warn("Invalid WebAuthn assertion", e.getCause()); // inspect converter root cause
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
Prevention
- Always send navigator.credentials.get() results through PublicKeyCredential.toJSON() with Content-Type: application/json
- Keep registration and assertion payload shapes on their respective endpoints
- Log e.getCause() of BadCredentialsException to expose the true deserialization error
- Add an integration test that POSTs the exact JSON produced by the browser
When it happens
Trigger: POSTing a login/assertion request to the WebAuthn endpoint whose body fails HttpMessageConverter deserialization: malformed JSON, missing required fields, wrong content type, or a body that is not a PublicKeyCredential at all.
Common situations: Frontend sends navigator.credentials.get() output as plain JSON without wrapping/formatting expected by the converter; wrong Content-Type header; using the registration payload shape for the authentication endpoint; API client hand-crafting assertion JSON.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- <ex.getMessage()>
- Unable to authenticate the PublicKeyCredential. No…
- Credential with id already exists
- Unknown Callback
- Unused placeholders in template
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/96cba59461b5d51c.
Report an issue: GitHub.
Appendix: source
Thrown at webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationFilter.java:110
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
setAuthenticationFailureHandler(
new AuthenticationEntryPointFailureHandler(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));
setAuthenticationSuccessHandler(new HttpMessageConverterAuthenticationSuccessHandler());
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request);
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(PublicKeyCredential.class,
AuthenticatorAssertionResponse.class);
PublicKeyCredential<AuthenticatorAssertionResponse> publicKeyCredential = null;
try {
publicKeyCredential = (PublicKeyCredential<AuthenticatorAssertionResponse>) this.converter
.read(resolvableType, httpRequest, null);
}
catch (Exception ex) {
throw new BadCredentialsException("Unable to authenticate the PublicKeyCredential", ex);
}
PublicKeyCredentialRequestOptions requestOptions = this.requestOptionsRepository.load(request);
if (requestOptions == null) {
throw new BadCredentialsException(
"Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found.");
}
this.requestOptionsRepository.save(request, response, null);
RelyingPartyAuthenticationRequest authenticationRequest = new RelyingPartyAuthenticationRequest(requestOptions,
publicKeyCredential);
WebAuthnAuthenticationRequestToken token = new WebAuthnAuthenticationRequestToken(authenticationRequest);
return getAuthenticationManager().authenticate(token);
}
/**
* Sets the {@link GenericHttpMessageConverter} to use for writing
* {@code PublicKeyCredential<AuthenticatorAssertionResponse>} to the response. The
* default is @{code MappingJackson2HttpMessageConverter}
* @param converter the {@link GenericHttpMessageConverter} to use. Cannot be null.View on GitHub (pinned to 96852e8860)