Skip to content

Commit 13dd698

Browse files
weizhouapacheDaanHoogland
authored andcommitted
util: check JSESSIONID in cookies if user is passed
1 parent 4fa22f4 commit 13dd698

File tree

12 files changed

+266
-65
lines changed

12 files changed

+266
-65
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.apache.cloudstack.saml.SAMLUtils;
4949
import org.apache.log4j.Logger;
5050

51+
import com.cloud.api.ApiServer;
5152
import com.cloud.api.response.ApiResponseSerializer;
5253
import com.cloud.domain.Domain;
5354
import com.cloud.domain.dao.DomainDao;
@@ -60,6 +61,8 @@
6061
import com.cloud.user.dao.UserDao;
6162
import com.cloud.utils.HttpUtils;
6263

64+
import org.apache.commons.lang3.EnumUtils;
65+
6366
@APICommand(name = "listAndSwitchSamlAccount", description = "Lists and switches to other SAML accounts owned by the SAML user", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
6467
public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthenticator {
6568
public static final Logger s_logger = Logger.getLogger(ListAndSwitchSAMLAccountCmd.class.getName());
@@ -104,7 +107,9 @@ public String authenticate(final String command, final Map<String, Object[]> par
104107
params, responseType));
105108
}
106109

107-
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
110+
HttpUtils.ApiSessionKeyCheckOption sessionKeyCheckOption = EnumUtils.getEnumIgnoreCase(HttpUtils.ApiSessionKeyCheckOption.class,
111+
ApiServer.ApiSessionKeyCheckLocations.value(), HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter);
112+
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY, sessionKeyCheckOption)) {
108113
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
109114
"Unauthorized session, please re-login",
110115
params, responseType));

plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManager.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableSe
7373
ConfigKey<Boolean> SAMLCheckSignature = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.check.signature", "true",
7474
"When enabled (default and recommended), SAML2 signature checks are enforced and lack of signature in the SAML SSO response will cause login exception. Disabling this is not advisable but provided for backward compatibility for users who are able to accept the risks.", false);
7575

76+
ConfigKey<String> SAMLUserSessionKeyPathAttribute = new ConfigKey<String>("Advanced", String.class, "saml2.user.sessionkey.path", "",
77+
"The Path attribute of sessionkey cookie when SAML users have logged in. If not set, it will be set to the path of SAML redirection URL (saml2.redirect.url).", true);
78+
7679
SAMLProviderMetadata getSPMetadata();
7780
SAMLProviderMetadata getIdPMetadata(String entityId);
7881
Collection<SAMLProviderMetadata> getAllIdPMetadata();

plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ public ConfigKey<?>[] getConfigKeys() {
542542
SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
543543
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
544544
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
545-
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature};
545+
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature,
546+
SAMLUserSessionKeyPathAttribute};
546547
}
547548
}

plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAMLUtils.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import java.io.StringWriter;
2626
import java.io.UnsupportedEncodingException;
2727
import java.math.BigInteger;
28+
import java.net.URI;
29+
import java.net.URISyntaxException;
2830
import java.net.URLEncoder;
2931
import java.nio.charset.Charset;
3032
import java.security.InvalidKeyException;
@@ -101,7 +103,9 @@
101103
import org.w3c.dom.Element;
102104
import org.xml.sax.SAXException;
103105

106+
import com.cloud.api.ApiServlet;
104107
import com.cloud.utils.HttpUtils;
108+
import com.cloud.utils.exception.CloudRuntimeException;
105109

