diff --git a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
index 6815aef517..add26e1c04 100755
--- a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
+++ b/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
@@ -207,7 +207,7 @@ public class ServletHttpFacade implements HttpFacade {
@Override
public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
- StringBuffer cookieBuf = new StringBuffer();
+ StringBuilder cookieBuf = new StringBuilder();
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null);
String cookie = cookieBuf.toString();
response.addHeader("Set-Cookie", cookie);
diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
index 43a7937ed2..7ff1fbceb6 100755
--- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
+++ b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
@@ -218,7 +218,7 @@ public class CatalinaHttpFacade implements HttpFacade {
@Override
public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
- StringBuffer cookieBuf = new StringBuffer();
+ StringBuilder cookieBuf = new StringBuilder();
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null);
String cookie = cookieBuf.toString();
response.addHeader("Set-Cookie", cookie);
diff --git a/common/src/main/java/org/keycloak/common/util/ServerCookie.java b/common/src/main/java/org/keycloak/common/util/ServerCookie.java
index be3850cf0b..1ce9901d5b 100755
--- a/common/src/main/java/org/keycloak/common/util/ServerCookie.java
+++ b/common/src/main/java/org/keycloak/common/util/ServerCookie.java
@@ -178,7 +178,7 @@ public class ServerCookie implements Serializable {
// TODO RFC2965 fields also need to be passed
- public static void appendCookieValue(StringBuffer headerBuf,
+ public static void appendCookieValue(StringBuilder headerBuf,
int version,
String name,
String value,
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
index 6a3db18682..085b96a9d5 100755
--- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
@@ -17,6 +17,7 @@
package org.keycloak.examples.authenticator;
+import org.keycloak.http.HttpCookie;
import org.keycloak.http.HttpResponse;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
@@ -24,7 +25,6 @@ import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.CredentialValidator;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.common.util.ServerCookie;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
@@ -33,7 +33,6 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.Cookie;
-import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
@@ -97,10 +96,7 @@ public class SecretQuestionAuthenticator implements Authenticator, CredentialVal
public void addCookie(AuthenticationFlowContext context, String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) {
HttpResponse response = context.getSession().getContext().getHttpResponse();
- StringBuffer cookieBuf = new StringBuffer();
- ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly, null);
- String cookie = cookieBuf.toString();
- response.addHeader(HttpHeaders.SET_COOKIE, cookie);
+ response.setCookieIfAbsent(new HttpCookie(1, name, value, path, domain, comment, maxAge, secure, httpOnly, null));
}
diff --git a/server-spi/src/main/java/org/keycloak/http/HttpCookie.java b/server-spi/src/main/java/org/keycloak/http/HttpCookie.java
new file mode 100644
index 0000000000..1f91771b08
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/http/HttpCookie.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.http;
+
+import org.keycloak.common.util.ServerCookie;
+import org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
+
+/**
+ * An extension of {@link javax.ws.rs.core.Cookie} in order to support additional
+ * fields and behavior.
+ */
+public final class HttpCookie extends javax.ws.rs.core.Cookie {
+
+ private final String comment;
+ private final int maxAge;
+ private final boolean secure;
+ private final boolean httpOnly;
+ private final SameSiteAttributeValue sameSite;
+
+ public HttpCookie(int version, String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, SameSiteAttributeValue sameSite) {
+ super(name, value, path, domain, version);
+ this.comment = comment;
+ this.maxAge = maxAge;
+ this.secure = secure;
+ this.httpOnly = httpOnly;
+ this.sameSite = sameSite;
+ }
+
+ public String toHeaderValue() {
+ StringBuilder cookieBuf = new StringBuilder();
+ ServerCookie.appendCookieValue(cookieBuf, getVersion(), getName(), getValue(), getPath(), getDomain(), comment, maxAge, secure, httpOnly, sameSite);
+ return cookieBuf.toString();
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/http/HttpResponse.java b/server-spi/src/main/java/org/keycloak/http/HttpResponse.java
index 821d89f8f8..f5ef357079 100644
--- a/server-spi/src/main/java/org/keycloak/http/HttpResponse.java
+++ b/server-spi/src/main/java/org/keycloak/http/HttpResponse.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.http;
+
/**
*
Represents an out coming HTTP response.
*
@@ -45,4 +46,20 @@ public interface HttpResponse {
* @param value the header value
*/
void setHeader(String name, String value);
+
+ /**
+ * Sets a new cookie only if not yet set.
+ *
+ * @param cookie the cookie
+ */
+ void setCookieIfAbsent(HttpCookie cookie);
+
+ /**
+ * Adding cookies at the end of the transaction helps when retrying a transaction might add the
+ * cookie multiple times. In some scenarios it must not be added at the end of the transaction,
+ * as at that time the response has already been sent to the caller ("committed"), so the code
+ * needs to make a choice. As retrying transactions is the exception, adding cookies at the end
+ * of the transaction is also the exception and needs to be switched on where necessary.
+ */
+ void setWriteCookiesOnTransactionComplete();
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index c160fa41b8..0d967b9751 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -49,7 +49,6 @@ import org.keycloak.services.clientpolicy.context.PreAuthorizationRequestContext
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
-import org.keycloak.services.util.CookieHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@@ -133,7 +132,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return KeycloakModelUtils.runJobInRetriableTransaction(session.getKeycloakSessionFactory(), new ResponseSessionTask(session) {
@Override
public Response runInternal(KeycloakSession session) {
- CookieHelper.addCookiesAtEndOfTransaction(session);
+ session.getContext().getHttpResponse().setWriteCookiesOnTransactionComplete();
// create another instance of the endpoint to isolate each run.
AuthorizationEndpoint other = new AuthorizationEndpoint(session,
new EventBuilder(session.getContext().getRealm(), session, clientConnection), action);
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
index 82eda92ff0..5d00a9637d 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
@@ -176,6 +176,6 @@ public class DefaultKeycloakContext implements KeycloakContext {
}
protected HttpResponse createHttpResponse() {
- return new HttpResponseImpl(getContextObject(org.jboss.resteasy.spi.HttpResponse.class));
+ return new HttpResponseImpl(session, getContextObject(org.jboss.resteasy.spi.HttpResponse.class));
}
}
diff --git a/services/src/main/java/org/keycloak/services/HttpResponseImpl.java b/services/src/main/java/org/keycloak/services/HttpResponseImpl.java
index f05135b58b..f9cb48647f 100644
--- a/services/src/main/java/org/keycloak/services/HttpResponseImpl.java
+++ b/services/src/main/java/org/keycloak/services/HttpResponseImpl.java
@@ -17,14 +17,26 @@
package org.keycloak.services;
-import org.keycloak.http.HttpResponse;
+import java.util.HashSet;
+import java.util.Set;
-public class HttpResponseImpl implements HttpResponse {
+import javax.ws.rs.core.HttpHeaders;
+
+import org.keycloak.http.HttpCookie;
+import org.keycloak.http.HttpResponse;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+
+public class HttpResponseImpl implements HttpResponse, KeycloakTransaction {
private final org.jboss.resteasy.spi.HttpResponse delegate;
+ private Set cookies;
+ private boolean transactionActive;
+ private boolean writeCookiesOnTransactionComplete;
- public HttpResponseImpl(org.jboss.resteasy.spi.HttpResponse delegate) {
+ public HttpResponseImpl(KeycloakSession session, org.jboss.resteasy.spi.HttpResponse delegate) {
this.delegate = delegate;
+ session.getTransactionManager().enlistAfterCompletion(this);
}
@Override
@@ -44,6 +56,31 @@ public class HttpResponseImpl implements HttpResponse {
delegate.getOutputHeaders().putSingle(name, value);
}
+ @Override
+ public void setCookieIfAbsent(HttpCookie cookie) {
+ if (cookie == null) {
+ throw new IllegalArgumentException("Cookie is null");
+ }
+
+ if (cookies == null) {
+ cookies = new HashSet<>();
+ }
+
+ if (cookies.add(cookie)) {
+ if (writeCookiesOnTransactionComplete) {
+ // cookies are written after transaction completes
+ return;
+ }
+
+ addHeader(HttpHeaders.SET_COOKIE, cookie.toHeaderValue());
+ }
+ }
+
+ @Override
+ public void setWriteCookiesOnTransactionComplete() {
+ this.writeCookiesOnTransactionComplete = true;
+ }
+
/**
* Validate that the response has not been committed.
* If the response is already committed, the headers and part of the response have been sent already.
@@ -55,4 +92,58 @@ public class HttpResponseImpl implements HttpResponse {
}
}
+ @Override
+ public void begin() {
+ transactionActive = true;
+ }
+
+ @Override
+ public void commit() {
+ if (!transactionActive) {
+ throw new IllegalStateException("Transaction not active. Response already committed or rolled back");
+ }
+
+ try {
+ addCookiesAfterTransaction();
+ } finally {
+ close();
+ }
+ }
+
+ @Override
+ public void rollback() {
+ close();
+ }
+
+ @Override
+ public void setRollbackOnly() {
+
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return false;
+ }
+
+ @Override
+ public boolean isActive() {
+ return transactionActive;
+ }
+
+ private void close() {
+ transactionActive = false;
+ cookies = null;
+ }
+
+ private void addCookiesAfterTransaction() {
+ if (cookies == null || !writeCookiesOnTransactionComplete) {
+ return;
+ }
+
+ // Ensure that cookies are only added when the transaction is complete, as otherwise cookies will be set for
+ // error pages, or will be added twice when running retries.
+ for (HttpCookie cookie : cookies) {
+ addHeader(HttpHeaders.SET_COOKIE, cookie.toHeaderValue());
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index dd406ad81e..ef3aa60b48 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -909,7 +909,6 @@ public class AuthenticationManager {
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, null, true, tokenString, session.getContext().getRequestHeaders(), VALIDATE_IDENTITY_COOKIE);
if (authResult == null) {
expireIdentityCookie(realm, session.getContext().getUri(), session);
- expireOldIdentityCookie(realm, session.getContext().getUri(), session);
return null;
}
authResult.getSession().setLastSessionRefresh(Time.currentTime());
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 1f7f5b0aaa..988b349438 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -81,7 +81,6 @@ import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.services.util.BrowserHistoryHelper;
import org.keycloak.services.util.CacheControlUtil;
-import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.LocaleUtil;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel;
@@ -261,7 +260,7 @@ public class LoginActionsService {
@Override
public Response runInternal(KeycloakSession session) {
// create another instance of the endpoint to isolate each run.
- CookieHelper.addCookiesAtEndOfTransaction(session);
+ session.getContext().getHttpResponse().setWriteCookiesOnTransactionComplete();
LoginActionsService other = new LoginActionsService(session, new EventBuilder(session.getContext().getRealm(), session, clientConnection));
// process the request in the created instance.
return other.authenticateInternal(authSessionId, code, execution, clientId, tabId);
diff --git a/services/src/main/java/org/keycloak/services/util/CookieHelper.java b/services/src/main/java/org/keycloak/services/util/CookieHelper.java
index 4fbe081872..8a4c6fa674 100755
--- a/services/src/main/java/org/keycloak/services/util/CookieHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/CookieHelper.java
@@ -18,18 +18,16 @@
package org.keycloak.services.util;
import org.jboss.logging.Logger;
+import org.keycloak.http.HttpCookie;
import org.keycloak.http.HttpResponse;
import org.jboss.resteasy.util.CookieParser;
-import org.keycloak.common.util.ServerCookie;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakTransaction;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
@@ -44,7 +42,6 @@ public class CookieHelper {
public static final String LEGACY_COOKIE = "_LEGACY";
private static final Logger logger = Logger.getLogger(CookieHelper.class);
- private static final String ADD_COOKIES_AT_END_OF_TRANSACTION = CookieHelper.class.getName() + "_ADD_COOKIES_AT_END_OF_TRANSACTION";
/**
* Set a response cookie. This solely exists because JAX-RS 1.1 does not support setting HttpOnly cookies
@@ -69,14 +66,9 @@ public class CookieHelper {
boolean secure_sameSite = sameSite == SameSiteAttributeValue.NONE || secure; // when SameSite=None, Secure attribute must be set
HttpResponse response = session.getContext().getHttpResponse();
- StringBuffer cookieBuf = new StringBuffer();
- ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure_sameSite, httpOnly, sameSite);
- String cookie = cookieBuf.toString();
- if (shouldAddCookiesAtEndOfTransaction(session)) {
- session.getTransactionManager().enlistAfterCompletion(new CookieTransaction(response, cookie));
- } else {
- response.addHeader(HttpHeaders.SET_COOKIE, cookie);
- }
+ HttpCookie cookie = new HttpCookie(1, name, value, path, domain, comment, maxAge, secure_sameSite, httpOnly, sameSite);
+
+ response.setCookieIfAbsent(cookie);
// a workaround for browser in older Apple OSs – browsers ignore cookies with SameSite=None
if (sameSiteParam == SameSiteAttributeValue.NONE) {
@@ -84,21 +76,6 @@ public class CookieHelper {
}
}
- private static boolean shouldAddCookiesAtEndOfTransaction(KeycloakSession session) {
- return Objects.equals(session.getAttribute(ADD_COOKIES_AT_END_OF_TRANSACTION), Boolean.TRUE);
- }
-
- /**
- * Adding cookies at the end of the transaction helps when retrying a transaction might add the
- * cookie multiple times. In some scenarios it must not be added at the end of the transaction,
- * as at that time the response has already been sent to the caller ("committed"), so the code
- * needs to make a choice. As retrying transactions is the exception, adding cookies at the end
- * of the transaction is also the exception and needs to be switched on where necessary.
- */
- public static void addCookiesAtEndOfTransaction(KeycloakSession session) {
- session.setAttribute(ADD_COOKIES_AT_END_OF_TRANSACTION, Boolean.TRUE);
- }
-
/**
* Set a response cookie avoiding SameSite parameter
* @param name
@@ -172,49 +149,4 @@ public class CookieHelper {
return cookies.get(legacy);
}
}
-
- /**
- * Ensure that cookies are only added when the transaction is complete, as otherwise cookies will be set for error pages,
- * or will be added twice when running retries.
- */
- private static class CookieTransaction implements KeycloakTransaction {
- private final HttpResponse response;
- private final String cookie;
- private boolean transactionActive;
-
- public CookieTransaction(HttpResponse response, String cookie) {
- this.response = response;
- this.cookie = cookie;
- }
-
- @Override
- public void begin() {
- transactionActive = true;
- }
-
- @Override
- public void commit() {
- response.addHeader(HttpHeaders.SET_COOKIE, cookie);
- transactionActive = false;
- }
-
- @Override
- public void rollback() {
- transactionActive = false;
- }
-
- @Override
- public void setRollbackOnly() {
- }
-
- @Override
- public boolean getRollbackOnly() {
- return false;
- }
-
- @Override
- public boolean isActive() {
- return transactionActive;
- }
- }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cookies/CookieTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cookies/CookieTest.java
index a7435e01ff..30d5b75de0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cookies/CookieTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cookies/CookieTest.java
@@ -16,20 +16,25 @@
*/
package org.keycloak.testsuite.cookies;
+import org.apache.http.Header;
+import org.apache.http.client.CookieStore;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.jboss.arquillian.graphene.page.Page;
-import org.junit.BeforeClass;
import org.junit.Test;
+import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.common.Profile;
+import org.keycloak.models.Constants;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.auth.page.AuthRealm;
@@ -40,7 +45,10 @@ import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
import org.keycloak.testsuite.util.RealmBuilder;
import org.openqa.selenium.Cookie;
+import java.io.IOException;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
@@ -56,6 +64,8 @@ import static org.keycloak.services.util.CookieHelper.LEGACY_COOKIE;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
+import javax.ws.rs.core.HttpHeaders;
+
/**
*
* @author hmlnarik
@@ -186,6 +196,48 @@ public class CookieTest extends AbstractKeycloakTest {
assertSameSiteCookies(sameSiteAuthSessionIdCookie, legacyAuthSessionIdCookie);
}
+ @Test
+ public void testNoDuplicationsWhenExpiringCookies() throws IOException {
+ ContainerAssume.assumeAuthServerSSL();
+
+ accountPage.navigateTo();
+ assertCurrentUrlStartsWithLoginUrlOf(accountPage);
+
+ loginPage.login("test-user@localhost", "password");
+
+ UsersResource usersResource = realmsResouce().realm(AuthRealm.TEST).users();
+ UserRepresentation user = usersResource.search("test-user@localhost").get(0);
+
+ usersResource.get(user.getId()).logout();
+
+ Cookie invalidIdentityCookie = driver.manage().getCookieNamed(KEYCLOAK_IDENTITY_COOKIE);
+ CookieStore cookieStore = new BasicCookieStore();
+
+ BasicClientCookie invalidClientIdentityCookie = new BasicClientCookie(invalidIdentityCookie.getName(), invalidIdentityCookie.getValue());
+
+ invalidClientIdentityCookie.setDomain(invalidIdentityCookie.getDomain());
+ invalidClientIdentityCookie.setPath(invalidClientIdentityCookie.getPath());
+
+ cookieStore.addCookie(invalidClientIdentityCookie);
+
+ try (CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build()) {
+ HttpGet get = new HttpGet(
+ suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/" + AuthRealm.TEST + "/protocol/openid-connect/auth?response_type=code&client_id=" + Constants.ACCOUNT_CONSOLE_CLIENT_ID +
+ "&redirect_uri=" + suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/" + AuthRealm.TEST + "/account&scope=openid");
+
+ try (CloseableHttpResponse response = client.execute(get)) {
+ Header[] headers = response.getHeaders(HttpHeaders.SET_COOKIE);
+ Set cookies = new HashSet<>();
+
+ for (Header header : headers) {
+ assertTrue("Cookie '" + header.getValue() + "' is duplicated", cookies.add(header.getValue()));
+ }
+
+ assertFalse(cookies.isEmpty());
+ }
+ }
+ }
+
private void assertSameSiteCookies(Cookie sameSiteCookie, Cookie legacyCookie) {
assertNotNull("SameSite cookie shouldn't be null", sameSiteCookie);
assertNotNull("Legacy cookie shouldn't be null", legacyCookie);