diff --git a/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java index e8c7f0f5eda..fac969dbfcc 100644 --- a/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java @@ -17,5 +17,5 @@ package org.apache.cloudstack.api.auth; public enum APIAuthenticationType { - LOGIN_API, LOGOUT_API + LOGIN_API, LOGOUT_API, READONLY_API } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java index 466a3ddc533..75353f2c3c4 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java @@ -256,7 +256,7 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent @Override public APIAuthenticationType getAPIType() { - return APIAuthenticationType.LOGIN_API; + return APIAuthenticationType.READONLY_API; } @Override diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java index 0592425a1a0..ad387044b5f 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java @@ -97,7 +97,7 @@ public class ListIdpsCmd extends BaseCmd implements APIAuthenticator { @Override public APIAuthenticationType getAPIType() { - return APIAuthenticationType.LOGIN_API; + return APIAuthenticationType.READONLY_API; } @Override diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java index 26a4c9b4b58..8e67408b5f0 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java @@ -328,13 +328,13 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent resp.addCookie(new Cookie("domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie(ApiConstants.SESSIONKEY, URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); String timezone = loginResponse.getTimeZone(); if (timezone != null) { 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", ApiConstants.SESSIONKEY, loginResponse.getSessionKey())); resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value()); return ApiResponseSerializer.toSerializedString(loginResponse, responseType); } diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java index db1323fa6df..86009ac4e5c 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java @@ -99,6 +99,6 @@ public class GetServiceProviderMetaDataCmdTest { @Test public void testGetAPIType() { - Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API); + Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.READONLY_API); } } diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java index 6960b3bb502..0c72512082d 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java @@ -197,6 +197,6 @@ public class SAML2LoginAPIAuthenticatorCmdTest { @Test public void testGetAPIType() { - Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API); + Assert.assertTrue(new SAML2LoginAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGIN_API); } } diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 2a2844ef015..e30a6ca2ba2 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -16,22 +16,13 @@ // under the License. package com.cloud.api; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.Arrays; -import java.util.Collections; -import java.net.InetAddress; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.User; +import com.cloud.utils.HttpUtils; +import com.cloud.utils.StringUtils; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.net.NetUtils; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiServerService; @@ -45,13 +36,22 @@ import org.apache.log4j.Logger; import org.springframework.stereotype.Component; import org.springframework.web.context.support.SpringBeanAutowiringSupport; -import com.cloud.user.Account; -import com.cloud.user.AccountService; -import com.cloud.user.User; -import com.cloud.utils.HttpUtils; -import com.cloud.utils.StringUtils; -import com.cloud.utils.db.EntityManager; -import com.cloud.utils.net.NetUtils; +import javax.inject.Inject; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.URLDecoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Component("apiServlet") @SuppressWarnings("serial") @@ -187,7 +187,7 @@ public class ApiServlet extends HttpServlet { } session = req.getSession(true); if (ApiServer.isSecureSessionCookieEnabled()) { - resp.setHeader("SET-COOKIE", "JSESSIONID=" + session.getId() + ";Secure;Path=/client"); + resp.setHeader("SET-COOKIE", String.format("JSESSIONID=%s;Secure;HttpOnly;Path=/client", session.getId())); if (s_logger.isDebugEnabled()) { if (s_logger.isDebugEnabled()) { s_logger.debug("Session cookie is marked secure!"); @@ -198,11 +198,15 @@ public class ApiServlet extends HttpServlet { try { responseString = apiAuthenticator.authenticate(command, params, session, InetAddress.getByName(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))); + } } catch (ServerApiException e) { httpResponseCode = e.getErrorCode().getHttpCode(); responseString = e.getMessage(); s_logger.debug("Authentication failure: " + e.getMessage()); } + if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) { if (session != null) { final Long userId = (Long) session.getAttribute("userid"); @@ -220,6 +224,9 @@ public class ApiServlet extends HttpServlet { } catch (final IllegalStateException ignored) { } } + Cookie sessionKeyCookie = new Cookie(ApiConstants.SESSIONKEY, ""); + sessionKeyCookie.setMaxAge(0); + resp.addCookie(sessionKeyCookie); } HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType, ApiServer.getJSONContentType()); return; @@ -236,11 +243,15 @@ public class ApiServlet extends HttpServlet { if (!isNew) { userId = (Long)session.getAttribute("userid"); - final String account = (String)session.getAttribute("account"); + final String account = (String) session.getAttribute("account"); final Object accountObj = session.getAttribute("accountobj"); - final String sessionKey = (String)session.getAttribute(ApiConstants.SESSIONKEY); - final String[] sessionKeyParam = (String[])params.get(ApiConstants.SESSIONKEY); - if ((sessionKeyParam == null) || (sessionKey == null) || !sessionKey.equals(sessionKeyParam[0])) { + final String sessionKey = (String) session.getAttribute(ApiConstants.SESSIONKEY); + final String sessionKeyFromCookie = HttpUtils.findCookie(req.getCookies(), ApiConstants.SESSIONKEY); + final String[] sessionKeyFromParams = (String[]) params.get(ApiConstants.SESSIONKEY); + if ((sessionKey == null) + || (sessionKeyFromParams == null && sessionKeyFromCookie == null) + || (sessionKeyFromParams != null && !sessionKey.equals(sessionKeyFromParams[0])) + || (sessionKeyFromCookie != null && !sessionKey.equals(sessionKeyFromCookie))) { try { session.invalidate(); } catch (final IllegalStateException ise) { diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index 14d0cc202f3..6d08735381f 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -107,28 +107,11 @@ // Use this for checking the session, to bypass login screen bypassLoginCheck: function(args) { //determine to show or bypass login screen if (g_loginResponse == null) { //show login screen - /* - * Since we no longer store sessionKey in cookie, opening the - * 2nd browser window (of the same domain) will show login screen (i.e. user has to - * enter credentials again) and will cause the 1st browser window session timeout. - */ var unBoxCookieValue = function (cookieName) { - var cookieValue = $.cookie(cookieName); - if (cookieValue && cookieValue.length > 2 && cookieValue[0] === '"' && cookieValue[cookieValue.length-1] === '"') { - cookieValue = cookieValue.slice(1, cookieValue.length-1); - $.cookie(cookieName, cookieValue, { expires: 1 }); - } - return decodeURIComponent(cookieValue); + return decodeURIComponent($.cookie(cookieName)).replace(/"([^"]+(?="))"/g, '$1'); }; - unBoxCookieValue('sessionkey'); - // if sessionkey cookie exists use this to set g_sessionKey - // and destroy sessionkey cookie - if ($.cookie('sessionkey')) { - g_sessionKey = $.cookie('sessionkey'); - $.cookie('sessionkey', null); - } else { - g_sessionKey = unBoxCookieValue('JSESSIONID'); - } + // sessionkey is a HttpOnly cookie now, no need to pass as API param + g_sessionKey = null; g_role = unBoxCookieValue('role'); g_userid = unBoxCookieValue('userid'); g_domainid = unBoxCookieValue('domainid'); @@ -136,7 +119,7 @@ g_username = unBoxCookieValue('username'); g_userfullname = unBoxCookieValue('userfullname'); g_timezone = unBoxCookieValue('timezone'); - } else { //single-sign-on (bypass login screen) + } else { //single-sign-on (bypass login screen) g_sessionKey = encodeURIComponent(g_loginResponse.sessionkey); g_role = g_loginResponse.type; g_username = g_loginResponse.username; @@ -144,7 +127,7 @@ g_account = g_loginResponse.account; g_domainid = g_loginResponse.domainid; g_userfullname = g_loginResponse.firstname + ' ' + g_loginResponse.lastname; - g_timezone = g_loginResponse.timezone; + g_timezone = g_loginResponse.timezone; } var userValid = false; @@ -180,7 +163,25 @@ return true; } }); - + + // Populate IDP list + $.ajax({ + type: 'GET', + url: createURL('listIdps'), + dataType: 'json', + async: false, + success: function(data, textStatus, xhr) { + if (data && data.listidpsresponse && data.listidpsresponse.idp) { + var idpList = data.listidpsresponse.idp.sort(function (a, b) { + return a.orgName.localeCompare(b.orgName); + }); + g_idpList = idpList; + } + }, + error: function(xhr) { + }, + }); + return userValid ? { user: { userid: g_userid, @@ -227,14 +228,16 @@ async: false, success: function(json) { var loginresponse = json.loginresponse; - - g_sessionKey = encodeURIComponent(loginresponse.sessionkey); + // sessionkey is recevied as a HttpOnly cookie + // therefore reset any g_sessionKey value, an explicit + // param in the API call is no longer needed + g_sessionKey = null; g_role = loginresponse.type; g_username = loginresponse.username; g_userid = loginresponse.userid; g_account = loginresponse.account; g_domainid = loginresponse.domainid; - g_timezone = loginresponse.timezone; + g_timezone = loginresponse.timezone; g_userfullname = loginresponse.firstname + ' ' + loginresponse.lastname; $.cookie('username', g_username, { @@ -320,7 +323,7 @@ $.ajax({ url: createURL('logout'), async: false, - success: function() { + success: function() { g_sessionKey = null; g_username = null; g_account = null; @@ -331,15 +334,16 @@ g_kvmsnapshotenabled = null; g_regionsecondaryenabled = null; g_loginCmdText = null; - - $.cookie('JSESSIONID', null); - $.cookie('sessionkey', null); - $.cookie('username', null); - $.cookie('account', null); - $.cookie('domainid', null); - $.cookie('role', null); - $.cookie('timezone', null); - + + // Remove any cookies + var cookies = document.cookie.split(";"); + for (var i = 0; i < cookies.length; i++) { + var cookieName = $.trim(cookies[i].split("=")[0]); + if (['login-option', 'lang'].indexOf(cookieName) < 0) { + $.cookie(cookieName, null); + } + } + if (onLogoutCallback()) { //onLogoutCallback() will set g_loginResponse(single-sign-on variable) to null, then bypassLoginCheck() will show login screen. document.location.reload(); //when onLogoutCallback() returns true, reload the current document. } @@ -367,13 +371,15 @@ g_regionsecondaryenabled = null; g_loginCmdText = null; - $.cookie('JSESSIONID', null); - $.cookie('sessionkey', null); - $.cookie('username', null); - $.cookie('account', null); - $.cookie('domainid', null); - $.cookie('role', null); - $.cookie('timezone', null); + // Remove any cookies + var cookies = document.cookie.split(";"); + for (var i = 0; i < cookies.length; i++) { + var cookieName = $.trim(cookies[i].split("=")[0]); + if (['login-option', 'lang'].indexOf(cookieName) < 0) { + $.cookie(cookieName, null); + } + } + var url = 'samlSso'; if (args.data.idpid) { url = url + '&idpid=' + args.data.idpid; diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index c29956a1d65..14cd0f35b30 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -170,7 +170,10 @@ var pollAsyncJobResult = function(args) { function createURL(apiName, options) { if (!options) options = {}; - var urlString = clientApiUrl + "?" + "command=" + apiName + "&response=json&sessionkey=" + g_sessionKey; + var urlString = clientApiUrl + "?" + "command=" + apiName + "&response=json"; + if (g_sessionKey) { + urlString += "&sessionkey=" + g_sessionKey; + } if (cloudStack.context && cloudStack.context.projects && !options.ignoreProject) { urlString = urlString + '&projectid=' + cloudStack.context.projects[0].id; diff --git a/ui/scripts/ui-custom/login.js b/ui/scripts/ui-custom/login.js index 7c7e923a0e3..e1129583454 100644 --- a/ui/scripts/ui-custom/login.js +++ b/ui/scripts/ui-custom/login.js @@ -129,10 +129,6 @@ return false; }); - // Show SAML button if only SP is configured - $login.find('#login-dropdown').hide(); - $login.find('#saml-login').hide(); - $login.find('#cloudstack-login').hide(); var toggleLoginView = function (selectedOption) { $login.find('#login-submit').show(); @@ -160,56 +156,43 @@ } }); - $.ajax({ - type: 'GET', - url: createURL('listIdps'), - dataType: 'json', - async: false, - success: function(data, textStatus, xhr) { - if (xhr.status === 200) { - $login.find('#login-dropdown').show(); - $login.find('#login-submit').hide(); - } else { - $login.find('#cloudstack-login').show(); - $login.find('#login-submit').show(); - return; - } + // By Default hide login option dropdown + $login.find('#login-dropdown').hide(); + $login.find('#login-submit').show(); + $login.find('#cloudstack-login').show(); + $login.find('#saml-login').hide(); + // If any IdP servers were set, SAML is enabled + if (g_idpList && g_idpList.length > 0) { + $login.find('#login-dropdown').show(); + $login.find('#login-submit').hide(); + $login.find('#cloudstack-login').hide(); + $login.find('#saml-login').hide(); + + $login.find('#login-options') + .append($('