{"record":{"id":"22b32e7517218a61","repo":"spring-projects/spring-security","slug":"cookie-token-did-not-contain-2-tokens-but-contain","errorCode":null,"errorMessage":"Cookie token did not contain 2 tokens, but contained '[cookieTokens]'","messagePattern":"Cookie token did not contain 2 tokens, but contained '\\[cookieTokens\\]'","errorType":"exception","errorClass":"InvalidCookieException","httpStatus":null,"severity":"warning","filePath":"web/src/main/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServices.java","lineNumber":99,"sourceCode":"\t}\n\n\t/**\n\t * Locates the presented cookie data in the token repository, using the series id. If\n\t * the data compares successfully with that in the persistent store, a new token is\n\t * generated and stored with the same series. The corresponding cookie value is set on\n\t * the response.\n\t * @param cookieTokens the series and token values\n\t * @throws RememberMeAuthenticationException if there is no stored token corresponding\n\t * to the submitted cookie, or if the token in the persistent store has expired.\n\t * @throws InvalidCookieException if the cookie doesn't have two tokens as expected.\n\t * @throws CookieTheftException if a presented series value is found, but the stored\n\t * token is different from the one presented.\n\t */\n\t@Override\n\tprotected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,\n\t\t\tHttpServletResponse response) {\n\t\tif (cookieTokens.length != 2) {\n\t\t\tthrow new InvalidCookieException(\"Cookie token did not contain \" + 2 + \" tokens, but contained '\"\n\t\t\t\t\t+ Arrays.asList(cookieTokens) + \"'\");\n\t\t}\n\t\tString presentedSeries = cookieTokens[0];\n\t\tString presentedToken = cookieTokens[1];\n\t\tPersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);\n\t\tif (token == null) {\n\t\t\t// No series match, so we can't authenticate using this cookie\n\t\t\tthrow new RememberMeAuthenticationException(\"No persistent token found for series id: \" + presentedSeries);\n\t\t}\n\t\t// We have a match for this user/series combination\n\t\tif (!presentedToken.equals(token.getTokenValue())) {\n\t\t\t// Token doesn't match series value. Delete all logins for this user and throw\n\t\t\t// an exception to warn them.\n\t\t\tthis.tokenRepository.removeUserTokens(token.getUsername());\n\t\t\tthrow new CookieTheftException(this.messages.getMessage(\n\t\t\t\t\t\"PersistentTokenBasedRememberMeServices.cookieStolen\",\n\t\t\t\t\t\"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.\"));\n\t\t}","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/web/src/main/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServices.java#L81-L117","documentation":"PersistentTokenBasedRememberMeServices.processAutoLoginCookie expects the decoded remember-me cookie to contain exactly two tokens: the series id and the token value. A cookie yielding any other number of tokens is rejected with InvalidCookieException because its structure is invalid.","triggerScenarios":"autoLogin -> processAutoLoginCookie receiving a cookieTokens array whose length != 2 — caused by a corrupted cookie, a cookie whose embedded values contain the ':' delimiter (which is also the token separator), or extra/missing fields from a custom encodeCookie override.","commonSituations":"Usernames containing ':' splitting into three tokens after decode; old-format cookies from a different remember-me implementation after a migration; manual cookie editing; decoding producing extra elements due to unescaped delimiters.","solutions":["Clear the stale cookie (cancelCookie happens automatically) and log in again so a fresh, well-formed cookie is issued.","Ensure usernames and token values never contain the ':' delimiter; sanitize or restrict usernames at registration.","Do not override encodeCookie/decodeCookie in ways that change the token count.","If migrating from another remember-me format, invalidate all existing cookies (change the key) so old cookies are rejected cleanly."],"exampleFix":"// before\n// username \"alice:admin\" -> cookie decodes to [\"alice\",\"admin\",token] -> 3 tokens\nusernameValidator.validateUsername(\"alice:admin\"); // not enforced\n// after\nAssert.isTrue(!username.contains(\":\"), \"username must not contain ':'\"); // keep cookie 2-token format","handlingStrategy":"try-catch","validationCode":"String plain = new String(Base64.getDecoder().decode(cookieValue), StandardCharsets.UTF_8);\nif (plain.split(\":\", -1).length != 2) {\n    // malformed remember-me cookie: cancel it\n}","typeGuard":null,"tryCatchPattern":"try {\n    Authentication a = rememberMeServices.autoLogin(request, response);\n} catch (InvalidCookieException e) {\n    ((AbstractRememberMeServices) rememberMeServices).cancelCookie(request, response);\n}","preventionTips":["Reject usernames containing ':' at registration/validation time","Change the remember-me key when migrating cookie formats to invalidate old cookies","Do not override encodeCookie/decodeCookie to alter the token count","Clear cookies after upgrade migrations rather than letting old formats fail"],"tags":["cookie","remember-me","token-format","spring-security"],"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-14T16:17:12.679Z"}