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

  1. Log the wrapped cause (ex) of the BadCredentialsException to see the actual deserialization error, then fix the payload accordingly.
  2. 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}).
  3. 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

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

Related errors


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)