spring-projects/spring-security · warning · InvalidCookieException

Cookie contained signature

Error message

Cookie contained signature '<actualTokenSignature>' but expected '<expectedTokenSignature>'

What it means

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.

Solutions

  1. Ensure the remember-me key is identical on all application instances (externalize to config)
  2. Invalidate and re-issue cookies after password changes — expected behavior since the signature includes the password
  3. Check the cookie was not modified in transit (secure transport, no tampering proxies)
  4. If key rotation is planned, plan for mass re-login or version the key

Example fix

// before (hardcoded divergent keys per node)
http.rememberMe(r -> r.key("node1-secret"));
// after (shared externalized key)
http.rememberMe(r -> r.key(env.getRequiredProperty("SECURITY_REMEMBER_ME_KEY")));
Defensive patterns

Strategy: validation

Validate before calling

// ensure all nodes share the same key at startup
@PostConstruct void checkKey() {
    Assert.notNull(env.getProperty("security.remember-me.key"),
        "remember-me key must be identical across nodes");
}

Try / catch

try {
    UserDetails u = rememberMeServices.autoLogin(request, response);
} catch (InvalidCookieException e) {
    logger.debug("Remember-me signature mismatch; clearing cookie", e);
    cookieClearingLogoutHandler.logout(request, response, null);
    chain.doFilter(request, response);
}

Prevention

When it happens

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

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

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/ee2722ef9160e5c4. Report an issue: GitHub.

Appendix: source

Thrown at web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java:159

				+ " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");
		// Check signature of token matches remaining details. Must do this after user
		// lookup, as we need the DAO-derived password. If efficiency was a major issue,
		// just add in a UserCache implementation, but recall that this method is usually
		// only called once per HttpSession - if the token is valid, it will cause
		// SecurityContextHolder population, whilst if invalid, will cause the cookie to
		// be cancelled.
		String actualTokenSignature = cookieTokens[2];
		RememberMeTokenAlgorithm actualAlgorithm = this.matchingAlgorithm;
		// If the cookie value contains the algorithm, we use that algorithm to check the
		// signature
		if (cookieTokens.length == 4) {
			actualTokenSignature = cookieTokens[3];
			actualAlgorithm = RememberMeTokenAlgorithm.valueOf(cookieTokens[2]);
		}
		String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
				userDetails.getPassword(), actualAlgorithm);
		if (!equals(expectedTokenSignature, actualTokenSignature)) {
			throw new InvalidCookieException("Cookie contained signature '" + actualTokenSignature + "' but expected '"
					+ expectedTokenSignature + "'");
		}
		return userDetails;
	}

	private boolean isValidCookieTokensLength(String[] cookieTokens) {
		return cookieTokens.length == 3 || cookieTokens.length == 4;
	}

	private long getTokenExpiryTime(String[] cookieTokens) {
		try {
			return Long.valueOf(cookieTokens[1]);
		}
		catch (NumberFormatException nfe) {
			throw new InvalidCookieException(
					"Cookie token[1] did not contain a valid number (contained '" + cookieTokens[1] + "')");
		}
	}

View on GitHub (pinned to 96852e8860)