util: check JSESSIONID in cookies if user is passed

This commit is contained in:
Wei Zhou 2024-09-10 18:25:36 +02:00 committed by Daan Hoogland
parent 1861e78361
commit 124d6b8b81
12 changed files with 266 additions and 65 deletions

View File

@ -48,6 +48,7 @@ import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.cloudstack.saml.SAMLUtils;
import org.apache.log4j.Logger;
import com.cloud.api.ApiServer;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
@ -60,6 +61,8 @@ import com.cloud.user.dao.UserAccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.HttpUtils;
import org.apache.commons.lang3.EnumUtils;
@APICommand(name = "listAndSwitchSamlAccount", description = "Lists and switches to other SAML accounts owned by the SAML user", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthenticator {
public static final Logger s_logger = Logger.getLogger(ListAndSwitchSAMLAccountCmd.class.getName());
@ -104,7 +107,9 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
params, responseType));
}
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
HttpUtils.ApiSessionKeyCheckOption sessionKeyCheckOption = EnumUtils.getEnumIgnoreCase(HttpUtils.ApiSessionKeyCheckOption.class,
ApiServer.ApiSessionKeyCheckLocations.value(), HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter);
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY, sessionKeyCheckOption)) {
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
"Unauthorized session, please re-login",
params, responseType));

View File

@ -73,6 +73,9 @@ public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableSe
ConfigKey<Boolean> SAMLCheckSignature = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.check.signature", "true",
"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);
ConfigKey<String> SAMLUserSessionKeyPathAttribute = new ConfigKey<String>("Advanced", String.class, "saml2.user.sessionkey.path", "",
"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);
SAMLProviderMetadata getSPMetadata();
SAMLProviderMetadata getIdPMetadata(String entityId);
Collection<SAMLProviderMetadata> getAllIdPMetadata();

View File

@ -542,6 +542,7 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature};
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature,
SAMLUserSessionKeyPathAttribute};
}
}

View File

