spring-projects/spring-security · error · BadCredentialsException

Unable to authenticate the PublicKeyCredential. No…

Error message

Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found.

What it means

WebAuthnAuthenticationFilter.attemptAuthentication() loads the stored PublicKeyCredentialRequestOptions for the current request before validating the credential; if the repository returns null it throws this BadCredentialsException. It means the server has no record of a challenge for this authentication attempt.

Solutions

  1. Ensure the client first calls the endpoint that generates and stores PublicKeyCredentialRequestOptions, then immediately POSTs the assertion.
  2. Use a shared/persistent PublicKeyCredentialRequestOptionsRepository (e.g. HTTP-session or database-backed) across nodes instead of per-instance in-memory storage.
  3. Check the request carries the identifying cookie/session data so load() can find the stored options; re-obtain a fresh challenge after every consumed attempt.

Example fix

// before
@Bean
PublicKeyCredentialRequestOptionsRepository requestOptionsRepository() {
    return new InMemoryPublicKeyCredentialRequestOptionsRepository(); // lost on restart / across nodes
}
// after
@Bean
PublicKeyCredentialRequestOptionsRepository requestOptionsRepository() {
    return new HttpSessionPublicKeyCredentialRequestOptionsRepository(); // shared per user session
}
Defensive patterns

Strategy: try-catch

Validate before calling

// client: fetch assertion options first and fail fast if unavailable
const opts = await fetch('/webauthn/options', { credentials: 'include' });
if (!opts.ok) throw new Error('No challenge issued — cannot authenticate');

Try / catch

try {
    authenticationManager.authenticate(token);
} catch (BadCredentialsException e) {
    if (e.getMessage().contains("No PublicKeyCredentialRequestOptions")) {
        throw new ResponseStatusException(HttpStatus.CONFLICT, "Challenge expired; request a new one");
    }
    throw e;
}

Prevention

When it happens

Trigger: POSTing an assertion when requestOptionsRepository.load(request) returns null: the challenge was never created, already consumed (saved as null after a prior attempt), expired/cleared from the repository, or the request hits a different server/session than the one that issued the challenge.

Common situations: Application restarted between challenge issuance and assertion (in-memory repository lost); load-balanced deployment without sticky sessions/shared repository; client retries the POST and the filter's save(request, response, null) already cleared the options; missing step that creates the options (WebAuthnRequestsURLEndpoint) beforehand.

Understand the failure class

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/0b036d8c778535b7. Report an issue: GitHub.

Appendix: source

Thrown at webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationFilter.java:114

	}

	@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.
	 * @deprecated use {@link #setConverter(SmartHttpMessageConverter)}
	 */
	@Deprecated(forRemoval = true, since = "7.0")
	public void setConverter(GenericHttpMessageConverter<Object> converter) {

View on GitHub (pinned to 96852e8860)