Skip to content

Commit 7a2cd83

Browse files
author
waterWang
committed
fix: clear session cookies on SAML SLO logout to prevent stale session key
Fixes: #13997 Signed-off-by: waterWang <672684719@qq.com>
1 parent 6764ca5 commit 7a2cd83

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.xml.sax.SAXException;
4242

4343
import javax.inject.Inject;
44+
import javax.servlet.http.Cookie;
4445
import javax.servlet.http.HttpServletRequest;
4546
import javax.servlet.http.HttpServletResponse;
4647
import javax.servlet.http.HttpSession;
@@ -89,6 +90,7 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
8990
String responseString = ApiResponseSerializer.toSerializedString(response, responseType);
9091

9192
if (session == null) {
93+
clearSessionCookies(req, resp);
9294
try {
9395
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
9496
} catch (IOException ignored) {
@@ -119,6 +121,7 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
119121
} catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
120122
logger.error("SAMLResponse processing error: " + e.getMessage());
121123
}
124+
clearSessionCookies(req, resp);
122125
try {
123126
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
124127
} catch (IOException ignored) {
@@ -131,6 +134,7 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
131134
SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(idpId);
132135
String nameId = (String) session.getAttribute(SAMLPluginConstants.SAML_NAMEID);
133136
if (idpMetadata == null || nameId == null || nameId.isEmpty()) {
137+
clearSessionCookies(req, resp);
134138
try {
135139
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
136140
} catch (IOException ignored) {
@@ -142,6 +146,7 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
142146

143147
try {
144148
String redirectUrl = idpMetadata.getSloUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
149+
clearSessionCookies(req, resp);
145150
resp.sendRedirect(redirectUrl);
146151
} catch (MarshallingException | IOException e) {
147152
logger.error("SAML SLO error: " + e.getMessage());
@@ -152,6 +157,24 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
152157
return responseString;
153158
}
154159

160+
/**
161+
* Clears the session cookies (JSESSIONID, sessionkey, userid, ...) received from the browser so the
162+
* SAML SLO redirect response actually instructs the browser to drop them. ApiServlet runs its cookie
163+
* cleanup only after this authenticator returns, but {@code sendRedirect} commits the response first,
164+
* so those Set-Cookie headers would be lost and the session key would survive the logout.
165+
*/
166+
private void clearSessionCookies(final HttpServletRequest req, final HttpServletResponse resp) {
167+
final Cookie[] cookies = req.getCookies();
168+
if (cookies == null) {
169+
return;
170+
}
171+
for (final Cookie cookie : cookies) {
172+
cookie.setValue("");
173+
cookie.setMaxAge(0);
174+
resp.addCookie(cookie);
175+
}
176+
}
177+
155178
@Override
156179
public APIAuthenticationType getAPIType() {
157180
return APIAuthenticationType.LOGOUT_API;

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

Lines changed: 29 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;
@@ -85,4 +86,32 @@ public void testAuthenticate() throws Exception {
8586
public void testGetAPIType() throws Exception {
8687
Assert.assertTrue(new SAML2LogoutAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGOUT_API);
8788
}
89+
90+
@Test
91+
public void testAuthenticateClearsSessionCookiesBeforeRedirect() throws Exception {
92+
SAML2LogoutAPIAuthenticatorCmd cmd = new SAML2LogoutAPIAuthenticatorCmd();
93+
94+
Field apiServerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
95+
apiServerField.setAccessible(true);
96+
apiServerField.set(cmd, apiServer);
97+
98+
Field managerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
99+
managerField.setAccessible(true);
100+
managerField.set(cmd, samlAuthManager);
101+
102+
Cookie jsessionid = new Cookie("JSESSIONID", "dummy-session-id");
103+
Cookie sessionkey = new Cookie("sessionkey", "dummy-session-key");
104+
Cookie[] cookies = new Cookie[]{jsessionid, sessionkey};
105+
Mockito.when(req.getCookies()).thenReturn(cookies);
106+
Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null);
107+
108+
cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
109+
110+
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.any());
111+
Mockito.verify(resp, Mockito.times(2)).addCookie(Mockito.any(Cookie.class));
112+
Assert.assertEquals(0, jsessionid.getMaxAge());
113+
Assert.assertEquals("", jsessionid.getValue());
114+
Assert.assertEquals(0, sessionkey.getMaxAge());
115+
Assert.assertEquals("", sessionkey.getValue());
116+
}
88117
}

0 commit comments

Comments
 (0)