{"record":{"id":"ee2722ef9160e5c4","repo":"spring-projects/spring-security","slug":"cookie-contained-signature-actualtokensignature","errorCode":null,"errorMessage":"Cookie contained signature '<actualTokenSignature>' but expected '<expectedTokenSignature>'","messagePattern":"Cookie contained signature '<actualTokenSignature>' but expected '<expectedTokenSignature>'","errorType":"exception","errorClass":"InvalidCookieException","httpStatus":null,"severity":"warning","filePath":"web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java","lineNumber":159,"sourceCode":"\t\t\t\t+ \" returned null for username \" + cookieTokens[0] + \". \" + \"This is an interface contract violation\");\n\t\t// Check signature of token matches remaining details. Must do this after user\n\t\t// lookup, as we need the DAO-derived password. If efficiency was a major issue,\n\t\t// just add in a UserCache implementation, but recall that this method is usually\n\t\t// only called once per HttpSession - if the token is valid, it will cause\n\t\t// SecurityContextHolder population, whilst if invalid, will cause the cookie to\n\t\t// be cancelled.\n\t\tString actualTokenSignature = cookieTokens[2];\n\t\tRememberMeTokenAlgorithm actualAlgorithm = this.matchingAlgorithm;\n\t\t// If the cookie value contains the algorithm, we use that algorithm to check the\n\t\t// signature\n\t\tif (cookieTokens.length == 4) {\n\t\t\tactualTokenSignature = cookieTokens[3];\n\t\t\tactualAlgorithm = RememberMeTokenAlgorithm.valueOf(cookieTokens[2]);\n\t\t}\n\t\tString expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),\n\t\t\t\tuserDetails.getPassword(), actualAlgorithm);\n\t\tif (!equals(expectedTokenSignature, actualTokenSignature)) {\n\t\t\tthrow new InvalidCookieException(\"Cookie contained signature '\" + actualTokenSignature + \"' but expected '\"\n\t\t\t\t\t+ expectedTokenSignature + \"'\");\n\t\t}\n\t\treturn userDetails;\n\t}\n\n\tprivate boolean isValidCookieTokensLength(String[] cookieTokens) {\n\t\treturn cookieTokens.length == 3 || cookieTokens.length == 4;\n\t}\n\n\tprivate long getTokenExpiryTime(String[] cookieTokens) {\n\t\ttry {\n\t\t\treturn Long.valueOf(cookieTokens[1]);\n\t\t}\n\t\tcatch (NumberFormatException nfe) {\n\t\t\tthrow new InvalidCookieException(\n\t\t\t\t\t\"Cookie token[1] did not contain a valid number (contained '\" + cookieTokens[1] + \"')\");\n\t\t}\n\t}","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java#L141-L177","documentation":"The remember-me cookie carries an HMAC signature computed over expiry time, username, password, and algorithm using the configured signing key. If the signature in the cookie does not match the recomputed expected signature, InvalidCookieException is thrown. This indicates the cookie was forged, corrupted, or signed with a different key/password.","triggerScenarios":"processAutoLoginCookie computes makeTokenSignature(expiry, username, password, algorithm) and compares it with cookieTokens[3] using a non-constant-time-vs-equals check; any mismatch throws. Occurs when the shared key differs between servers, the user's password changed (signature includes password hash), or the cookie was altered.","commonSituations":"Signing key not identical across cluster nodes (each node rejects the other's cookies); user's password changed after cookie issuance (signature embeds password); cookie tampering attempts; key rotated without invalidating old cookies.","solutions":["Ensure the remember-me key is identical on all application instances (externalize to config)","Invalidate and re-issue cookies after password changes — expected behavior since the signature includes the password","Check the cookie was not modified in transit (secure transport, no tampering proxies)","If key rotation is planned, plan for mass re-login or version the key"],"exampleFix":"// before (hardcoded divergent keys per node)\nhttp.rememberMe(r -> r.key(\"node1-secret\"));\n// after (shared externalized key)\nhttp.rememberMe(r -> r.key(env.getRequiredProperty(\"SECURITY_REMEMBER_ME_KEY\")));","handlingStrategy":"validation","validationCode":"// ensure all nodes share the same key at startup\n@PostConstruct void checkKey() {\n    Assert.notNull(env.getProperty(\"security.remember-me.key\"),\n        \"remember-me key must be identical across nodes\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    UserDetails u = rememberMeServices.autoLogin(request, response);\n} catch (InvalidCookieException e) {\n    logger.debug(\"Remember-me signature mismatch; clearing cookie\", e);\n    cookieClearingLogoutHandler.logout(request, response, null);\n    chain.doFilter(request, response);\n}","preventionTips":["Externalize the remember-me key and deploy it identically to every node","Expect cookies to be invalid after password changes; don't treat as a bug","Enable HTTPS to prevent in-transit cookie modification","Plan cookie invalidation windows around key rotations"],"tags":["remember-me","spring-security","signature","tampering"],"backgroundTag":"checksum-mismatch","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"}