{"record":{"id":"af4139c263889826","repo":"spring-projects/spring-security","slug":"digestauthenticationfilter-noncenottwotokens","errorCode":"DigestAuthenticationFilter.nonceNotTwoTokens","errorMessage":"Nonce should have yielded two tokens but was {0}","messagePattern":"Nonce should have yielded two tokens but was (.+?)","errorType":"exception","errorClass":"BadCredentialsException","httpStatus":401,"severity":"error","filePath":"web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java","lineNumber":394,"sourceCode":"\t\t\t\t\t\t\"DigestAuthenticationFilter.incorrectRealm\", new Object[] { this.realm, expectedRealm },\n\t\t\t\t\t\t\"Response realm name '{0}' does not match system realm name of '{1}'\"));\n\t\t\t}\n\t\t\t// Check nonce was Base64 encoded (as sent by DigestAuthenticationEntryPoint)\n\t\t\tfinal byte[] nonceBytes;\n\t\t\ttry {\n\t\t\t\tnonceBytes = Base64.getDecoder().decode(this.nonce.getBytes());\n\t\t\t}\n\t\t\tcatch (IllegalArgumentException ex) {\n\t\t\t\tthrow new BadCredentialsException(\n\t\t\t\t\t\tDigestAuthenticationFilter.this.messages.getMessage(\"DigestAuthenticationFilter.nonceEncoding\",\n\t\t\t\t\t\t\t\tnew Object[] { this.nonce }, \"Nonce is not encoded in Base64; received nonce {0}\"));\n\t\t\t}\n\t\t\t// Decode nonce from Base64 format of nonce is: base64(expirationTime + \":\" +\n\t\t\t// md5Hex(expirationTime + \":\" + key))\n\t\t\tString nonceAsPlainText = new String(nonceBytes);\n\t\t\tString[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, \":\");\n\t\t\tif (nonceTokens.length != 2) {\n\t\t\t\tthrow new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(\n\t\t\t\t\t\t\"DigestAuthenticationFilter.nonceNotTwoTokens\", new Object[] { nonceAsPlainText },\n\t\t\t\t\t\t\"Nonce should have yielded two tokens but was {0}\"));\n\t\t\t}\n\t\t\t// Extract expiry time from nonce\n\t\t\ttry {\n\t\t\t\tthis.nonceExpiryTime = Long.valueOf(nonceTokens[0]);\n\t\t\t}\n\t\t\tcatch (NumberFormatException nfe) {\n\t\t\t\tthrow new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(\n\t\t\t\t\t\t\"DigestAuthenticationFilter.nonceNotNumeric\", new Object[] { nonceAsPlainText },\n\t\t\t\t\t\t\"Nonce token should have yielded a numeric first token, but was {0}\"));\n\t\t\t}\n\t\t\t// Check signature of nonce matches this expiry time\n\t\t\tString expectedNonceSignature = DigestAuthUtils.md5Hex(this.nonceExpiryTime + \":\" + entryPointKey);\n\t\t\tif (!Utf8.isEqual(expectedNonceSignature, nonceTokens[1])) {\n\t\t\t\tthrow new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(\n\t\t\t\t\t\t\"DigestAuthenticationFilter.nonceCompromised\", new Object[] { nonceAsPlainText },\n\t\t\t\t\t\t\"Nonce token compromised {0}\"));","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java#L376-L412","documentation":"After Base64-decoding, the nonce plaintext must have the form 'expiryTime:signature' (two colon-separated tokens). validateAndDecode throws this BadCredentialsException when splitting the decoded nonce on ':' does not yield exactly two tokens, i.e. the nonce payload structure isn't what the server's entry point produced.","triggerScenarios":"Base64-decoding succeeds but the plaintext contains zero, one, or 3+ colon-separated segments — e.g. a nonce fabricated by the client, a nonce issued by a different application/server sharing the Base64 shape but not the format, or a corrupted/transformed nonce that happens to be valid Base64.","commonSituations":"Two different services both doing digest auth behind one domain and the client replaying a nonce from the wrong service; manual testing with arbitrary Base64 strings as nonces; a load balancer pool running mismatched application versions with different nonce formats.","solutions":["Obtain nonces only from this server's DigestAuthenticationEntryPoint via a fresh 401 challenge; never fabricate or reuse nonces from another app.","Check for multiple digest-auth apps behind the same host and make clients target the correct origin / consume each app's own challenge.","Verify all server instances behind the load balancer run a consistent Spring Security version and identical entry point configuration.","Decode the nonce client-side (base64 -> 'expiry:md5sig') to sanity-check the shape before sending."],"exampleFix":"// before (hand-made nonce)\nString nonce = Base64.getEncoder().encodeToString(\"12345\".getBytes());\n// after: request a challenge and use the server-issued nonce\nString nonce = extractNonce(authConn.getHeaderField(\"WWW-Authenticate\")); // expiry:signature format","handlingStrategy":"validation","validationCode":"String plain = new String(java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8)));\nif (plain.split(\":\", -1).length != 2) {\n    throw new IllegalStateException(\"nonce payload must be '<expiry>:<signature>'; request a fresh challenge\");\n}\n","typeGuard":"boolean hasNonceShape(String decodedNonce) {\n    return decodedNonce != null && decodedNonce.split(\":\", -1).length == 2;\n}","tryCatchPattern":"try {\n    chain.doFilter(request, response);\n} catch (BadCredentialsException e) {\n    if (e.getMessage().contains(\"should have yielded two tokens\")) {\n        response.sendError(401, \"Unrecognized nonce format; re-authenticate from the server challenge\");\n    }\n}","preventionTips":["Only use nonces issued by this application's DigestAuthenticationEntryPoint","Avoid multiple digest-auth apps behind one hostname sharing clients","Keep all server instances on the same Spring Security version","Decode and sanity-check the nonce shape before sending"],"tags":["spring-security","digest-auth","nonce","bad-credentials"],"backgroundTag":"invalid-argument-format","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}