{"record":{"id":"9bb82a2eac1d2982","repo":"halo-dev/halo","slug":"cookie-contained-signature-but-expected","errorCode":null,"errorMessage":"Cookie contained signature '{}' but expected '{}'","messagePattern":"Cookie contained signature '(.+?)' but expected '(.+?)'","errorType":"validation","errorClass":"InvalidCookieException","httpStatus":null,"severity":"warning","filePath":"application/src/main/java/run/halo/app/security/authentication/rememberme/TokenBasedRememberMeServices.java","lineNumber":178,"sourceCode":"                    // be cancelled.\n                    String actualTokenSignature;\n                    String actualAlgorithm = DEFAULT_ALGORITHM;\n                    // If the cookie value contains the algorithm, we use that algorithm to check the\n                    // signature\n                    if (cookieTokens.length == 4) {\n                        actualTokenSignature = cookieTokens[3];\n                        actualAlgorithm = cookieTokens[2];\n                    } else {\n                        actualTokenSignature = cookieTokens[2];\n                    }\n                    return makeTokenSignature(\n                                    tokenExpiryTime,\n                                    userDetails.getUsername(),\n                                    userDetails.getPassword(),\n                                    actualAlgorithm)\n                            .doOnNext(expectedTokenSignature -> {\n                                if (!equals(expectedTokenSignature, actualTokenSignature)) {\n                                    throw new InvalidCookieException(\n                                            \"Cookie contained signature '\" + actualTokenSignature\n                                                    + \"' but expected '\"\n                                                    + expectedTokenSignature + \"'\");\n                                }\n                            })\n                            .thenReturn(userDetails);\n                });\n    }\n\n    protected boolean isTokenExpired(long tokenExpiryTime) {\n        return tokenExpiryTime < System.currentTimeMillis();\n    }\n\n    private long getTokenExpiryTime(String[] cookieTokens) {\n        try {\n            return Long.parseLong(cookieTokens[1]);\n        } catch (NumberFormatException nfe) {\n            throw new InvalidCookieException(","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/application/src/main/java/run/halo/app/security/authentication/rememberme/TokenBasedRememberMeServices.java#L160-L196","documentation":"Halo's TokenBasedRememberMeServices re-derives the remember-me cookie signature from (tokenExpiryTime, username, password, algorithm) using the server key and compares it to the signature embedded in the cookie (token slot [2] or [3]). When the recomputed 'expected' signature does not equal the cookie's 'actual' signature, the cookie is treated as invalid/tampered and an InvalidCookieException is thrown. This mirrors Spring Security's classic TokenBasedRememberMeServices contract: the signature proves the cookie was minted by this server for this user's current password.","triggerScenarios":"Auto-login from a remember-me cookie after: the user's password hash changed since the cookie was issued; the server-side remember-me key changed (so makeTokenSignature produces a different HMAC); the cookie was manually edited; or a cookie minted by a different/older Halo deployment is presented. The check runs in processAutoLoginCookie -> makeTokenSignature(...).doOnNext(...).","commonSituations":"Server redeployed/restarted with a non-persistent or rotated remember-me key; password reset or password-change flow; user record now resolved from a different identity source with a different stored credential; cookie shared between environments (staging vs prod). Note handleError() only logs this at DEBUG and cancels the cookie, so to the user it looks like a silent re-login prompt.","solutions":["Have the user log in again interactively: cancelCookie() already expires the bad cookie and a fresh valid one is issued on next successful login (loginSuccess -> onLoginSuccess).","Make the remember-me key a stable, explicitly configured value (e.g. halo.security.remember-me.key) instead of a generated/rotated one, so signatures survive restarts.","If it occurs right after a password change, treat it as expected behavior; no code change is needed.","Confirm the same key and the same password source is used across all replicas of a clustered deployment."],"exampleFix":"// before: key is generated/rotated per restart -> all prior cookies fail signature check\n// application.yaml\nhalo:\n  security:\n    remember-me:\n      key: ${REMEMBER_ME_KEY}   # a stable, secret, shared value\n\n// After a password change, simply let the stale cookie be rejected and re-issue on next login.","handlingStrategy":"fallback","validationCode":"// Cannot validate a cookie signature client-side; the server recomputes it.\n// Guard at the API boundary by expecting auto-login to fail gracefully:\nclient.get(\"/api/console/user\").onErrorResume(e -> {\n    if (e instanceof InvalidCookieException) {\n        return redirectToLogin(); // cookie is already cancelled server-side\n    }\n    return Mono.error(e);\n});","typeGuard":null,"tryCatchPattern":"// In a ServerWebExchange filter / onErrorResume:\n.onErrorResume(InvalidCookieException.class, ex -> {\n    log.debug(\"Stale remember-me cookie rejected: {}\", ex.getMessage());\n    return Mono.empty(); // treat as not-authenticated, prompt login\n})","preventionTips":["Configure a stable halo.security.remember-me.key shared across all replicas and restarts.","Invalidate remember-me tokens when a user changes their password (expected, not a bug).","Never share cookies between staging and production deployments.","Monitor DEBUG logs for InvalidCookieException spikes to detect key drift or tampering campaigns."],"tags":["security","authentication","cookie","remember-me"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}