This is an automated email from the ASF dual-hosted git repository.

DaanHoogland pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.22 by this push:
     new 2b3cca6cf47 SAML SLO does not clear session cookies on logout (#14017)
2b3cca6cf47 is described below

commit 2b3cca6cf47197bc755b1d8c0ad2931b1246cd99
Author: dahn <[email protected]>
AuthorDate: Fri Sep 4 09:31:06 2026 +0200

    SAML SLO does not clear session cookies on logout (#14017)
---
 .../command/SAML2LogoutAPIAuthenticatorCmd.java    |  3 +++
 .../SAML2LogoutAPIAuthenticatorCmdTest.java        | 26 ++++++++++++++++++++++
 server/src/main/java/com/cloud/api/ApiServlet.java | 26 ++++++++++++++--------
 3 files changed, 46 insertions(+), 9 deletions(-)

diff --git 
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
 
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
index ca46bef4b5a..927d815f572 100644
--- 
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
+++ 
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
@@ -16,6 +16,7 @@
 // under the License.
 package org.apache.cloudstack.api.command;
 
+import com.cloud.api.ApiServlet;
 import com.cloud.api.response.ApiResponseSerializer;
 import com.cloud.user.Account;
 import org.apache.cloudstack.api.APICommand;
@@ -88,6 +89,8 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd 
implements APIAuthen
         response.setResponseName(getCommandName());
         String responseString = 
ApiResponseSerializer.toSerializedString(response, responseType);
 
+        ApiServlet.clearRequestCookies(req, resp);
+
         if (session == null) {
             try {
                 
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
diff --git 
a/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java
 
b/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java
index 2060d0baf31..01797aa6feb 100644
--- 
a/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java
+++ 
b/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java
@@ -23,6 +23,7 @@ import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.security.cert.X509Certificate;
 
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
@@ -35,6 +36,7 @@ import org.apache.cloudstack.utils.security.CertUtils;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -81,6 +83,30 @@ public class SAML2LogoutAPIAuthenticatorCmdTest {
         Mockito.verify(session, 
Mockito.atLeastOnce()).getAttribute(Mockito.anyString());
     }
 
+    @Test
+    public void testAuthenticateClearsSessionCookiesBeforeRedirect() throws 
Exception {
+        SAML2LogoutAPIAuthenticatorCmd cmd = new 
SAML2LogoutAPIAuthenticatorCmd();
+
+        Field apiServerField = 
SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
+        apiServerField.setAccessible(true);
+        apiServerField.set(cmd, apiServer);
+
+        Field managerField = 
SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
+        managerField.setAccessible(true);
+        managerField.set(cmd, samlAuthManager);
+
+        
Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null);
+        Cookie sessionKeyCookie = new Cookie("sessionkey", "someKey");
+        Mockito.when(req.getCookies()).thenReturn(new 
Cookie[]{sessionKeyCookie});
+
+        cmd.authenticate("command", null, session, 
InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new 
StringBuilder(), req, resp);
+
+        ArgumentCaptor<Cookie> cookieCaptor = 
ArgumentCaptor.forClass(Cookie.class);
+        Mockito.verify(resp, 
Mockito.times(1)).addCookie(cookieCaptor.capture());
+        Assert.assertEquals(0, cookieCaptor.getValue().getMaxAge());
+        Assert.assertEquals("", cookieCaptor.getValue().getValue());
+    }
+
     @Test
     public void testGetAPIType() throws Exception {
         Assert.assertTrue(new SAML2LogoutAPIAuthenticatorCmd().getAPIType() == 
APIAuthenticationType.LOGOUT_API);
diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java 
b/server/src/main/java/com/cloud/api/ApiServlet.java
index 3ac5bbb01a7..6a14a00198a 100644
--- a/server/src/main/java/com/cloud/api/ApiServlet.java
+++ b/server/src/main/java/com/cloud/api/ApiServlet.java
@@ -332,15 +332,7 @@ public class ApiServlet extends HttpServlet {
                             apiServer.logoutUser(userId);
                         }
                         invalidateHttpSession(session, "invalidating session 
after logout call");
-
-                        final Cookie[] cookies = req.getCookies();
-                        if (cookies != null) {
-                            for (final Cookie cookie : cookies) {
-                                cookie.setValue("");
-                                cookie.setMaxAge(0);
-                                resp.addCookie(cookie);
-                            }
-                        }
+                        clearRequestCookies(req, resp);
                     }
                     HttpUtils.writeHttpResponse(resp, responseString, 
httpResponseCode, responseType, ApiServer.JSONContentType.value());
                     return;
@@ -632,6 +624,22 @@ public class ApiServlet extends HttpServlet {
         return false;
     }
 
+    /**
+     * Echoes back every cookie on the request with Max-Age=0 so the browser 
drops them. Must be
+     * called before the response is committed (e.g. before 
HttpServletResponse#sendRedirect),
+     * otherwise the Set-Cookie headers are silently dropped by the servlet 
container.
+     */
+    public static void clearRequestCookies(HttpServletRequest req, 
HttpServletResponse resp) {
+        final Cookie[] cookies = req.getCookies();
+        if (cookies != null) {
+            for (final Cookie cookie : cookies) {
+                cookie.setValue("");
+                cookie.setMaxAge(0);
+                resp.addCookie(cookie);
+            }
+        }
+    }
+
     public static void invalidateHttpSession(HttpSession session, String msg) {
         try {
             if (LOGGER.isTraceEnabled()) {

Reply via email to