Skip to content

Commit 2b3cca6

Browse files
authored
SAML SLO does not clear session cookies on logout (#14017)
1 parent e3c7b76 commit 2b3cca6

3 files changed

Lines changed: 46 additions & 9 deletions

File tree

‎plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717
package org.apache.cloudstack.api.command;
1818

19+
import com.cloud.api.ApiServlet;
1920
import com.cloud.api.response.ApiResponseSerializer;
2021
import com.cloud.user.Account;
2122
import org.apache.cloudstack.api.APICommand;
@@ -88,6 +89,8 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
8889
response.setResponseName(getCommandName());
8990
String responseString = ApiResponseSerializer.toSerializedString(response, responseType);
9091

92+
ApiServlet.clearRequestCookies(req, resp);
93+
9194
if (session == null) {
9295
try {
9396
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());

‎plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.net.InetAddress;
2424
import java.security.cert.X509Certificate;
2525

26+
import javax.servlet.http.Cookie;
2627
import javax.servlet.http.HttpServletRequest;
2728
import javax.servlet.http.HttpServletResponse;
2829
import javax.servlet.http.HttpSession;
@@ -35,6 +36,7 @@
3536
import org.junit.Assert;
3637
import org.junit.Test;
3738
import org.junit.runner.RunWith;
39+
import org.mockito.ArgumentCaptor;
3840
import org.mockito.Mock;
3941
import org.mockito.Mockito;
4042
import org.mockito.junit.MockitoJUnitRunner;
@@ -81,6 +83,30 @@ public void testAuthenticate() throws Exception {
8183
Mockito.verify(session, Mockito.atLeastOnce()).getAttribute(Mockito.anyString());
8284
}
8385

86+
@Test
87+
public void testAuthenticateClearsSessionCookiesBeforeRedirect() throws Exception {
88+
SAML2LogoutAPIAuthenticatorCmd cmd = new SAML2LogoutAPIAuthenticatorCmd();
89+
90+
Field apiServerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
91+
apiServerField.setAccessible(true);
92+
apiServerField.set(cmd, apiServer);
93+
94+
Field managerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
95+
managerField.setAccessible(true);
96+
managerField.set(cmd, samlAuthManager);
97+
98+
Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null);
99+
Cookie sessionKeyCookie = new Cookie("sessionkey", "someKey");
100+
Mockito.when(req.getCookies()).thenReturn(new Cookie[]{sessionKeyCookie});
101+
102+
cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
103+
104+
ArgumentCaptor<Cookie> cookieCaptor = ArgumentCaptor.forClass(Cookie.class);
105+
Mockito.verify(resp, Mockito.times(1)).addCookie(cookieCaptor.capture());
106+
Assert.assertEquals(0, cookieCaptor.getValue().getMaxAge());
107+
Assert.assertEquals("", cookieCaptor.getValue().getValue());
108+
}
109+
84110
@Test
85111
public void testGetAPIType() throws Exception {
86112
Assert.assertTrue(new SAML2LogoutAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGOUT_API);

‎server/src/main/java/com/cloud/api/ApiServlet.java‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
332332
apiServer.logoutUser(userId);
333333
}
334334
invalidateHttpSession(session, "invalidating session after logout call");
335-
336-
final Cookie[] cookies = req.getCookies();
337-
if (cookies != null) {
338-
for (final Cookie cookie : cookies) {
339-
cookie.setValue("");
340-
cookie.setMaxAge(0);
341-
resp.addCookie(cookie);
342-
}
343-
}
335+
clearRequestCookies(req, resp);
344336
}
345337
HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType, ApiServer.JSONContentType.value());
346338
return;
@@ -632,6 +624,22 @@ private boolean invalidateHttpSessionIfNeeded(HttpServletRequest req, HttpServle
632624
return false;
633625
}
634626

627+
/**
628+
* Echoes back every cookie on the request with Max-Age=0 so the browser drops them. Must be
629+
* called before the response is committed (e.g. before HttpServletResponse#sendRedirect),
630+
* otherwise the Set-Cookie headers are silently dropped by the servlet container.
631+
*/
632+
public static void clearRequestCookies(HttpServletRequest req, HttpServletResponse resp) {
633+
final Cookie[] cookies = req.getCookies();
634+
if (cookies != null) {
635+
for (final Cookie cookie : cookies) {
636+
cookie.setValue("");
637+
cookie.setMaxAge(0);
638+
resp.addCookie(cookie);
639+
}
640+
}
641+
}
642+
635643
public static void invalidateHttpSession(HttpSession session, String msg) {
636644
try {
637645
if (LOGGER.isTraceEnabled()) {

0 commit comments

Comments
 (0)