106110
public class SAMLUtils {
107111
public static final Logger s_logger = Logger.getLogger(SAMLUtils.class);
@@ -296,7 +300,26 @@ public static void setupSamlUserCookies(final LoginCmdResponse loginResponse, fi
296300
resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
297301
}
298302
resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
299-
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;Path=/client/api", ApiConstants.SESSIONKEY, loginResponse.getSessionKey()));
303+
304+
String redirectUrl = SAML2AuthManager.SAMLCloudStackRedirectionUrl.value();
305+
String path = SAML2AuthManager.SAMLUserSessionKeyPathAttribute.value();
306+
String domain = null;
307+
try {
308+
URI redirectUri = new URI(redirectUrl);
309+
domain = redirectUri.getHost();
310+
if (StringUtils.isBlank(path)) {
311+
path = redirectUri.getPath();
312+
}
313+
if (StringUtils.isBlank(path)) {
314+
path = "/";
315+
}
316+
} catch (URISyntaxException ex) {
317+
throw new CloudRuntimeException("Invalid URI: " + redirectUrl);
318+
}
319+
String sameSite = ApiServlet.getApiSessionKeySameSite();
320+
String sessionKeyCookie = String.format("%s=%s;Domain=%s;Path=%s;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), domain, path, sameSite);
321+
s_logger.debug("Adding sessionkey cookie to response: " + sessionKeyCookie);
322+
resp.addHeader("SET-COOKIE", sessionKeyCookie);
300323
}
301324

302325
/**

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.HashMap;
2828
import java.util.Map;
2929

30+
import javax.servlet.http.Cookie;
3031
import javax.servlet.http.HttpServletRequest;
3132
import javax.servlet.http.HttpServletResponse;
3233
import javax.servlet.http.HttpSession;
@@ -88,13 +89,17 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
8889
@Mock
8990
HttpServletRequest req;
9091

92+
final String sessionId = "node0xxxxxxxxxxxxx";
93+
Cookie[] cookies;
94+
9195
@Test
9296
public void testListAndSwitchSAMLAccountCmd() throws Exception {
9397
// Setup
9498
final Map<String, Object[]> params = new HashMap<String, Object[]>();
9599
final String sessionKeyValue = "someSessionIDValue";
96100
Mockito.when(session.getAttribute(ApiConstants.SESSIONKEY)).thenReturn(sessionKeyValue);
97101
Mockito.when(session.getAttribute("userid")).thenReturn(2L);
102+
Mockito.when(session.getId()).thenReturn(sessionId);
98103
params.put(ApiConstants.USER_ID, new String[]{"2"});
99104
params.put(ApiConstants.DOMAIN_ID, new String[]{"1"});
100105
Mockito.when(userDao.findByUuid(anyString())).thenReturn(new UserVO(2L));
@@ -146,7 +151,25 @@ public void testListAndSwitchSAMLAccountCmd() throws Exception {
146151
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
147152
}
148153

149-
// valid sessionkey value test
154+
// valid sessionkey value and invalid JSESSIONID test
155+
cookies = new Cookie[2];
156+
cookies[0] = new Cookie(ApiConstants.SESSIONKEY, sessionKeyValue);
157+
cookies[1] = new Cookie("JSESSIONID", "invalid-JSESSIONID");
158+
Mockito.when(req.getCookies()).thenReturn(cookies);
159+
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
160+
try {
161+
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
162+
} catch (ServerApiException exception) {
163+
assertEquals(exception.getErrorCode(), ApiErrorCode.UNAUTHORIZED);
164+
} finally {
165+
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
166+
}
167+
168+
// valid sessionkey value and valid JSESSIONID test
169+
cookies = new Cookie[2];
170+
cookies[0] = new Cookie(ApiConstants.SESSIONKEY, sessionKeyValue);
171+
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
172+
Mockito.when(req.getCookies()).thenReturn(cookies);
150173
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
151174
try {
152175
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);

server/src/main/java/com/cloud/api/ApiServer.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.ArrayList;
3434
import java.util.Collections;
3535
import java.util.Date;
36+
import java.util.EnumSet;
3637
import java.util.Enumeration;
3738
import java.util.HashMap;
3839
import java.util.HashSet;
@@ -47,6 +48,7 @@
4748
import java.util.concurrent.TimeUnit;
4849
import java.util.regex.Matcher;
4950
import java.util.regex.Pattern;
51+
import java.util.stream.Collectors;
5052

5153
import javax.crypto.Mac;
5254
import javax.crypto.spec.SecretKeySpec;
@@ -168,6 +170,8 @@
168170
import com.cloud.utils.ConstantTimeComparator;
169171
import com.cloud.utils.DateUtil;
170172
import com.cloud.utils.HttpUtils;
173+
import com.cloud.utils.HttpUtils.ApiSessionKeySameSite;
174+
import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption;
171175
import com.cloud.utils.Pair;
172176
import com.cloud.utils.ReflectUtil;
173177
import com.cloud.utils.StringUtils;
@@ -306,6 +310,24 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
306310
, true
307311
, ConfigKey.Scope.Global);
308312

313+
static final ConfigKey<String> ApiSessionKeyCookieSameSiteSetting = new ConfigKey<>(String.class
314+
, "api.sessionkey.cookie.samesite"
315+
, ConfigKey.CATEGORY_ADVANCED
316+
, ApiSessionKeySameSite.Lax.name()
317+
, "The SameSite attribute of cookie 'sessionkey'. Valid options are: Lax (default), Strict, NoneAndSecure and Null."
318+
, true
319+
, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select,
320+
EnumSet.allOf(ApiSessionKeySameSite.class).stream().map(Enum::toString).collect(Collectors.joining(", ")));
321+
322+
public static final ConfigKey<String> ApiSessionKeyCheckLocations = new ConfigKey<>(String.class
323+
, "api.sessionkey.check.locations"
324+
, ConfigKey.CATEGORY_ADVANCED
325+
, ApiSessionKeyCheckOption.CookieAndParameter.name()
326+
, "The locations of 'sessionkey' during the validation of the API requests. Valid options are: CookieOrParameter, ParameterOnly, CookieAndParameter (default)."
327+
, true
328+
, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select,
329+
EnumSet.allOf(ApiSessionKeyCheckOption.class).stream().map(Enum::toString).collect(Collectors.joining(", ")));
330+
309331
@Override
310332
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
311333
messageBus.subscribe(AsyncJob.Topics.JOB_EVENT_PUBLISH, MessageDispatcher.getDispatcher(this));
@@ -1531,7 +1553,9 @@ public ConfigKey<?>[] getConfigKeys() {
15311553
JSONDefaultContentType,
15321554
proxyForwardList,
15331555
useForwardHeader,
1534-
listOfForwardHeaders
1556+
listOfForwardHeaders,
1557+
ApiSessionKeyCookieSameSiteSetting,
1558+
ApiSessionKeyCheckLocations
15351559
};
15361560
}
15371561
}

0 commit comments

Comments
 (0)