An authentication challenge is a fresh random value generated by the backend for one WebAuthn login ceremony. The client signs it with the credential’s private key, and the backend verifies the signature with the registered public key. Matching the response to the stored challenge helps prevent replay and phishing attacks. section 15.3 section 15.4
A practical authentication flow uses these steps:
- Identify the user and retrieve the credential ID associated with the registered public key. section 15.3
- Generate a 32-byte unpredictable value with a secure random generator. section 15.3 section 15.3.1
- Store the challenge for that credential and return it to the client with the allowed credential. section 15.3
- Have the device sign the challenge, then verify the returned signature against the stored challenge and public key. section 15.3 section 15.4
Why freshness matters
A server that accepts a signed response without checking it against the current stored challenge could accept an old response again. Requiring the original challenge from the correct domain ties the signature to the current ceremony. section 15.4
Listing 15.6 The challenge endpoint
@PostMapping("/authenticate/challenge")
public Map<String, Object> getAuthenticationChallenge(
@RequestBody Map<String, String> request) {
String username = request.get("username");
String storedCredentialId = getUser(username); #1
byte[] challenge = new byte[32]; #2
new SecureRandom().nextBytes(challenge);
store.put(storedCredentialId, challenge); #3
Map<String, Object> allowCredential
= new HashMap<>(); #4
allowCredential.put("type", "public-key");
allowCredential.put("id", storedCredentialId);
var encChallange = Base64.getEncoder()
.encodeToString(challenge); #5
Map<String, Object> response = new HashMap<>(); #6
response.put("challenge", encChallange );
response.put("allowCredentials", List.of(allowCredential));
return response;
}
The challenge endpoint shows the core implementation: identify the credential, generate 32 random bytes, store them, Base64-encode the value, and return it to the client. section 15.3
Listing 15.7 The authentication endpoint
@PostMapping("/authenticate")
public String authenticate(
@RequestBody AuthenticationResponse response) {
String credentialId = response.getId(); #1
byte[] signature = Base64.getDecoder()
.decode(response.getResponse().getSignature());
byte[] authenticatorData = Base64.getDecoder()
.decode(response.getResponse().getAuthenticatorData());
byte[] clientDataJSON = Base64.getDecoder()
.decode(response.getResponse().getClientDataJSON());
byte[] storedPublicKey #2
= getRegiteredPublicKey(credentialId); #2
byte[] originalChallenge #2
= store.get(credentialId); #2
var succss = verifySignatureAndAuthenticate( #3
storedPublicKey,
orginalChallenge,
signature);
if (success) return "Authentication successful";
else throw new UnsuccesfulAuthenticationException();
}
The authentication endpoint demonstrates retrieving the stored challenge and public key before verifying the signed response. section 15.3

