halo-dev/halo · warning · InvalidCookieException

Cookie contained signature '{}' but expected '{}'

Error message

Cookie contained signature '{}' but expected '{}'

What it means

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.

Source

Thrown at application/src/main/java/run/halo/app/security/authentication/rememberme/TokenBasedRememberMeServices.java:178

                    // be cancelled.
                    String actualTokenSignature;
                    String actualAlgorithm = DEFAULT_ALGORITHM;
                    // If the cookie value contains the algorithm, we use that algorithm to check the
                    // signature
                    if (cookieTokens.length == 4) {
                        actualTokenSignature = cookieTokens[3];
                        actualAlgorithm = cookieTokens[2];
                    } else {
                        actualTokenSignature = cookieTokens[2];
                    }
                    return makeTokenSignature(
                                    tokenExpiryTime,
                                    userDetails.getUsername(),
                                    userDetails.getPassword(),
                                    actualAlgorithm)
                            .doOnNext(expectedTokenSignature -> {
                                if (!equals(expectedTokenSignature, actualTokenSignature)) {
                                    throw new InvalidCookieException(
                                            "Cookie contained signature '" + actualTokenSignature
                                                    + "' but expected '"
                                                    + expectedTokenSignature + "'");
                                }
                            })
                            .thenReturn(userDetails);
                });
    }

    protected boolean isTokenExpired(long tokenExpiryTime) {
        return tokenExpiryTime < System.currentTimeMillis();
    }

    private long getTokenExpiryTime(String[] cookieTokens) {
        try {
            return Long.parseLong(cookieTokens[1]);
        } catch (NumberFormatException nfe) {
            throw new InvalidCookieException(

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. 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).
  2. 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.
  3. If it occurs right after a password change, treat it as expected behavior; no code change is needed.
  4. Confirm the same key and the same password source is used across all replicas of a clustered deployment.

Example fix

// before: key is generated/rotated per restart -> all prior cookies fail signature check
// application.yaml
halo:
  security:
    remember-me:
      key: ${REMEMBER_ME_KEY}   # a stable, secret, shared value

// After a password change, simply let the stale cookie be rejected and re-issue on next login.
Defensive patterns

Strategy: fallback

Validate before calling

// Cannot validate a cookie signature client-side; the server recomputes it.
// Guard at the API boundary by expecting auto-login to fail gracefully:
client.get("/api/console/user").onErrorResume(e -> {
    if (e instanceof InvalidCookieException) {
        return redirectToLogin(); // cookie is already cancelled server-side
    }
    return Mono.error(e);
});

Try / catch

// In a ServerWebExchange filter / onErrorResume:
.onErrorResume(InvalidCookieException.class, ex -> {
    log.debug("Stale remember-me cookie rejected: {}", ex.getMessage());
    return Mono.empty(); // treat as not-authenticated, prompt login
})

Prevention

When it happens

Trigger: 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(...).

Common situations: 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.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/9bb82a2eac1d2982. Report an issue: GitHub.