@ -25,6 +25,8 @@ import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
@ -101,7 +103,9 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import com.cloud.api.ApiServlet;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.exception.CloudRuntimeException;
public class SAMLUtils {
public static final Logger s_logger = Logger.getLogger(SAMLUtils.class);
@ -296,7 +300,26 @@ public class SAMLUtils {
resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
}
resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;Path=/client/api", ApiConstants.SESSIONKEY, loginResponse.getSessionKey()));
String redirectUrl = SAML2AuthManager.SAMLCloudStackRedirectionUrl.value();
String path = SAML2AuthManager.SAMLUserSessionKeyPathAttribute.value();
String domain = null;
try {
URI redirectUri = new URI(redirectUrl);
domain = redirectUri.getHost();
if (StringUtils.isBlank(path)) {
path = redirectUri.getPath();
}
if (StringUtils.isBlank(path)) {
path = "/";
}
} catch (URISyntaxException ex) {
throw new CloudRuntimeException("Invalid URI: " + redirectUrl);
}
String sameSite = ApiServlet.getApiSessionKeySameSite();
String sessionKeyCookie = String.format("%s=%s;Domain=%s;Path=%s;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), domain, path, sameSite);
s_logger.debug("Adding sessionkey cookie to response: " + sessionKeyCookie);
resp.addHeader("SET-COOKIE", sessionKeyCookie);
}
/**

View File

@ -27,6 +27,7 @@ import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@ -88,6 +89,9 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
@Mock
HttpServletRequest req;
final String sessionId = "node0xxxxxxxxxxxxx";
Cookie[] cookies;
@Test
public void testListAndSwitchSAMLAccountCmd() throws Exception {
// Setup
@ -95,6 +99,7 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
final String sessionKeyValue = "someSessionIDValue";
Mockito.when(session.getAttribute(ApiConstants.SESSIONKEY)).thenReturn(sessionKeyValue);
Mockito.when(session.getAttribute("userid")).thenReturn(2L);
Mockito.when(session.getId()).thenReturn(sessionId);
params.put(ApiConstants.USER_ID, new String[]{"2"});
params.put(ApiConstants.DOMAIN_ID, new String[]{"1"});
Mockito.when(userDao.findByUuid(anyString())).thenReturn(new UserVO(2L));
@ -146,7 +151,25 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
}
// valid sessionkey value test
// valid sessionkey value and invalid JSESSIONID test
cookies = new Cookie[2];
cookies[0] = new Cookie(ApiConstants.SESSIONKEY, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", "invalid-JSESSIONID");
Mockito.when(req.getCookies()).thenReturn(cookies);
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
try {
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
assertEquals(exception.getErrorCode(), ApiErrorCode.UNAUTHORIZED);
} finally {
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
}
// valid sessionkey value and valid JSESSIONID test
cookies = new Cookie[2];
cookies[0] = new Cookie(ApiConstants.SESSIONKEY, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
Mockito.when(req.getCookies()).thenReturn(cookies);
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
try {
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);

View File

@ -33,6 +33,7 @@ import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
@ -47,6 +48,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@ -168,6 +170,8 @@ import com.cloud.user.UserVO;
import com.cloud.utils.ConstantTimeComparator;
import com.cloud.utils.DateUtil;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.HttpUtils.ApiSessionKeySameSite;
import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption;
import com.cloud.utils.Pair;
import com.cloud.utils.ReflectUtil;
import com.cloud.utils.StringUtils;
@ -306,6 +310,24 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
, true
, ConfigKey.Scope.Global);
static final ConfigKey<String> ApiSessionKeyCookieSameSiteSetting = new ConfigKey<>(String.class
, "api.sessionkey.cookie.samesite"
, ConfigKey.CATEGORY_ADVANCED
, ApiSessionKeySameSite.Lax.name()
, "The SameSite attribute of cookie 'sessionkey'. Valid options are: Lax (default), Strict, NoneAndSecure and Null."
, true
, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select,
EnumSet.allOf(ApiSessionKeySameSite.class).stream().map(Enum::toString).collect(Collectors.joining(", ")));
public static final ConfigKey<String> ApiSessionKeyCheckLocations = new ConfigKey<>(String.class
, "api.sessionkey.check.locations"
, ConfigKey.CATEGORY_ADVANCED
, ApiSessionKeyCheckOption.CookieAndParameter.name()
, "The locations of 'sessionkey' during the validation of the API requests. Valid options are: CookieOrParameter, ParameterOnly, CookieAndParameter (default)."
, true
, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select,
EnumSet.allOf(ApiSessionKeyCheckOption.class).stream().map(Enum::toString).collect(Collectors.joining(", ")));
@Override
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
messageBus.subscribe(AsyncJob.Topics.JOB_EVENT_PUBLISH, MessageDispatcher.getDispatcher(this));
@ -1531,7 +1553,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
JSONDefaultContentType,
proxyForwardList,
useForwardHeader,
listOfForwardHeaders
listOfForwardHeaders,
ApiSessionKeyCookieSameSiteSetting,
ApiSessionKeyCheckLocations
};
}
}

View File

@ -47,6 +47,7 @@ import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
import org.apache.log4j.Logger;
import org.apache.commons.lang3.EnumUtils;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
@ -63,13 +64,15 @@ import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.HttpUtils.ApiSessionKeySameSite;
import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.net.NetUtils;
@Component("apiServlet")
public class ApiServlet extends HttpServlet {
public static final Logger s_logger = Logger.getLogger(ApiServlet.class.getName());
public static final Logger LOGGER = Logger.getLogger(ApiServlet.class.getName());
private static final Logger s_accessLogger = Logger.getLogger("apiserver." + ApiServlet.class.getName());
private static final String REPLACEMENT = "_";
private static final String LOG_REPLACEMENTS = "[\n\r\t]";
@ -127,7 +130,7 @@ public class ApiServlet extends HttpServlet {
String value = decodeUtf8(paramTokens[1]);
params.put(name, new String[] {value});
} else {
s_logger.debug("Invalid parameter in URL found. param: " + param);
LOGGER.debug("Invalid parameter in URL found. param: " + param);
}
}
}
@ -156,7 +159,7 @@ public class ApiServlet extends HttpServlet {
if (v.length > 1) {
String message = String.format("Query parameter '%s' has multiple values %s. Only the last value will be respected." +
"It is advised to pass only a single parameter", k, Arrays.toString(v));
s_logger.warn(message);
LOGGER.warn(message);
}
});
@ -167,7 +170,7 @@ public class ApiServlet extends HttpServlet {
try {
remoteAddress = getClientAddress(req);
} catch (UnknownHostException e) {
s_logger.warn("UnknownHostException when trying to lookup remote IP-Address. This should never happen. Blocking request.", e);
LOGGER.warn("UnknownHostException when trying to lookup remote IP-Address. This should never happen. Blocking request.", e);
final String response = apiServer.getSerializedApiError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"UnknownHostException when trying to lookup remote IP-Address", null,
HttpUtils.RESPONSE_TYPE_XML);
@ -191,17 +194,17 @@ public class ApiServlet extends HttpServlet {
// logging the request start and end in management log for easy debugging
String reqStr = "";
String cleanQueryString = StringUtils.cleanString(req.getQueryString());
if (s_logger.isDebugEnabled()) {
if (LOGGER.isDebugEnabled()) {
reqStr = auditTrailSb.toString() + " " + cleanQueryString;
s_logger.debug("===START=== " + reqStr);
LOGGER.debug("===START=== " + reqStr);
}
try {
resp.setContentType(HttpUtils.XML_CONTENT_TYPE);
HttpSession session = req.getSession(false);
if (s_logger.isTraceEnabled()) {
s_logger.trace(String.format("session found: %s", session));
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("session found: %s", session));
}
final Object[] responseTypeParam = params.get(ApiConstants.RESPONSE);
if (responseTypeParam != null) {
@ -212,10 +215,10 @@ public class ApiServlet extends HttpServlet {
final String command = commandObj == null ? null : (String) commandObj[0];
final Object[] userObj = params.get(ApiConstants.USERNAME);
String username = userObj == null ? null : (String)userObj[0];
if (s_logger.isTraceEnabled()) {
if (LOGGER.isTraceEnabled()) {
String logCommand = saveLogString(command);
String logName = saveLogString(username);
s_logger.trace(String.format("command %s processing for user \"%s\"",
LOGGER.trace(String.format("command %s processing for user \"%s\"",
logCommand,
logName));
}
@ -238,25 +241,26 @@ public class ApiServlet extends HttpServlet {
if (ApiServer.EnableSecureSessionCookie.value()) {
resp.setHeader("SET-COOKIE", String.format("JSESSIONID=%s;Secure;HttpOnly;Path=/client", session.getId()));
if (s_logger.isDebugEnabled()) {
s_logger.debug("Session cookie is marked secure!");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Session cookie is marked secure!");
}
}
}
try {
if (s_logger.isTraceEnabled()) {
s_logger.trace(String.format("apiAuthenticator.authenticate(%s, params[%d], %s, %s, %s, %s, %s,%s)",
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("apiAuthenticator.authenticate(%s, params[%d], %s, %s, %s, %s, %s,%s)",
saveLogString(command), params.size(), session.getId(), remoteAddress.getHostAddress(), saveLogString(responseType), "auditTrailSb", "req", "resp"));
}
responseString = apiAuthenticator.authenticate(command, params, session, remoteAddress, responseType, auditTrailSb, req, resp);
if (session != null && session.getAttribute(ApiConstants.SESSIONKEY) != null) {
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", ApiConstants.SESSIONKEY, session.getAttribute(ApiConstants.SESSIONKEY)));
String sameSite = getApiSessionKeySameSite();
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;%s", ApiConstants.SESSIONKEY, session.getAttribute(ApiConstants.SESSIONKEY), sameSite));
}
} catch (ServerApiException e) {
httpResponseCode = e.getErrorCode().getHttpCode();
responseString = e.getMessage();
s_logger.debug("Authentication failure: " + e.getMessage());
LOGGER.debug("Authentication failure: " + e.getMessage());
}
if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) {
@ -289,7 +293,7 @@ public class ApiServlet extends HttpServlet {
return;
}
} else {
s_logger.trace("no command available");
LOGGER.trace("no command available");
}
auditTrailSb.append(cleanQueryString);
final boolean isNew = ((session == null) ? true : session.isNew());
@ -298,15 +302,15 @@ public class ApiServlet extends HttpServlet {
// we no longer rely on web-session here, verifyRequest will populate user/account information
// if a API key exists
if (isNew && s_logger.isTraceEnabled()) {
s_logger.trace(String.format("new session: %s", session));
if (isNew && LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("new session: %s", session));
}
if (!isNew && (command.equalsIgnoreCase(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME) || (!skip2FAcheckForAPIs(command) && !skip2FAcheckForUser(session)))) {
s_logger.debug("Verifying two factor authentication");
LOGGER.debug("Verifying two factor authentication");
boolean success = verify2FA(session, command, auditTrailSb, params, remoteAddress, responseType, req, resp);
if (!success) {
s_logger.debug("Verification of two factor authentication failed");
LOGGER.debug("Verification of two factor authentication failed");
return;
}
}
@ -319,8 +323,8 @@ public class ApiServlet extends HttpServlet {
if (account != null) {
if (invalidateHttpSessionIfNeeded(req, resp, auditTrailSb, responseType, params, session, account)) return;
} else {
if (s_logger.isDebugEnabled()) {
s_logger.debug("no account, this request will be validated through apikey(%s)/signature");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("no account, this request will be validated through apikey(%s)/signature");
}
}
@ -330,8 +334,8 @@ public class ApiServlet extends HttpServlet {
CallContext.register(accountMgr.getSystemUser(), accountMgr.getSystemAccount());
}
setProjectContext(params);
if (s_logger.isTraceEnabled()) {
s_logger.trace(String.format("verifying request for user %s from %s with %d parameters",
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("verifying request for user %s from %s with %d parameters",
userId, remoteAddress.getHostAddress(), params.size()));
}
if (apiServer.verifyRequest(params, userId, remoteAddress)) {
@ -362,18 +366,34 @@ public class ApiServlet extends HttpServlet {
HttpUtils.writeHttpResponse(resp, serializedResponseText, se.getErrorCode().getHttpCode(), responseType, ApiServer.JSONcontentType.value());
auditTrailSb.append(" " + se.getErrorCode() + " " + se.getDescription());
} catch (final Exception ex) {
s_logger.error("unknown exception writing api response", ex);
LOGGER.error("unknown exception writing api response", ex);
auditTrailSb.append(" unknown exception writing api response");
} finally {
s_accessLogger.info(auditTrailSb.toString());
if (s_logger.isDebugEnabled()) {
s_logger.debug("===END=== " + reqStr);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("===END=== " + reqStr);
}
// cleanup user context to prevent from being peeked in other request context
CallContext.unregister();
}
}
public static String getApiSessionKeySameSite() {
ApiSessionKeySameSite sameSite = EnumUtils.getEnumIgnoreCase(ApiSessionKeySameSite.class,
ApiServer.ApiSessionKeyCookieSameSiteSetting.value(), ApiSessionKeySameSite.Lax);
switch (sameSite) {
case Strict:
return "SameSite=Strict";
case NoneAndSecure:
return "SameSite=None;Secure";
case Null:
return "";
case Lax:
default:
return "SameSite=Lax";
}
}
private boolean checkIfAuthenticatorIsOf2FA(String command) {
boolean verify2FA = false;
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
@ -402,7 +422,7 @@ public class ApiServlet extends HttpServlet {
Long userId = (Long) session.getAttribute("userid");
boolean is2FAverified = (boolean) session.getAttribute(ApiConstants.IS_2FA_VERIFIED);
if (is2FAverified) {
s_logger.debug(String.format("Two factor authentication is already verified for the user %d, so skipping", userId));
LOGGER.debug(String.format("Two factor authentication is already verified for the user %d, so skipping", userId));
skip2FAcheck = true;
} else {
UserAccount userAccount = accountMgr.getUserAccountById(userId);
@ -433,7 +453,7 @@ public class ApiServlet extends HttpServlet {
HttpUtils.writeHttpResponse(resp, responseString, HttpServletResponse.SC_OK, responseType, ApiServer.JSONcontentType.value());
verify2FA = true;
} else {
s_logger.error("Cannot find API authenticator while verifying 2FA");
LOGGER.error("Cannot find API authenticator while verifying 2FA");
auditTrailSb.append(" Cannot find API authenticator while verifying 2FA");
verify2FA = false;
}
@ -457,7 +477,7 @@ public class ApiServlet extends HttpServlet {
errorMsg = "Two factor authentication is mandated by admin, user needs to setup 2FA using setupUserTwoFactorAuthentication API and" +
" then verify 2FA using validateUserTwoFactorAuthenticationCode API before calling other APIs. Existing session is invalidated.";
}
s_logger.error(errorMsg);
LOGGER.error(errorMsg);
invalidateHttpSession(session, String.format("Unable to process the API request for %s from %s due to %s", userId, remoteAddress.getHostAddress(), errorMsg));
auditTrailSb.append(" " + ApiErrorCode.UNAUTHORIZED2FA + " " + errorMsg);
@ -488,7 +508,7 @@ public class ApiServlet extends HttpServlet {
private boolean requestChecksoutAsSane(HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String command, Long userId, String account, Object accountObj) {
if ((userId != null) && (account != null) && (accountObj != null) && apiServer.verifyUser(userId)) {
if (command == null) {
s_logger.info("missing command, ignoring request...");
LOGGER.info("missing command, ignoring request...");
auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + "no command specified");
final String serializedResponse = apiServer.getSerializedApiError(HttpServletResponse.SC_BAD_REQUEST, "no command specified", params, responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType, ApiServer.JSONcontentType.value());
@ -509,7 +529,9 @@ public class ApiServlet extends HttpServlet {
}
private boolean invalidateHttpSessionIfNeeded(HttpServletRequest req, HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String account) {
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
ApiSessionKeyCheckOption sessionKeyCheckOption = EnumUtils.getEnumIgnoreCase(ApiSessionKeyCheckOption.class,
ApiServer.ApiSessionKeyCheckLocations.value(), ApiSessionKeyCheckOption.CookieAndParameter);
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY, sessionKeyCheckOption)) {
String msg = String.format("invalidating session %s for account %s", session.getId(), account);
invalidateHttpSession(session, msg);
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials");
@ -523,13 +545,13 @@ public class ApiServlet extends HttpServlet {
public static void invalidateHttpSession(HttpSession session, String msg) {
try {
if (s_logger.isTraceEnabled()) {
s_logger.trace(msg);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(msg);
}
session.invalidate();
} catch (final IllegalStateException ise) {
if (s_logger.isTraceEnabled()) {
s_logger.trace(String.format("failed to invalidate session %s", session.getId()));
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("failed to invalidate session %s", session.getId()));
}
}
}
@ -537,7 +559,7 @@ public class ApiServlet extends HttpServlet {
private void setProjectContext(Map<String, Object[]> requestParameters) {
final String[] command = (String[])requestParameters.get(ApiConstants.COMMAND);
if (command == null) {
s_logger.info("missing command, ignoring request...");
LOGGER.info("missing command, ignoring request...");
return;
}
@ -585,14 +607,14 @@ public class ApiServlet extends HttpServlet {
header = header.trim();
ip = getCorrectIPAddress(request.getHeader(header));
if (StringUtils.isNotBlank(ip)) {
s_logger.debug(String.format("found ip %s in header %s ", ip, header));
LOGGER.debug(String.format("found ip %s in header %s ", ip, header));
break;
}
} // no address found in header so ip is blank and use remote addr
} // else not an allowed proxy address, ip is blank and use remote addr
}
if (StringUtils.isBlank(ip)) {
s_logger.trace(String.format("no ip found in headers, returning remote address %s.", pretender.getHostAddress()));
LOGGER.trace(String.format("no ip found in headers, returning remote address %s.", pretender.getHostAddress()));
return pretender;
}

View File

@ -133,6 +133,7 @@ class TestLogin(cloudstackTestCase):
args["command"] = 'listUsers'
args["listall"] = 'true'
args["response"] = "json"
args["sessionkey"] = response.json()['loginresponse']['sessionkey']
response = session.get(self.server_url, params=args)
self.assertEqual(
response.status_code,

View File

@ -15,8 +15,13 @@
// specific language governing permissions and limitations
// under the License.
import Cookies from 'js-cookie'
import { axios, sourceToken } from '@/utils/request'
import { message, notification } from 'ant-design-vue'
import { vueProps } from '@/vue-app'
import {
ACCESS_TOKEN
} from '@/store/mutation-types'
export function api (command, args = {}, method = 'GET', data = {}) {
let params = {}
@ -30,6 +35,11 @@ export function api (command, args = {}, method = 'GET', data = {}) {
})
}
const sessionkey = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('sessionkey')
if (sessionkey) {
args.sessionkey = sessionkey
}
return axios({
params: {
...args

View File

@ -24,7 +24,6 @@ import router from '@/router'
import store from '@/store'
import { login, logout, api } from '@/api'
import { i18n } from '@/locales'
import { sourceToken } from '@/utils/request'
import {
ACCESS_TOKEN,

View File

@ -37,6 +37,14 @@ public class HttpUtils {
public static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8";
public static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8";
public enum ApiSessionKeySameSite {
Lax, Strict, NoneAndSecure, Null
}
public enum ApiSessionKeyCheckOption {
CookieOrParameter, ParameterOnly, CookieAndParameter
}
public static void addSecurityHeaders(final HttpServletResponse resp) {
if (resp.containsHeader("X-Content-Type-Options")) {
resp.setHeader("X-Content-Type-Options", "nosniff");
@ -103,23 +111,43 @@ public class HttpUtils {
return null;
}
public static boolean validateSessionKey(final HttpSession session, final Map<String, Object[]> params, final Cookie[] cookies, final String sessionKeyString) {
public static boolean validateSessionKey(final HttpSession session, final Map<String, Object[]> params, final Cookie[] cookies, final String sessionKeyString, final ApiSessionKeyCheckOption apiSessionKeyCheckLocations) {
if (session == null || sessionKeyString == null) {
return false;
}
final String jsessionidFromCookie = HttpUtils.findCookie(cookies, "JSESSIONID");
if (jsessionidFromCookie == null
|| !(jsessionidFromCookie.startsWith(session.getId() + '.'))) {
s_logger.error("JSESSIONID from cookie is invalid.");
return false;
}
final String sessionKey = (String) session.getAttribute(sessionKeyString);
if (sessionKey == null) {
s_logger.error("sessionkey attribute of the session is null.");
return false;
}
final String sessionKeyFromCookie = HttpUtils.findCookie(cookies, sessionKeyString);
boolean isSessionKeyFromCookieValid = sessionKeyFromCookie != null && sessionKey.equals(sessionKeyFromCookie);
String[] sessionKeyFromParams = null;
if (params != null) {
sessionKeyFromParams = (String[]) params.get(sessionKeyString);
}
if ((sessionKey == null)
|| (sessionKeyFromParams == null && sessionKeyFromCookie == null)
|| (sessionKeyFromParams != null && !sessionKey.equals(sessionKeyFromParams[0]))
|| (sessionKeyFromCookie != null && !sessionKey.equals(sessionKeyFromCookie))) {
return false;
boolean isSessionKeyFromParamsValid = sessionKeyFromParams != null && sessionKey.equals(sessionKeyFromParams[0]);
switch (apiSessionKeyCheckLocations) {
case CookieOrParameter:
return (sessionKeyFromCookie != null || sessionKeyFromParams != null)
&& (sessionKeyFromCookie == null || isSessionKeyFromCookieValid)
&& (sessionKeyFromParams == null || isSessionKeyFromParamsValid);
case ParameterOnly:
return sessionKeyFromParams != null && isSessionKeyFromParamsValid
&& (sessionKeyFromCookie == null || isSessionKeyFromCookieValid);
case CookieAndParameter:
default:
return sessionKeyFromCookie != null && isSessionKeyFromCookieValid
&& sessionKeyFromParams != null && isSessionKeyFromParamsValid;
}
return true;
}
}

View File

@ -60,35 +60,97 @@ public class HttpUtilsTest {
final String sessionKeyValue = "randomUniqueSessionID";
// session and sessionKeyString null test
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
sessionKeyString = "sessionkey";
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param and cookie null test
session = new MockHttpSession();
final String sessionId = session.getId();
session.setAttribute(sessionKeyString, sessionKeyValue);
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param null, cookies not null test
// param null, cookies not null test (JSESSIONID is null)
params = null;
cookies = new Cookie[]{new Cookie(sessionKeyString, sessionKeyValue)};
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString"));
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString", HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param null, cookies not null test (JSESSIONID is not null and matches)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString", HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param null, cookies not null test (JSESSIONID is not null but mismatches)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", "node0xxxxxxxxxxxxx.node0");
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString", HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param not null, cookies null test
params = new HashMap<String, Object[]>();
params.put(sessionKeyString, new String[]{"randomString"});
cookies = null;
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// both param and cookies not null test
// both param and cookies not null test (JSESSIONID is null)
params = new HashMap<String, Object[]>();
cookies = new Cookie[]{new Cookie(sessionKeyString, sessionKeyValue)};
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// both param and cookies not null test (JSESSIONID is not null but mismatches)
params = new HashMap<String, Object[]>();
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", "node0xxxxxxxxxxxxx.node0");
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// both param and cookies not null test (JSESSIONID is not null amd matches)
params = new HashMap<String, Object[]>();
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param not null, cookies null test (JSESSIONID is not null amd matches)
params = new HashMap<String, Object[]>();
cookies = new Cookie[1];
cookies[0] = new Cookie("JSESSIONID", sessionId + ".node0");
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
// param not null (correct), cookies not null test (correct)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
// param not null (correct), cookies not null test (wrong)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, "incorrectValue");
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
}
}