From df95a762f4a5ca7671995cf035223ab90790174a Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Tue, 12 Aug 2014 13:10:05 +0200 Subject: [PATCH 01/49] SAML2: add saml sso and slo apicmds skeleton, add classes to AuthManager Signed-off-by: Rohit Yadav --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../auth/APIAuthenticationManagerImpl.java | 2 + .../auth/SAML2LoginAPIAuthenticatorCmd.java | 94 +++++++++++++++++++ .../auth/SAML2LogoutAPIAuthenticatorCmd.java | 71 ++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java create mode 100644 server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index f89aa14df15..6baa95c39ab 100755 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -514,6 +514,7 @@ public class ApiConstants { public static final String VMPROFILE_ID = "vmprofileid"; public static final String VMGROUP_ID = "vmgroupid"; public static final String CS_URL = "csurl"; + public static final String IDP_URL = "idpurl"; public static final String SCALEUP_POLICY_IDS = "scaleuppolicyids"; public static final String SCALEDOWN_POLICY_IDS = "scaledownpolicyids"; public static final String SCALEUP_POLICIES = "scaleuppolicies"; diff --git a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java index 886d277f15b..ece2e032787 100644 --- a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java +++ b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java @@ -57,6 +57,8 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth s_commandList = new ArrayList>(); s_commandList.add(DefaultLoginAPIAuthenticatorCmd.class); s_commandList.add(DefaultLogoutAPIAuthenticatorCmd.class); + s_commandList.add(SAML2LoginAPIAuthenticatorCmd.class); + s_commandList.add(SAML2LogoutAPIAuthenticatorCmd.class); } return s_commandList; } diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java new file mode 100644 index 00000000000..beba4f15c5e --- /dev/null +++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.api.auth; + +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.Map; + +@APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) +public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(SAML2LoginAPIAuthenticatorCmd.class.getName()); + private static final String s_name = "loginresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true) + private String idpUrl; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getIdpUrl() { + return idpUrl; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { + + String response = null; + try { + resp.sendRedirect(getIdpUrl()); + + // TODO: create and send assertion with the URL as GET params + + } catch (IOException e) { + auditTrailSb.append("SP initiated SAML authentication using HTTP redirection failed:"); + auditTrailSb.append(e.getMessage()); + } + return response; + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } +} diff --git a/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java new file mode 100644 index 00000000000..91195887424 --- /dev/null +++ b/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.api.auth; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.LogoutCmdResponse; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Map; + +@APICommand(name = "samlslo", description = "SAML Single Log Out API", responseObject = LogoutCmdResponse.class, entityType = {}) +public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName()); + private static final String s_name = "logoutresponse"; + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { + auditTrailSb.append("=== Logging out ==="); + // TODO: check global config and do either local or global log out + LogoutCmdResponse response = new LogoutCmdResponse(); + response.setDescription("success"); + response.setResponseName(getCommandName()); + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGOUT_API; + } +} From 18ff47efc046c134592461938b87f560762c11b8 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 17 Aug 2014 19:11:40 +0200 Subject: [PATCH 02/49] server: add opensaml as dependency Signed-off-by: Rohit Yadav --- server/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/pom.xml b/server/pom.xml index 1b21ebde579..04c25aff904 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -133,6 +133,11 @@ cloud-engine-components-api ${project.version} + + org.opensaml + opensaml + 2.6.1 + From b82207e081b79261a274058cdd0323aff9c3be46 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 17 Aug 2014 19:12:00 +0200 Subject: [PATCH 03/49] SAML: WIP redirections work now Signed-off-by: Rohit Yadav --- .../auth/SAML2LoginAPIAuthenticatorCmd.java | 241 +++++++++++++++++- 1 file changed, 239 insertions(+), 2 deletions(-) diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java index beba4f15c5e..c6b0bb69472 100644 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java @@ -26,11 +26,54 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.common.SAMLVersion; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.AuthnContextClassRef; +import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameIDPolicy; +import org.opensaml.saml2.core.RequestedAuthnContext; +import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; +import org.opensaml.saml2.core.impl.AuthnRequestBuilder; +import org.opensaml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; +import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.io.Unmarshaller; +import org.opensaml.xml.io.UnmarshallerFactory; +import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.util.Base64; +import org.opensaml.xml.util.XMLHelper; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.StringWriter; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.security.SecureRandom; import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; @APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { @@ -71,12 +114,206 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); } + public String buildAuthnRequestUrl(String resourceUrl) { + String randomId = new BigInteger(130, new SecureRandom()).toString(32); + // TODO: Add method to get this url from metadata + String identityProviderUrl = "https://idp.ssocircle.com:443/sso/SSORedirect/metaAlias/ssocircle"; + String encodedAuthRequest = ""; + + try { + DefaultBootstrap.bootstrap(); + AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, identityProviderUrl, resourceUrl); // SAML AuthRequest + encodedAuthRequest = encodeAuthnRequest(authnRequest); + } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { + s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); + } + return identityProviderUrl + "?SAMLRequest=" + encodedAuthRequest; // + "&RelayState=" + relayState; + } + + private AuthnRequest buildAuthnRequestObject(String authnId, String idpUrl, String consumerUrl) { + // Issuer object + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + //SAMLConstants.SAML20_NS, + // "Issuer", "samlp"); + issuer.setValue("apache-cloudstack"); + + // NameIDPolicy + NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); + NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); + nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"); + nameIdPolicy.setSPNameQualifier("Apache CloudStack"); + nameIdPolicy.setAllowCreate(true); + + // AuthnContextClass + AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); + AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( + SAMLConstants.SAML20_NS, + "AuthnContextClassRef", "saml"); + authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + + // AuthnContex + RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); + RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); + requestedAuthnContext + .setComparison(AuthnContextComparisonTypeEnumeration.EXACT); + requestedAuthnContext.getAuthnContextClassRefs().add( + authnContextClassRef); + + + // Creation of AuthRequestObject + AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); + AuthnRequest authnRequest = authRequestBuilder.buildObject(); + //SAMLConstants.SAML20P_NS, + // "AuthnRequest", "samlp"); + authnRequest.setID(authnId); + authnRequest.setDestination(idpUrl); + authnRequest.setVersion(SAMLVersion.VERSION_20); + authnRequest.setForceAuthn(true); + authnRequest.setIsPassive(false); + authnRequest.setIssuer(issuer); + authnRequest.setIssueInstant(new DateTime()); + authnRequest.setProviderName("Apache CloudStack"); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + authnRequest.setAssertionConsumerServiceURL(consumerUrl); + //authnRequest.setNameIDPolicy(nameIdPolicy); + //authnRequest.setRequestedAuthnContext(requestedAuthnContext); + + return authnRequest; + } + + private String encodeAuthnRequest(AuthnRequest authnRequest) + throws MarshallingException, IOException { + + Marshaller marshaller = null; + org.w3c.dom.Element authDOM = null; + StringWriter requestWriter = null; + String requestMessage = null; + Deflater deflater = null; + ByteArrayOutputStream byteArrayOutputStream = null; + DeflaterOutputStream deflaterOutputStream = null; + String encodedRequestMessage = null; + + marshaller = org.opensaml.Configuration.getMarshallerFactory() + .getMarshaller(authnRequest); // object to DOM converter + + authDOM = marshaller.marshall(authnRequest); // converting to a DOM + + requestWriter = new StringWriter(); + XMLHelper.writeNode(authDOM, requestWriter); + requestMessage = requestWriter.toString(); // DOM to string + + deflater = new Deflater(Deflater.DEFLATED, true); + byteArrayOutputStream = new ByteArrayOutputStream(); + deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, + deflater); + deflaterOutputStream.write(requestMessage.getBytes()); // compressing + deflaterOutputStream.close(); + + encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream + .toByteArray(), Base64.DONT_BREAK_LINES); + encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, + "UTF-8").trim(); // encoding string + + return encodedRequestMessage; + } + + + public String processResponseMessage(String responseMessage) { + + XMLObject responseObject = null; + + try { + + responseObject = this.unmarshall(responseMessage); + + } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { + e.printStackTrace(); + } + + return this.getResult(responseObject); + } + + private XMLObject unmarshall(String responseMessage) + throws ConfigurationException, ParserConfigurationException, + SAXException, IOException, UnmarshallingException { + + DocumentBuilderFactory documentBuilderFactory = null; + DocumentBuilder docBuilder = null; + Document document = null; + Element element = null; + UnmarshallerFactory unmarshallerFactory = null; + Unmarshaller unmarshaller = null; + + DefaultBootstrap.bootstrap(); + + documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + documentBuilderFactory.setNamespaceAware(true); + + docBuilder = documentBuilderFactory.newDocumentBuilder(); + + document = docBuilder.parse(new ByteArrayInputStream(responseMessage + .trim().getBytes())); // response to DOM + + element = document.getDocumentElement(); // the DOM element + + unmarshallerFactory = Configuration.getUnmarshallerFactory(); + + unmarshaller = unmarshallerFactory.getUnmarshaller(element); + + return unmarshaller.unmarshall(element); // Response object + + } + + private String getResult(XMLObject responseObject) { + + Element ele = null; + NodeList statusNodeList = null; + Node statusNode = null; + NamedNodeMap statusAttr = null; + Node valueAtt = null; + String statusValue = null; + + String[] word = null; + String result = null; + + NodeList nameIDNodeList = null; + Node nameIDNode = null; + String nameID = null; + + // reading the Response Object + ele = responseObject.getDOM(); + statusNodeList = ele.getElementsByTagName("samlp:StatusCode"); + statusNode = statusNodeList.item(0); + statusAttr = statusNode.getAttributes(); + valueAtt = statusAttr.item(0); + statusValue = valueAtt.getNodeValue(); + + word = statusValue.split(":"); + result = word[word.length - 1]; + + nameIDNodeList = ele.getElementsByTagNameNS( + "urn:oasis:names:tc:SAML:2.0:assertion", "NameID"); + nameIDNode = nameIDNodeList.item(0); + nameID = nameIDNode.getFirstChild().getNodeValue(); + + result = nameID + ":" + result; + + return result; + } + + + @Override public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { - String response = null; try { - resp.sendRedirect(getIdpUrl()); + String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=login"); + resp.sendRedirect(redirectUrl); + + //resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + //resp.setHeader("Location", redirectUrl); // TODO: create and send assertion with the URL as GET params From 1a3813a342ebaf454387475585a9f427c4540d1f Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 03:35:11 +0200 Subject: [PATCH 04/49] ApiServer: change loginUser method signature to return ResponseObject Signed-off-by: Rohit Yadav --- server/src/com/cloud/api/ApiServer.java | 53 ++++++++++++++++++- .../src/com/cloud/api/ApiServerService.java | 13 +++-- server/src/com/cloud/api/ApiServlet.java | 3 ++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index 5d6c9257645..fc550269d61 100755 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -32,6 +32,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -54,6 +55,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.cloud.utils.HttpUtils; +import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.commons.codec.binary.Base64; import org.apache.http.ConnectionClosedException; import org.apache.http.HttpException; @@ -932,8 +934,55 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer return null; } + private ResponseObject createLoginResponse(HttpSession session) { + LoginCmdResponse response = new LoginCmdResponse(); + response.setTimeout(session.getMaxInactiveInterval()); + + final String user_UUID = (String)session.getAttribute("user_UUID"); + session.removeAttribute("user_UUID"); + response.setUserId(user_UUID); + + final String domain_UUID = (String)session.getAttribute("domain_UUID"); + session.removeAttribute("domain_UUID"); + response.setDomainId(domain_UUID); + + final Enumeration attrNames = session.getAttributeNames(); + if (attrNames != null) { + while (attrNames.hasMoreElements()) { + final String attrName = (String) attrNames.nextElement(); + final Object attrObj = session.getAttribute(attrName); + if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) { + response.setUsername(attrObj.toString()); + } + if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) { + response.setAccount(attrObj.toString()); + } + if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) { + response.setFirstName(attrObj.toString()); + } + if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) { + response.setLastName(attrObj.toString()); + } + if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) { + response.setType((attrObj.toString())); + } + if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) { + response.setTimeZone(attrObj.toString()); + } + if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) { + response.setRegistered(attrObj.toString()); + } + if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) { + response.setSessionKey(attrObj.toString()); + } + } + } + response.setResponseName("loginresponse"); + return response; + } + @Override - public void loginUser(final HttpSession session, final String username, final String password, Long domainId, final String domainPath, final String loginIpAddress, + public ResponseObject loginUser(final HttpSession session, final String username, final String password, Long domainId, final String domainPath, final String loginIpAddress, final Map requestParameters) throws CloudAuthenticationException { // We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist // we will default to ROOT @@ -1003,7 +1052,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer final String sessionKey = Base64.encodeBase64String(sessionKeyBytes); session.setAttribute("sessionkey", sessionKey); - return; + return createLoginResponse(session); } throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials"); } diff --git a/server/src/com/cloud/api/ApiServerService.java b/server/src/com/cloud/api/ApiServerService.java index 5d078c31605..aa3b8f776c7 100644 --- a/server/src/com/cloud/api/ApiServerService.java +++ b/server/src/com/cloud/api/ApiServerService.java @@ -16,21 +16,20 @@ // under the License. package com.cloud.api; -import java.util.Map; - -import javax.servlet.http.HttpSession; - +import com.cloud.exception.CloudAuthenticationException; +import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; -import com.cloud.exception.CloudAuthenticationException; +import javax.servlet.http.HttpSession; +import java.util.Map; public interface ApiServerService { public boolean verifyRequest(Map requestParameters, Long userId) throws ServerApiException; public Long fetchDomainId(String domainUUID); - public void loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress, - Map requestParameters) throws CloudAuthenticationException; + public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress, + Map requestParameters) throws CloudAuthenticationException; public void logoutUser(long userId); diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 8dff6ebc952..5484b1e1252 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -201,6 +201,9 @@ public class ApiServlet extends HttpServlet { } catch (final IllegalStateException ignored) { } } + } else { + auditTrailSb.insert(0, "(userId=" + session.getAttribute("userid") + " accountId=" + ((Account) session.getAttribute("accountobj")).getId() + + " sessionId=" + session.getId() + ")"); } HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType); return; From 9c7204d38637f046768541a3dc3aa707fe5eb927 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 03:43:37 +0200 Subject: [PATCH 05/49] DefaultLoginAPIAuthenticatorCmd: move createLoginResponse to ApiServer Signed-off-by: Rohit Yadav --- .../auth/DefaultLoginAPIAuthenticatorCmd.java | 56 +------------------ 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java index f5d633e025f..243ad9b1697 100644 --- a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java @@ -25,7 +25,6 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.log4j.Logger; @@ -33,7 +32,6 @@ import org.apache.log4j.Logger; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import java.util.Enumeration; import java.util.Map; @APICommand(name = "login", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) @@ -100,54 +98,6 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); } - private String createLoginResponse(HttpSession session, String responseType) { - LoginCmdResponse response = new LoginCmdResponse(); - response.setTimeout(session.getMaxInactiveInterval()); - - final String user_UUID = (String)session.getAttribute("user_UUID"); - session.removeAttribute("user_UUID"); - response.setUserId(user_UUID); - - final String domain_UUID = (String)session.getAttribute("domain_UUID"); - session.removeAttribute("domain_UUID"); - response.setDomainId(domain_UUID); - - // FIXME: the while loop mess - final Enumeration attrNames = session.getAttributeNames(); - if (attrNames != null) { - while (attrNames.hasMoreElements()) { - final String attrName = (String) attrNames.nextElement(); - final Object attrObj = session.getAttribute(attrName); - if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) { - response.setUsername(attrObj.toString()); - } - if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) { - response.setAccount(attrObj.toString()); - } - if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) { - response.setFirstName(attrObj.toString()); - } - if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) { - response.setLastName(attrObj.toString()); - } - if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) { - response.setType((attrObj.toString())); - } - if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) { - response.setTimeZone(attrObj.toString()); - } - if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) { - response.setRegistered(attrObj.toString()); - } - if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) { - response.setSessionKey(attrObj.toString()); - } - } - } - response.setResponseName(getCommandName()); - return ApiResponseSerializer.toSerializedString((ResponseObject) response, responseType); - } - @Override public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { @@ -197,10 +147,8 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe if (username != null) { final String pwd = ((password == null) ? null : password[0]); try { - _apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params); - auditTrailSb.insert(0, "(userId=" + session.getAttribute("userid") + " accountId=" + ((Account) session.getAttribute("accountobj")).getId() + - " sessionId=" + session.getId() + ")"); - return createLoginResponse(session, responseType); + return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params), + responseType); } catch (final CloudAuthenticationException ex) { // TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401) try { From a1dc9e8189ebdab3f7e8b849f1777f282a7a295b Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 03:43:58 +0200 Subject: [PATCH 06/49] SAML2LoginAPIAuthenticatorCmd: Implement SAML SSO using HTTP Redirect binding - Creates SAMLRequest and uses HTTP redirect binding (uses GET/302) - Redirects to IdP for auth - On successful auth, check for assertion - Tries to get attributes based on standard LDAP attribute names - Next, gets user using EntityManager, if not found creates one with NameID as UUID - Finally tries to log in and redirect Signed-off-by: Rohit Yadav --- .../auth/SAML2LoginAPIAuthenticatorCmd.java | 283 ++++++++++-------- 1 file changed, 150 insertions(+), 133 deletions(-) diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java index c6b0bb69472..4e17d3d9127 100644 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java @@ -17,7 +17,13 @@ package com.cloud.api.auth; +import com.cloud.api.ApiServerService; +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; +import com.cloud.user.User; +import com.cloud.utils.HttpUtils; +import com.cloud.utils.db.EntityManager; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -25,18 +31,26 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.common.SAMLVersion; import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.NameIDPolicy; +import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.RequestedAuthnContext; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.StatusCode; import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; import org.opensaml.saml2.core.impl.AuthnRequestBuilder; import org.opensaml.saml2.core.impl.IssuerBuilder; @@ -49,15 +63,15 @@ import org.opensaml.xml.io.MarshallingException; import org.opensaml.xml.io.Unmarshaller; import org.opensaml.xml.io.UnmarshallerFactory; import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.signature.Signature; import org.opensaml.xml.util.Base64; import org.opensaml.xml.util.XMLHelper; import org.w3c.dom.Document; import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.xml.sax.SAXException; +import javax.inject.Inject; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.xml.parsers.DocumentBuilder; @@ -71,6 +85,7 @@ import java.io.StringWriter; import java.math.BigInteger; import java.net.URLEncoder; import java.security.SecureRandom; +import java.util.List; import java.util.Map; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -86,6 +101,11 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent @Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true) private String idpUrl; + @Inject + ApiServerService _apiServer; + @Inject + EntityManager _entityMgr; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -114,34 +134,30 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); } - public String buildAuthnRequestUrl(String resourceUrl) { + public String buildAuthnRequestUrl(String consumerUrl, String identityProviderUrl) { String randomId = new BigInteger(130, new SecureRandom()).toString(32); - // TODO: Add method to get this url from metadata - String identityProviderUrl = "https://idp.ssocircle.com:443/sso/SSORedirect/metaAlias/ssocircle"; - String encodedAuthRequest = ""; - + String spId = "org.apache.cloudstack"; + String redirectUrl = ""; try { DefaultBootstrap.bootstrap(); - AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, identityProviderUrl, resourceUrl); // SAML AuthRequest - encodedAuthRequest = encodeAuthnRequest(authnRequest); + AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl); + redirectUrl = identityProviderUrl + "?SAMLRequest=" + encodeAuthnRequest(authnRequest); } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); } - return identityProviderUrl + "?SAMLRequest=" + encodedAuthRequest; // + "&RelayState=" + relayState; + return redirectUrl; } - private AuthnRequest buildAuthnRequestObject(String authnId, String idpUrl, String consumerUrl) { + private AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) { // Issuer object IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject(); - //SAMLConstants.SAML20_NS, - // "Issuer", "samlp"); - issuer.setValue("apache-cloudstack"); + issuer.setValue(spId); // NameIDPolicy NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); - nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"); + nameIdPolicy.setFormat(NameIDType.PERSISTENT); nameIdPolicy.setSPNameQualifier("Apache CloudStack"); nameIdPolicy.setAllowCreate(true); @@ -156,16 +172,13 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); requestedAuthnContext - .setComparison(AuthnContextComparisonTypeEnumeration.EXACT); + .setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM); requestedAuthnContext.getAuthnContextClassRefs().add( authnContextClassRef); - // Creation of AuthRequestObject AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); AuthnRequest authnRequest = authRequestBuilder.buildObject(); - //SAMLConstants.SAML20P_NS, - // "AuthnRequest", "samlp"); authnRequest.setID(authnId); authnRequest.setDestination(idpUrl); authnRequest.setVersion(SAMLVersion.VERSION_20); @@ -174,154 +187,158 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent authnRequest.setIssuer(issuer); authnRequest.setIssueInstant(new DateTime()); authnRequest.setProviderName("Apache CloudStack"); - authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); //SAML2_ARTIFACT_BINDING_URI); authnRequest.setAssertionConsumerServiceURL(consumerUrl); - //authnRequest.setNameIDPolicy(nameIdPolicy); - //authnRequest.setRequestedAuthnContext(requestedAuthnContext); + authnRequest.setNameIDPolicy(nameIdPolicy); + authnRequest.setRequestedAuthnContext(requestedAuthnContext); return authnRequest; } private String encodeAuthnRequest(AuthnRequest authnRequest) throws MarshallingException, IOException { - - Marshaller marshaller = null; - org.w3c.dom.Element authDOM = null; - StringWriter requestWriter = null; - String requestMessage = null; - Deflater deflater = null; - ByteArrayOutputStream byteArrayOutputStream = null; - DeflaterOutputStream deflaterOutputStream = null; - String encodedRequestMessage = null; - - marshaller = org.opensaml.Configuration.getMarshallerFactory() - .getMarshaller(authnRequest); // object to DOM converter - - authDOM = marshaller.marshall(authnRequest); // converting to a DOM - - requestWriter = new StringWriter(); + Marshaller marshaller = Configuration.getMarshallerFactory() + .getMarshaller(authnRequest); + Element authDOM = marshaller.marshall(authnRequest); + StringWriter requestWriter = new StringWriter(); XMLHelper.writeNode(authDOM, requestWriter); - requestMessage = requestWriter.toString(); // DOM to string - - deflater = new Deflater(Deflater.DEFLATED, true); - byteArrayOutputStream = new ByteArrayOutputStream(); - deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, - deflater); - deflaterOutputStream.write(requestMessage.getBytes()); // compressing + String requestMessage = requestWriter.toString(); + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater); + deflaterOutputStream.write(requestMessage.getBytes()); deflaterOutputStream.close(); - - encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream - .toByteArray(), Base64.DONT_BREAK_LINES); - encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, - "UTF-8").trim(); // encoding string - + String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); + encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim(); return encodedRequestMessage; } - - public String processResponseMessage(String responseMessage) { - + public Response processSAMLResponse(String responseMessage) { XMLObject responseObject = null; - try { - responseObject = this.unmarshall(responseMessage); } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { - e.printStackTrace(); + s_logger.error("SAMLResponse processing error: " + e.getMessage()); } - - return this.getResult(responseObject); + return (Response) responseObject; } private XMLObject unmarshall(String responseMessage) throws ConfigurationException, ParserConfigurationException, SAXException, IOException, UnmarshallingException { - - DocumentBuilderFactory documentBuilderFactory = null; - DocumentBuilder docBuilder = null; - Document document = null; - Element element = null; - UnmarshallerFactory unmarshallerFactory = null; - Unmarshaller unmarshaller = null; - - DefaultBootstrap.bootstrap(); - - documentBuilderFactory = DocumentBuilderFactory.newInstance(); - + try { + DefaultBootstrap.bootstrap(); + } catch (ConfigurationException | FactoryConfigurationError e) { + s_logger.error("SAML response message decoding error: " + e.getMessage()); + } + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); - - docBuilder = documentBuilderFactory.newDocumentBuilder(); - - document = docBuilder.parse(new ByteArrayInputStream(responseMessage - .trim().getBytes())); // response to DOM - - element = document.getDocumentElement(); // the DOM element - - unmarshallerFactory = Configuration.getUnmarshallerFactory(); - - unmarshaller = unmarshallerFactory.getUnmarshaller(element); - - return unmarshaller.unmarshall(element); // Response object - + DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); + byte[] base64DecodedResponse = Base64.decode(responseMessage); + Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse)); + Element element = document.getDocumentElement(); + UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + return unmarshaller.unmarshall(element); } - private String getResult(XMLObject responseObject) { - - Element ele = null; - NodeList statusNodeList = null; - Node statusNode = null; - NamedNodeMap statusAttr = null; - Node valueAtt = null; - String statusValue = null; - - String[] word = null; - String result = null; - - NodeList nameIDNodeList = null; - Node nameIDNode = null; - String nameID = null; - - // reading the Response Object - ele = responseObject.getDOM(); - statusNodeList = ele.getElementsByTagName("samlp:StatusCode"); - statusNode = statusNodeList.item(0); - statusAttr = statusNode.getAttributes(); - valueAtt = statusAttr.item(0); - statusValue = valueAtt.getNodeValue(); - - word = statusValue.split(":"); - result = word[word.length - 1]; - - nameIDNodeList = ele.getElementsByTagNameNS( - "urn:oasis:names:tc:SAML:2.0:assertion", "NameID"); - nameIDNode = nameIDNodeList.item(0); - nameID = nameIDNode.getFirstChild().getNodeValue(); - - result = nameID + ":" + result; - - return result; - } - - - @Override - public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { - String response = null; + public String authenticate(final String command, final Map params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { try { - String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=login"); - resp.sendRedirect(redirectUrl); + if (!params.containsKey("SAMLResponse")) { + final String[] idps = (String[])params.get("idpurl"); + String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=samlsso", idps[0]); + resp.sendRedirect(redirectUrl); + return ""; + } else { + final String samlResponse = ((String[])params.get("SAMLResponse"))[0]; + Response processedSAMLResponse = processSAMLResponse(samlResponse); + String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); + if (!statusCode.equals(StatusCode.SUCCESS_URI)) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Identity Provider send a non-successful authentication status code", + params, responseType)); + } - //resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); - //resp.setHeader("Location", redirectUrl); + Signature sig = processedSAMLResponse.getSignature(); + //SignatureValidator validator = new SignatureValidator(credential); + //validator.validate(sig); - // TODO: create and send assertion with the URL as GET params + String uniqueUserId = null; + String accountName = "admin"; //GET from config, try, fail + Long domainId = 1L; // GET from config, try, fail + String username = null; + String password = ""; + String firstName = ""; + String lastName = ""; + String timeZone = ""; + String email = ""; + Assertion assertion = processedSAMLResponse.getAssertions().get(0); + NameID nameId = assertion.getSubject().getNameID(); + + if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) { + username = nameId.getValue(); + uniqueUserId = "saml-" + username; + if (nameId.getFormat().equals(NameIDType.EMAIL)) { + email = username; + } + } + + String issuer = assertion.getIssuer().getValue(); + String audience = assertion.getConditions().getAudienceRestrictions().get(0).getAudiences().get(0).getAudienceURI(); + AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0); + List attributes = attributeStatement.getAttributes(); + + // Try capturing standard LDAP attributes + for (Attribute attribute: attributes) { + String attributeName = attribute.getName(); + String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent(); + if (attributeName.equalsIgnoreCase("uid") && uniqueUserId == null) { + username = attributeValue; + uniqueUserId = "saml-" + username; + } else if (attributeName.equalsIgnoreCase("givenName")) { + firstName = attributeValue; + } else if (attributeName.equalsIgnoreCase(("sn"))) { + lastName = attributeValue; + } else if (attributeName.equalsIgnoreCase("mail")) { + email = attributeValue; + } + } + + User user = _entityMgr.findByUuid(User.class, uniqueUserId); + if (user == null && uniqueUserId != null && username != null + && accountName != null && domainId != null) { + CallContext.current().setEventDetails("UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName); + user = _accountService.createUser(username, password, firstName, lastName, email, timeZone, accountName, domainId, uniqueUserId); + } + + if (user != null) { + try { + if (_apiServer.verifyUser(user.getId())) { + LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, user.getPassword(), domainId, null, remoteAddress, params); + resp.addCookie(new Cookie("userid", loginResponse.getUserId())); + resp.addCookie(new Cookie("domainid", loginResponse.getDomainId())); + resp.addCookie(new Cookie("role", loginResponse.getType())); + resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); + //resp.sendRedirect("http://localhost:8080/client"); + return ApiResponseSerializer.toSerializedString(loginResponse, responseType); + + } + } catch (final CloudAuthenticationException ignored) { + } + } + } } catch (IOException e) { auditTrailSb.append("SP initiated SAML authentication using HTTP redirection failed:"); auditTrailSb.append(e.getMessage()); } - return response; + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Unable to authenticate or retrieve user while performing SAML based SSO", + params, responseType)); } @Override From 2694ad7bd91671629831f9feede879cb24d05d69 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 03:50:18 +0200 Subject: [PATCH 07/49] ApiServlet: Fix NPE while inserting to auditTrail Signed-off-by: Rohit Yadav --- server/src/com/cloud/api/ApiServlet.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 5484b1e1252..8dff6ebc952 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -201,9 +201,6 @@ public class ApiServlet extends HttpServlet { } catch (final IllegalStateException ignored) { } } - } else { - auditTrailSb.insert(0, "(userId=" + session.getAttribute("userid") + " accountId=" + ((Account) session.getAttribute("accountobj")).getId() + - " sessionId=" + session.getId() + ")"); } HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType); return; From 9b1a6dac4a4bb766ee050040356998776ddca190 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 04:20:03 +0200 Subject: [PATCH 08/49] ui: Unbox extra quotes from sessionKey cookie value Signed-off-by: Rohit Yadav --- ui/scripts/cloudStack.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index b6dd5593345..420c1373694 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -131,6 +131,13 @@ */ g_mySession = $.cookie('JSESSIONID'); g_sessionKey = $.cookie('sessionKey'); + // Unbox quotes from sessionKey cookie value + if (g_sessionKey[0] === '"' && g_sessionKey[g_sessionKey.length-1] === '"') { + g_sessionKey = g_sessionKey.slice(1, g_sessionKey.length-1); + $.cookie('sessionKey', g_sessionKey, { + expires: 1 + }); + } g_role = $.cookie('role'); g_username = $.cookie('username'); g_userid = $.cookie('userid'); From 2464e02bf45515c6b642b3a3a3fd62045d48ab74 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 04:20:54 +0200 Subject: [PATCH 09/49] SAML2LoginAPIAuthenticatorCmd: Set all necessary cookies and redirect to UI Signed-off-by: Rohit Yadav --- .../src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java index 4e17d3d9127..1f88c1cbce1 100644 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java @@ -324,7 +324,9 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); - //resp.sendRedirect("http://localhost:8080/client"); + resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8))); + resp.sendRedirect("http://localhost:8080/client"); return ApiResponseSerializer.toSerializedString(loginResponse, responseType); } From a364054db60e1a91c5fb671616783b1db9deb590 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 04:31:46 +0200 Subject: [PATCH 10/49] Minor fixes Signed-off-by: Rohit Yadav --- .../src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java | 2 +- ui/scripts/cloudStack.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java index 1f88c1cbce1..ce97cfd71d3 100644 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java @@ -325,7 +325,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName())); resp.sendRedirect("http://localhost:8080/client"); return ApiResponseSerializer.toSerializedString(loginResponse, responseType); diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index 420c1373694..edc7c21529c 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -132,7 +132,7 @@ g_mySession = $.cookie('JSESSIONID'); g_sessionKey = $.cookie('sessionKey'); // Unbox quotes from sessionKey cookie value - if (g_sessionKey[0] === '"' && g_sessionKey[g_sessionKey.length-1] === '"') { + if (g_sessionKey && g_sessionKey[0] === '"' && g_sessionKey[g_sessionKey.length-1] === '"') { g_sessionKey = g_sessionKey.slice(1, g_sessionKey.length-1); $.cookie('sessionKey', g_sessionKey, { expires: 1 From e6ec51e12a317a6594b9acdef8dc1ca990be1867 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 18 Aug 2014 04:56:54 +0200 Subject: [PATCH 11/49] ui: refactor and use a unified unboxing helping method in cloudStack.js Signed-off-by: Rohit Yadav --- ui/scripts/cloudStack.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index edc7c21529c..38cf501f1aa 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -129,24 +129,25 @@ i.e. calling listCapabilities API with g_sessionKey from $.cookie('sessionKey') will succeed, then userValid will be set to true, then an user object (instead of "false") will be returned, then login screen will be bypassed. */ + 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 cookieValue; + }; g_mySession = $.cookie('JSESSIONID'); - g_sessionKey = $.cookie('sessionKey'); - // Unbox quotes from sessionKey cookie value - if (g_sessionKey && g_sessionKey[0] === '"' && g_sessionKey[g_sessionKey.length-1] === '"') { - g_sessionKey = g_sessionKey.slice(1, g_sessionKey.length-1); - $.cookie('sessionKey', g_sessionKey, { - expires: 1 - }); - } - g_role = $.cookie('role'); - g_username = $.cookie('username'); - g_userid = $.cookie('userid'); - g_account = $.cookie('account'); - g_domainid = $.cookie('domainid'); - g_userfullname = $.cookie('userfullname'); - g_timezone = $.cookie('timezone'); + g_sessionKey = unBoxCookieValue('sessionKey'); + g_role = unBoxCookieValue('role'); + g_userid = unBoxCookieValue('userid'); + g_domainid = unBoxCookieValue('domainid'); + g_account = unBoxCookieValue('account'); + g_username = unBoxCookieValue('username'); + g_userfullname = unBoxCookieValue('userfullname'); + g_timezone = unBoxCookieValue('timezone'); if ($.cookie('timezoneoffset') != null) - g_timezoneoffset = isNaN($.cookie('timezoneoffset')) ? null : parseFloat($.cookie('timezoneoffset')); + g_timezoneoffset = isNaN(unBoxCookieValue('timezoneoffset')) ? null : parseFloat(unBoxCookieValue('timezoneoffset')); else g_timezoneoffset = null; } else { //single-sign-on (bypass login screen) From d6ea4ad7e064eea23e584be44d10a8c4ee80b608 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:50:07 +0200 Subject: [PATCH 12/49] utils: refactor and aggregate methods in SAMLUtils Signed-off-by: Rohit Yadav --- utils/pom.xml | 5 + .../cloudstack/utils/auth/SAMLUtils.java | 162 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java diff --git a/utils/pom.xml b/utils/pom.xml index 27eeee37a9f..7dafbba65d8 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -143,6 +143,11 @@ + + org.opensaml + opensaml + ${cs.opensaml.version} + commons-net commons-net diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java new file mode 100644 index 00000000000..bc39eaffe21 --- /dev/null +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -0,0 +1,162 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.auth; + +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.common.SAMLVersion; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.AuthnContextClassRef; +import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameIDPolicy; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.core.RequestedAuthnContext; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; +import org.opensaml.saml2.core.impl.AuthnRequestBuilder; +import org.opensaml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; +import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.io.Unmarshaller; +import org.opensaml.xml.io.UnmarshallerFactory; +import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.util.Base64; +import org.opensaml.xml.util.XMLHelper; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URLEncoder; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +public class SAMLUtils { + public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); + + public static final String SAML_NS = "saml-"; + + public static String createSAMLId(String uid) { + return SAML_NS + uid; + } + + public static Boolean checkSAMLUserId(String uuid) { + return uuid.startsWith(SAML_NS); + } + + public static AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) { + // Issuer object + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(spId); + + // NameIDPolicy + NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); + NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); + nameIdPolicy.setFormat(NameIDType.PERSISTENT); + nameIdPolicy.setSPNameQualifier("Apache CloudStack"); + nameIdPolicy.setAllowCreate(true); + + // AuthnContextClass + AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); + AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( + SAMLConstants.SAML20_NS, + "AuthnContextClassRef", "saml"); + authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + + // AuthnContex + RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); + RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); + requestedAuthnContext + .setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM); + requestedAuthnContext.getAuthnContextClassRefs().add( + authnContextClassRef); + + // Creation of AuthRequestObject + AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); + AuthnRequest authnRequest = authRequestBuilder.buildObject(); + authnRequest.setID(authnId); + authnRequest.setDestination(idpUrl); + authnRequest.setVersion(SAMLVersion.VERSION_20); + authnRequest.setForceAuthn(true); + authnRequest.setIsPassive(false); + authnRequest.setIssuer(issuer); + authnRequest.setIssueInstant(new DateTime()); + authnRequest.setProviderName("Apache CloudStack"); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + authnRequest.setAssertionConsumerServiceURL(consumerUrl); + authnRequest.setNameIDPolicy(nameIdPolicy); + authnRequest.setRequestedAuthnContext(requestedAuthnContext); + + return authnRequest; + } + + public static String encodeSAMLRequest(AuthnRequest authnRequest) + throws MarshallingException, IOException { + Marshaller marshaller = Configuration.getMarshallerFactory() + .getMarshaller(authnRequest); + Element authDOM = marshaller.marshall(authnRequest); + StringWriter requestWriter = new StringWriter(); + XMLHelper.writeNode(authDOM, requestWriter); + String requestMessage = requestWriter.toString(); + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater); + deflaterOutputStream.write(requestMessage.getBytes()); + deflaterOutputStream.close(); + String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); + encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim(); + return encodedRequestMessage; + } + + public static Response decodeSAMLResponse(String responseMessage) + throws ConfigurationException, ParserConfigurationException, + SAXException, IOException, UnmarshallingException { + try { + DefaultBootstrap.bootstrap(); + } catch (ConfigurationException | FactoryConfigurationError e) { + s_logger.error("SAML response message decoding error: " + e.getMessage()); + } + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); + byte[] base64DecodedResponse = Base64.decode(responseMessage); + Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse)); + Element element = document.getDocumentElement(); + UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + return (Response) unmarshaller.unmarshall(element); + } + +} From c04b9ed661f38ab47825e5bfdc12b0a59d97fc92 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:50:40 +0200 Subject: [PATCH 13/49] Maven: add opensaml 2.6.1 version id in pom.xml Signed-off-by: Rohit Yadav --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 558aaba50cd..09c76e6c39c 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,7 @@ 2.5 2.5.3 2.9.1 + 2.6.1 From 68e094ebaf6ce0c3280c9b6c191f1c389758e8f9 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:51:29 +0200 Subject: [PATCH 14/49] saml: move refactor files from server to api module - Move interfaces and classes from server to api module - This can be then used for pluggable api authenticators Signed-off-by: Rohit Yadav --- .../src/org/apache/cloudstack}/api/ApiServerService.java | 2 +- .../cloudstack}/api/auth/APIAuthenticationManager.java | 2 +- .../cloudstack}/api/auth/APIAuthenticationType.java | 2 +- .../org/apache/cloudstack}/api/auth/APIAuthenticator.java | 2 +- .../api/command}/SAML2LoginAPIAuthenticatorCmd.java | 8 +++++--- .../api/command}/SAML2LogoutAPIAuthenticatorCmd.java | 4 +++- .../cloudstack/{ => saml}/SAML2UserAuthenticator.java | 2 +- 7 files changed, 13 insertions(+), 9 deletions(-) rename {server/src/com/cloud => api/src/org/apache/cloudstack}/api/ApiServerService.java (98%) rename {server/src/com/cloud => api/src/org/apache/cloudstack}/api/auth/APIAuthenticationManager.java (96%) rename {server/src/com/cloud => api/src/org/apache/cloudstack}/api/auth/APIAuthenticationType.java (95%) rename {server/src/com/cloud => api/src/org/apache/cloudstack}/api/auth/APIAuthenticator.java (97%) rename {server/src/com/cloud/api/auth => plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command}/SAML2LoginAPIAuthenticatorCmd.java (98%) rename {server/src/com/cloud/api/auth => plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command}/SAML2LogoutAPIAuthenticatorCmd.java (94%) rename plugins/user-authenticators/saml2/src/org/apache/cloudstack/{ => saml}/SAML2UserAuthenticator.java (98%) diff --git a/server/src/com/cloud/api/ApiServerService.java b/api/src/org/apache/cloudstack/api/ApiServerService.java similarity index 98% rename from server/src/com/cloud/api/ApiServerService.java rename to api/src/org/apache/cloudstack/api/ApiServerService.java index aa3b8f776c7..9c0cfa39d79 100644 --- a/server/src/com/cloud/api/ApiServerService.java +++ b/api/src/org/apache/cloudstack/api/ApiServerService.java @@ -14,7 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.api; +package org.apache.cloudstack.api; import com.cloud.exception.CloudAuthenticationException; import org.apache.cloudstack.api.ResponseObject; diff --git a/server/src/com/cloud/api/auth/APIAuthenticationManager.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java similarity index 96% rename from server/src/com/cloud/api/auth/APIAuthenticationManager.java rename to api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java index 7fd4da92a59..5d4d664538c 100644 --- a/server/src/com/cloud/api/auth/APIAuthenticationManager.java +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package com.cloud.api.auth; +package org.apache.cloudstack.api.auth; import com.cloud.utils.component.PluggableService; diff --git a/server/src/com/cloud/api/auth/APIAuthenticationType.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java similarity index 95% rename from server/src/com/cloud/api/auth/APIAuthenticationType.java rename to api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java index bdd37ec5e79..e8c7f0f5eda 100644 --- a/server/src/com/cloud/api/auth/APIAuthenticationType.java +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java @@ -14,7 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.api.auth; +package org.apache.cloudstack.api.auth; public enum APIAuthenticationType { LOGIN_API, LOGOUT_API diff --git a/server/src/com/cloud/api/auth/APIAuthenticator.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java similarity index 97% rename from server/src/com/cloud/api/auth/APIAuthenticator.java rename to api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java index 90cd7ec7a8a..20fe61f7fbf 100644 --- a/server/src/com/cloud/api/auth/APIAuthenticator.java +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java @@ -14,7 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.api.auth; +package org.apache.cloudstack.api.auth; import org.apache.cloudstack.api.ServerApiException; diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java similarity index 98% rename from server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java rename to plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java index ce97cfd71d3..611c69b33dc 100644 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java @@ -15,9 +15,9 @@ // specific language governing permissions and limitations // under the License. -package com.cloud.api.auth; +package org.apache.cloudstack.api.command; -import com.cloud.api.ApiServerService; +import org.apache.cloudstack.api.ApiServerService; import com.cloud.api.response.ApiResponseSerializer; import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; @@ -30,6 +30,8 @@ import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; @@ -187,7 +189,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent authnRequest.setIssuer(issuer); authnRequest.setIssueInstant(new DateTime()); authnRequest.setProviderName("Apache CloudStack"); - authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); //SAML2_ARTIFACT_BINDING_URI); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); authnRequest.setAssertionConsumerServiceURL(consumerUrl); authnRequest.setNameIDPolicy(nameIdPolicy); authnRequest.setRequestedAuthnContext(requestedAuthnContext); diff --git a/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java similarity index 94% rename from server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java rename to plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 91195887424..b82f2c81909 100644 --- a/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -14,7 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. -package com.cloud.api.auth; +package org.apache.cloudstack.api.command; import com.cloud.api.response.ApiResponseSerializer; import com.cloud.user.Account; @@ -22,6 +22,8 @@ import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.response.LogoutCmdResponse; import org.apache.log4j.Logger; diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java similarity index 98% rename from plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java rename to plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java index 4d4f1d3e8d1..1a37f65c057 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java @@ -12,7 +12,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package org.apache.cloudstack; +package org.apache.cloudstack.saml; import com.cloud.server.auth.DefaultUserAuthenticator; import com.cloud.server.auth.UserAuthenticator; From 74f57959085c170fe18808b9da9eac2a1cb22d78 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:52:55 +0200 Subject: [PATCH 15/49] api: fix refactored ApiServerService interface to api Signed-off-by: Rohit Yadav --- api/pom.xml | 5 +++++ api/src/org/apache/cloudstack/api/ApiServerService.java | 3 --- api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index abfa2c59e02..ba547dfa47a 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -35,6 +35,11 @@ com.google.code.gson gson + + javax.servlet + servlet-api + ${cs.servlet.version} + org.apache.cloudstack cloud-framework-db diff --git a/api/src/org/apache/cloudstack/api/ApiServerService.java b/api/src/org/apache/cloudstack/api/ApiServerService.java index 9c0cfa39d79..69215c51658 100644 --- a/api/src/org/apache/cloudstack/api/ApiServerService.java +++ b/api/src/org/apache/cloudstack/api/ApiServerService.java @@ -17,9 +17,6 @@ package org.apache.cloudstack.api; import com.cloud.exception.CloudAuthenticationException; -import org.apache.cloudstack.api.ResponseObject; -import org.apache.cloudstack.api.ServerApiException; - import javax.servlet.http.HttpSession; import java.util.Map; diff --git a/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java index 20fe61f7fbf..b008f00aed1 100644 --- a/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java @@ -37,5 +37,4 @@ public interface APIAuthenticator { HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException; public APIAuthenticationType getAPIType(); - } From 7ff50499a1b2eb42beeea021d850d5bf5a726571 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:53:53 +0200 Subject: [PATCH 16/49] api: Add PluggableAPIAuthenticator interface This interface is used by any plugin for implementing a pluggable API authenticator such as SAML, OAuth etc. Signed-off-by: Rohit Yadav --- .../api/auth/PluggableAPIAuthenticator.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 api/src/org/apache/cloudstack/api/auth/PluggableAPIAuthenticator.java diff --git a/api/src/org/apache/cloudstack/api/auth/PluggableAPIAuthenticator.java b/api/src/org/apache/cloudstack/api/auth/PluggableAPIAuthenticator.java new file mode 100644 index 00000000000..e1e46b86052 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/auth/PluggableAPIAuthenticator.java @@ -0,0 +1,25 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.auth; + +import com.cloud.utils.component.Adapter; + +import java.util.List; + +public interface PluggableAPIAuthenticator extends Adapter { + public List> getAuthCommands(); +} \ No newline at end of file From 47c90508712e7bc6a9dfed7c53ebc0b0ccd27696 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:54:55 +0200 Subject: [PATCH 17/49] core: Add registry and beans in spring registry for PluggableAPIAuthenticator Signed-off-by: Rohit Yadav --- .../spring-core-lifecycle-api-context-inheritable.xml | 6 ++++++ .../core/spring-core-registry-core-context.xml | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml b/core/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml index d4dcc80b2b5..f1566b13021 100644 --- a/core/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml +++ b/core/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml @@ -35,6 +35,12 @@ value="com.cloud.server.auth.UserAuthenticator" /> + + + + + - + + + + + + + Date: Sun, 24 Aug 2014 15:56:10 +0200 Subject: [PATCH 18/49] saml2: Implement SAML2AuthServiceImpl which is a PluggableAPIAuthenticator Signed-off-by: Rohit Yadav --- .../cloudstack/saml/SAML2AuthServiceImpl.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java new file mode 100644 index 00000000000..44e29ca7ac6 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.saml; + +import com.cloud.utils.component.AdapterBase; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd; +import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import javax.ejb.Local; +import java.util.ArrayList; +import java.util.List; + +@Component +@Local(value = PluggableAPIAuthenticator.class) +public class SAML2AuthServiceImpl extends AdapterBase implements PluggableAPIAuthenticator { + private static final Logger s_logger = Logger.getLogger(SAML2AuthServiceImpl.class); + + protected SAML2AuthServiceImpl() { + super(); + } + + @Override + public boolean start() { + return true; + } + + @Override + public List> getAuthCommands() { + List> cmdList = new ArrayList>(); + cmdList.add(SAML2LoginAPIAuthenticatorCmd.class); + cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class); + return cmdList; + } +} From 591a686d77b93675fcfa0c7940bddd02a389dffd Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 15:59:10 +0200 Subject: [PATCH 19/49] server: fix server package after auth plugin refactor - Have opensaml dependency to get version from root pom - add com.cloud.api.auth.APIAuthenticationManagerImpl to spring ctx manager - Fix getCommands() in APIAuthenticationManagerImpl - Fix imports in resources, test and src classes Signed-off-by: Rohit Yadav --- server/pom.xml | 2 +- .../spring-server-core-managers-context.xml | 5 +++- server/src/com/cloud/api/ApiServlet.java | 7 ++--- .../auth/APIAuthenticationManagerImpl.java | 27 +++++++++++++------ .../auth/DefaultLoginAPIAuthenticatorCmd.java | 4 ++- .../DefaultLogoutAPIAuthenticatorCmd.java | 2 ++ server/test/com/cloud/api/ApiServletTest.java | 6 ++--- 7 files changed, 36 insertions(+), 17 deletions(-) diff --git a/server/pom.xml b/server/pom.xml index 04c25aff904..0e517f7854e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -136,7 +136,7 @@ org.opensaml opensaml - 2.6.1 + ${cs.opensaml.version} diff --git a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 17681f7c4dc..e2d4d2798a4 100644 --- a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -32,7 +32,10 @@ http://www.springframework.org/schema/util/spring-util-3.0.xsd" > - + + + _apiAuthenticators; + private static Map> s_authenticators = null; - private static List> s_commandList = null; public APIAuthenticationManagerImpl() { } + public List getApiAuthenticators() { + return _apiAuthenticators; + } + + public void setApiAuthenticators(List authenticators) { + _apiAuthenticators = authenticators; + } + @Override public boolean start() { s_authenticators = new HashMap>(); @@ -53,14 +65,13 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth @Override public List> getCommands() { - if (s_commandList == null) { - s_commandList = new ArrayList>(); - s_commandList.add(DefaultLoginAPIAuthenticatorCmd.class); - s_commandList.add(DefaultLogoutAPIAuthenticatorCmd.class); - s_commandList.add(SAML2LoginAPIAuthenticatorCmd.class); - s_commandList.add(SAML2LogoutAPIAuthenticatorCmd.class); + List> cmdList = new ArrayList>(); + cmdList.add(DefaultLoginAPIAuthenticatorCmd.class); + cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class); + for (PluggableAPIAuthenticator apiAuthenticator: _apiAuthenticators) { + cmdList.addAll(apiAuthenticator.getAuthCommands()); } - return s_commandList; + return cmdList; } @Override diff --git a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java index 243ad9b1697..2fb3f560175 100644 --- a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java @@ -16,7 +16,7 @@ // under the License. package com.cloud.api.auth; -import com.cloud.api.ApiServerService; +import org.apache.cloudstack.api.ApiServerService; import com.cloud.api.response.ApiResponseSerializer; import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; @@ -26,6 +26,8 @@ import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.log4j.Logger; diff --git a/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java index a5802bfcbc6..999cefd8e3f 100644 --- a/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java @@ -22,6 +22,8 @@ import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.response.LogoutCmdResponse; import org.apache.log4j.Logger; diff --git a/server/test/com/cloud/api/ApiServletTest.java b/server/test/com/cloud/api/ApiServletTest.java index 0a9029663fa..1a9c13d3e74 100644 --- a/server/test/com/cloud/api/ApiServletTest.java +++ b/server/test/com/cloud/api/ApiServletTest.java @@ -16,9 +16,9 @@ // under the License. package com.cloud.api; -import com.cloud.api.auth.APIAuthenticationManager; -import com.cloud.api.auth.APIAuthenticationType; -import com.cloud.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.APIAuthenticationManager; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; import com.cloud.server.ManagementServer; import com.cloud.user.Account; import com.cloud.user.AccountService; From d45b303569af77c3c8c673bfb47ad6fdf21a8cbc Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 16:01:22 +0200 Subject: [PATCH 20/49] saml2: Fix plugin after refactoring - Use opensaml version from root pom - Add utils and api as explicit dependency - Add org.apache.cloudstack.saml.SAML2AuthServiceImpl bean - Fix imports in all source files and resource xmls - Use methods available from SAMLUtils to encode/decode SAML request/response - SAML logout api is not the global logout api Signed-off-by: Rohit Yadav --- plugins/user-authenticators/saml2/pom.xml | 12 +- .../cloudstack/saml2/spring-saml2-context.xml | 6 +- .../SAML2LoginAPIAuthenticatorCmd.java | 128 ++---------------- .../SAML2LogoutAPIAuthenticatorCmd.java | 2 +- .../saml/SAML2UserAuthenticator.java | 3 +- 5 files changed, 27 insertions(+), 124 deletions(-) diff --git a/plugins/user-authenticators/saml2/pom.xml b/plugins/user-authenticators/saml2/pom.xml index a220dcf2779..df6aa458f3f 100644 --- a/plugins/user-authenticators/saml2/pom.xml +++ b/plugins/user-authenticators/saml2/pom.xml @@ -35,7 +35,17 @@ org.opensaml opensaml - 2.6.1 + ${cs.opensaml.version} + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-api + ${project.version} diff --git a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml index f244292c3b6..15e085ded20 100644 --- a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml +++ b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml @@ -25,8 +25,12 @@ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> - + + + + + 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 611c69b33dc..463df7d117e 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 @@ -34,63 +34,35 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.utils.auth.SAMLUtils; import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; -import org.opensaml.common.SAMLVersion; -import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeStatement; -import org.opensaml.saml2.core.AuthnContextClassRef; -import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; import org.opensaml.saml2.core.AuthnRequest; -import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.NameIDPolicy; import org.opensaml.saml2.core.NameIDType; -import org.opensaml.saml2.core.RequestedAuthnContext; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.StatusCode; -import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; -import org.opensaml.saml2.core.impl.AuthnRequestBuilder; -import org.opensaml.saml2.core.impl.IssuerBuilder; -import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; -import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; import org.opensaml.xml.ConfigurationException; -import org.opensaml.xml.XMLObject; -import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.io.MarshallingException; -import org.opensaml.xml.io.Unmarshaller; -import org.opensaml.xml.io.UnmarshallerFactory; import org.opensaml.xml.io.UnmarshallingException; import org.opensaml.xml.signature.Signature; -import org.opensaml.xml.util.Base64; -import org.opensaml.xml.util.XMLHelper; -import org.w3c.dom.Document; -import org.w3c.dom.Element; import org.xml.sax.SAXException; import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.FactoryConfigurationError; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.StringWriter; import java.math.BigInteger; import java.net.URLEncoder; import java.security.SecureRandom; import java.util.List; import java.util.Map; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; @APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { @@ -142,107 +114,23 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent String redirectUrl = ""; try { DefaultBootstrap.bootstrap(); - AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl); - redirectUrl = identityProviderUrl + "?SAMLRequest=" + encodeAuthnRequest(authnRequest); + AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl); + redirectUrl = identityProviderUrl + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest); } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); } return redirectUrl; } - private AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) { - // Issuer object - IssuerBuilder issuerBuilder = new IssuerBuilder(); - Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(spId); - - // NameIDPolicy - NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); - NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); - nameIdPolicy.setFormat(NameIDType.PERSISTENT); - nameIdPolicy.setSPNameQualifier("Apache CloudStack"); - nameIdPolicy.setAllowCreate(true); - - // AuthnContextClass - AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); - AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( - SAMLConstants.SAML20_NS, - "AuthnContextClassRef", "saml"); - authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); - - // AuthnContex - RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); - RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); - requestedAuthnContext - .setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM); - requestedAuthnContext.getAuthnContextClassRefs().add( - authnContextClassRef); - - // Creation of AuthRequestObject - AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); - AuthnRequest authnRequest = authRequestBuilder.buildObject(); - authnRequest.setID(authnId); - authnRequest.setDestination(idpUrl); - authnRequest.setVersion(SAMLVersion.VERSION_20); - authnRequest.setForceAuthn(true); - authnRequest.setIsPassive(false); - authnRequest.setIssuer(issuer); - authnRequest.setIssueInstant(new DateTime()); - authnRequest.setProviderName("Apache CloudStack"); - authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); - authnRequest.setAssertionConsumerServiceURL(consumerUrl); - authnRequest.setNameIDPolicy(nameIdPolicy); - authnRequest.setRequestedAuthnContext(requestedAuthnContext); - - return authnRequest; - } - - private String encodeAuthnRequest(AuthnRequest authnRequest) - throws MarshallingException, IOException { - Marshaller marshaller = Configuration.getMarshallerFactory() - .getMarshaller(authnRequest); - Element authDOM = marshaller.marshall(authnRequest); - StringWriter requestWriter = new StringWriter(); - XMLHelper.writeNode(authDOM, requestWriter); - String requestMessage = requestWriter.toString(); - Deflater deflater = new Deflater(Deflater.DEFLATED, true); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater); - deflaterOutputStream.write(requestMessage.getBytes()); - deflaterOutputStream.close(); - String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); - encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim(); - return encodedRequestMessage; - } - public Response processSAMLResponse(String responseMessage) { - XMLObject responseObject = null; + Response responseObject = null; try { - responseObject = this.unmarshall(responseMessage); + responseObject = SAMLUtils.decodeSAMLResponse(responseMessage); } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { s_logger.error("SAMLResponse processing error: " + e.getMessage()); } - return (Response) responseObject; - } - - private XMLObject unmarshall(String responseMessage) - throws ConfigurationException, ParserConfigurationException, - SAXException, IOException, UnmarshallingException { - try { - DefaultBootstrap.bootstrap(); - } catch (ConfigurationException | FactoryConfigurationError e) { - s_logger.error("SAML response message decoding error: " + e.getMessage()); - } - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); - DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); - byte[] base64DecodedResponse = Base64.decode(responseMessage); - Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse)); - Element element = document.getDocumentElement(); - UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); - Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); - return unmarshaller.unmarshall(element); + return responseObject; } @Override @@ -282,7 +170,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) { username = nameId.getValue(); - uniqueUserId = "saml-" + username; + uniqueUserId = SAMLUtils.createSAMLId(username); if (nameId.getFormat().equals(NameIDType.EMAIL)) { email = username; } @@ -299,7 +187,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent(); if (attributeName.equalsIgnoreCase("uid") && uniqueUserId == null) { username = attributeValue; - uniqueUserId = "saml-" + username; + uniqueUserId = SAMLUtils.createSAMLId(username); } else if (attributeName.equalsIgnoreCase("givenName")) { firstName = attributeValue; } else if (attributeName.equalsIgnoreCase(("sn"))) { diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index b82f2c81909..32e2f99ba88 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -31,7 +31,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Map; -@APICommand(name = "samlslo", description = "SAML Single Log Out API", responseObject = LogoutCmdResponse.class, entityType = {}) +@APICommand(name = "samlslo", description = "SAML Global Log Out API", responseObject = LogoutCmdResponse.class, entityType = {}) public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName()); private static final String s_name = "logoutresponse"; diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java index 1a37f65c057..a4902d10312 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java @@ -21,6 +21,7 @@ import com.cloud.user.UserAccount; import com.cloud.user.dao.UserAccountDao; import com.cloud.user.dao.UserDao; import com.cloud.utils.Pair; +import org.apache.cloudstack.utils.auth.SAMLUtils; import org.apache.log4j.Logger; import javax.ejb.Local; @@ -48,7 +49,7 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator { } else { User user = _userDao.getUser(userAccount.getId()); // TODO: check SAMLRequest, signature etc. from requestParameters - if (user != null && user.getUuid().startsWith("saml")) { + if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid())) { return new Pair(true, null); } } From 37d696db80fd521b7ddc9b958cb5205c496e75bc Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 16:03:21 +0200 Subject: [PATCH 21/49] ApiServer: Fix imports order, use org.apache.cloudstack.api.ApiServerService Signed-off-by: Rohit Yadav --- server/src/com/cloud/api/ApiServer.java | 214 ++++++++++++------------ 1 file changed, 106 insertions(+), 108 deletions(-) diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index fc550269d61..43572307212 100755 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -16,82 +16,49 @@ // under the License. package com.cloud.api; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.security.SecureRandom; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - +import com.cloud.api.dispatch.DispatchChainFactory; +import com.cloud.api.dispatch.DispatchTask; +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.configuration.Config; +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.event.ActionEventUtils; +import com.cloud.event.EventCategory; +import com.cloud.event.EventTypes; +import com.cloud.exception.AccountLimitException; +import com.cloud.exception.CloudAuthenticationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.RequestLimitException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserVO; import com.cloud.utils.HttpUtils; -import org.apache.cloudstack.api.response.LoginCmdResponse; -import org.apache.commons.codec.binary.Base64; -import org.apache.http.ConnectionClosedException; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.HttpServerConnection; -import org.apache.http.HttpStatus; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.entity.BasicHttpEntity; -import org.apache.http.impl.DefaultHttpResponseFactory; -import org.apache.http.impl.DefaultHttpServerConnection; -import org.apache.http.impl.NoConnectionReuseStrategy; -import org.apache.http.impl.SocketHttpServerConnection; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.CoreConnectionPNames; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.BasicHttpProcessor; -import org.apache.http.protocol.HttpContext; -import org.apache.http.protocol.HttpRequestHandler; -import org.apache.http.protocol.HttpRequestHandlerRegistry; -import org.apache.http.protocol.HttpService; -import org.apache.http.protocol.ResponseConnControl; -import org.apache.http.protocol.ResponseContent; -import org.apache.http.protocol.ResponseDate; -import org.apache.http.protocol.ResponseServer; -import org.apache.log4j.Logger; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.stereotype.Component; - +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; +import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.UUIDManager; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.exception.ExceptionProxyObject; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; import org.apache.cloudstack.api.BaseCmd; @@ -124,6 +91,7 @@ import org.apache.cloudstack.api.response.AsyncJobResponse; import org.apache.cloudstack.api.response.CreateCmdResponse; import org.apache.cloudstack.api.response.ExceptionResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.impl.ConfigurationVO; @@ -136,44 +104,74 @@ import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.MessageDispatcher; import org.apache.cloudstack.framework.messagebus.MessageHandler; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.ConnectionClosedException; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpServerConnection; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.DefaultHttpResponseFactory; +import org.apache.http.impl.DefaultHttpServerConnection; +import org.apache.http.impl.NoConnectionReuseStrategy; +import org.apache.http.impl.SocketHttpServerConnection; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.CoreConnectionPNames; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.BasicHttpProcessor; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpRequestHandler; +import org.apache.http.protocol.HttpRequestHandlerRegistry; +import org.apache.http.protocol.HttpService; +import org.apache.http.protocol.ResponseConnControl; +import org.apache.http.protocol.ResponseContent; +import org.apache.http.protocol.ResponseDate; +import org.apache.http.protocol.ResponseServer; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.stereotype.Component; -import com.cloud.api.dispatch.DispatchChainFactory; -import com.cloud.api.dispatch.DispatchTask; -import com.cloud.api.response.ApiResponseSerializer; -import com.cloud.configuration.Config; -import com.cloud.domain.Domain; -import com.cloud.domain.DomainVO; -import com.cloud.domain.dao.DomainDao; -import com.cloud.event.ActionEventUtils; -import com.cloud.event.EventCategory; -import com.cloud.event.EventTypes; -import com.cloud.exception.AccountLimitException; -import com.cloud.exception.CloudAuthenticationException; -import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.exception.RequestLimitException; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.user.Account; -import com.cloud.user.AccountManager; -import com.cloud.user.DomainManager; -import com.cloud.user.User; -import com.cloud.user.UserAccount; -import com.cloud.user.UserVO; -import com.cloud.utils.NumbersUtil; -import com.cloud.utils.Pair; -import com.cloud.utils.StringUtils; -import com.cloud.utils.component.ComponentContext; -import com.cloud.utils.component.ManagerBase; -import com.cloud.utils.component.PluggableService; -import com.cloud.utils.concurrency.NamedThreadFactory; -import com.cloud.utils.db.EntityManager; -import com.cloud.utils.db.SearchCriteria; -import com.cloud.utils.db.TransactionLegacy; -import com.cloud.utils.db.UUIDManager; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.exception.ExceptionProxyObject; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.security.SecureRandom; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Component public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService { From 37961ebdd8690075c39925476c12c705388a4014 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 17:32:56 +0200 Subject: [PATCH 22/49] saml: Implement SAML2AuthManager interface Signed-off-by: Rohit Yadav --- .../cloudstack/saml/SAML2AuthManager.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java new file mode 100644 index 00000000000..c01cf21926b --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.saml; + +public interface SAML2AuthManager { + public String getServiceProviderId(); + public String getSpSingleSignOnUrl(); + public String getSpSingleLogOutUrl(); + + public String getIdpSingleSignOnUrl(); + public String getIdpSingleLogOutUrl(); +} From 5d94fd5be51384e878e84af5618b51ac5b30a8f1 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 17:33:20 +0200 Subject: [PATCH 23/49] server: Add SAML related config params in Config Signed-off-by: Rohit Yadav --- .../src/com/cloud/configuration/Config.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index b499df58bd4..3f42a525dc5 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1379,6 +1379,46 @@ public enum Config { "300000", "The allowable clock difference in milliseconds between when an SSO login request is made and when it is received.", null), + SAMLServiceProviderID( + "Advanced", + ManagementServer.class, + String.class, + "saml2.sp.id", + "Apache CloudStack", + "SAML2 Service Provider Identifier String", + null), + SAMLServiceProviderSingleSignOnURL( + "Advanced", + ManagementServer.class, + String.class, + "saml2.sp.sso.url", + "http://localhost:8080/client/api?command=samlsso", + "SAML2 CloudStack Service Provider Single Sign On URL", + null), + SAMLServiceProviderSingleLogOutURL( + "Advanced", + ManagementServer.class, + String.class, + "saml2.sp.slo.url", + "http://localhost:8080/client/api?command=samlslo", + "SAML2 CloudStack Service Provider Single Log Out URL", + null), + SAMLIdentityProviderMetadataURL( + "Advanced", + ManagementServer.class, + String.class, + "saml2.idp.metadata.url", + "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php", + "SAML2 Identity Provider Metadata XML Url", + null), + SAMLTimeout( + "Advanced", + ManagementServer.class, + Long.class, + "saml2.timeout", + "30000", + "SAML2 IDP Metadata Downloading and parsing etc. activity timeout in milliseconds", + null), //NetworkType("Hidden", ManagementServer.class, String.class, "network.type", "vlan", "The type of network that this deployment will use.", "vlan,direct"), RouterRamSize("Hidden", NetworkOrchestrationService.class, Integer.class, "router.ram.size", "128", "Default RAM for router VM (in MB).", null), From 1b0f81ec6f13ce967908049a4bb5bba0cd0a08d9 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 17:33:45 +0200 Subject: [PATCH 24/49] utils: Remove hard coded strings from SAMLUtils Signed-off-by: Rohit Yadav --- utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index bc39eaffe21..9c54053a223 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.utils.auth; +import com.cloud.utils.HttpUtils; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.opensaml.Configuration; @@ -85,7 +86,7 @@ public class SAMLUtils { NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); nameIdPolicy.setFormat(NameIDType.PERSISTENT); - nameIdPolicy.setSPNameQualifier("Apache CloudStack"); + nameIdPolicy.setSPNameQualifier(spId); nameIdPolicy.setAllowCreate(true); // AuthnContextClass @@ -113,7 +114,7 @@ public class SAMLUtils { authnRequest.setIsPassive(false); authnRequest.setIssuer(issuer); authnRequest.setIssueInstant(new DateTime()); - authnRequest.setProviderName("Apache CloudStack"); + authnRequest.setProviderName(spId); authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); authnRequest.setAssertionConsumerServiceURL(consumerUrl); authnRequest.setNameIDPolicy(nameIdPolicy); @@ -136,7 +137,7 @@ public class SAMLUtils { deflaterOutputStream.write(requestMessage.getBytes()); deflaterOutputStream.close(); String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); - encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim(); + encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, HttpUtils.UTF_8).trim(); return encodedRequestMessage; } From 06e909923a604a348c9ff18380a868b96145c6e2 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 17:34:20 +0200 Subject: [PATCH 25/49] saml: Have the plugin use IDP metadata from URL, get values from Config Signed-off-by: Rohit Yadav --- .../cloudstack/saml2/spring-saml2-context.xml | 2 +- .../SAML2LoginAPIAuthenticatorCmd.java | 28 +++- .../cloudstack/saml/SAML2AuthManagerImpl.java | 131 ++++++++++++++++++ .../cloudstack/saml/SAML2AuthServiceImpl.java | 51 ------- 4 files changed, 153 insertions(+), 59 deletions(-) create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java delete mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java diff --git a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml index 15e085ded20..92f89b8dfbc 100644 --- a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml +++ b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml @@ -29,7 +29,7 @@ - + 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 463df7d117e..ec3a4d242bb 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 @@ -17,7 +17,6 @@ package org.apache.cloudstack.api.command; -import org.apache.cloudstack.api.ApiServerService; import com.cloud.api.response.ApiResponseSerializer; import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; @@ -27,6 +26,7 @@ import com.cloud.utils.db.EntityManager; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; @@ -34,6 +34,7 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.utils.auth.SAMLUtils; import org.apache.log4j.Logger; import org.opensaml.DefaultBootstrap; @@ -79,6 +80,8 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent ApiServerService _apiServer; @Inject EntityManager _entityMgr; + @Inject + SAML2AuthManager _samlAuthManager; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -108,13 +111,20 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); } - public String buildAuthnRequestUrl(String consumerUrl, String identityProviderUrl) { - String randomId = new BigInteger(130, new SecureRandom()).toString(32); - String spId = "org.apache.cloudstack"; + public String buildAuthnRequestUrl(String idpUrl) { + String randomSecureId = new BigInteger(130, new SecureRandom()).toString(32); + String spId = _samlAuthManager.getServiceProviderId(); + String consumerUrl = _samlAuthManager.getSpSingleSignOnUrl(); + String identityProviderUrl = _samlAuthManager.getIdpSingleSignOnUrl(); + + if (idpUrl != null) { + identityProviderUrl = idpUrl; + } + String redirectUrl = ""; try { DefaultBootstrap.bootstrap(); - AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl); + AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(randomSecureId, spId, identityProviderUrl, consumerUrl); redirectUrl = identityProviderUrl + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest); } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); @@ -137,8 +147,12 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent public String authenticate(final String command, final Map params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { try { if (!params.containsKey("SAMLResponse")) { - final String[] idps = (String[])params.get("idpurl"); - String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=samlsso", idps[0]); + String idpUrl = null; + final String[] idps = (String[])params.get(ApiConstants.IDP_URL); + if (idps != null && idps.length > 0) { + idpUrl = idps[0]; + } + String redirectUrl = buildAuthnRequestUrl(idpUrl); resp.sendRedirect(redirectUrl); return ""; } else { diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java new file mode 100644 index 00000000000..41595b6108a --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -0,0 +1,131 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.saml; + +import com.cloud.configuration.Config; +import com.cloud.utils.component.AdapterBase; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd; +import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.log4j.Logger; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.SingleLogoutService; +import org.opensaml.saml2.metadata.SingleSignOnService; +import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.parse.BasicParserPool; +import org.springframework.stereotype.Component; + +import javax.ejb.Local; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +@Component +@Local(value = {PluggableAPIAuthenticator.class, SAML2AuthManager.class}) +public class SAML2AuthManagerImpl extends AdapterBase implements PluggableAPIAuthenticator, SAML2AuthManager { + private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class); + + private String serviceProviderId; + private String spSingleSignOnUrl; + private String spSingleLogOutUrl; + + private String idpSingleSignOnUrl; + private String idpSingleLogOutUrl; + + @Inject + ConfigurationDao _configDao; + + protected SAML2AuthManagerImpl() { + super(); + } + + @Override + public boolean start() { + this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key()); + this.spSingleSignOnUrl = _configDao.getValue(Config.SAMLServiceProviderSingleSignOnURL.key()); + this.spSingleLogOutUrl = _configDao.getValue(Config.SAMLServiceProviderSingleLogOutURL.key()); + + String idpMetaDataUrl = _configDao.getValue(Config.SAMLIdentityProviderMetadataURL.key()); + + int tolerance = 30000; + String timeout = _configDao.getValue(Config.SAMLTimeout.key()); + if (timeout != null) { + tolerance = Integer.parseInt(timeout); + } + + try { + HTTPMetadataProvider idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance); + + idpMetaDataProvider.setRequireValidMetadata(true); + idpMetaDataProvider.setParserPool(new BasicParserPool()); + idpMetaDataProvider.initialize(); + + EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor("Some entity id"); + for (SingleSignOnService ssos: idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleSignOnServices()) { + if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { + this.idpSingleSignOnUrl = ssos.getLocation(); + } + } + for (SingleLogoutService slos: idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleLogoutServices()) { + if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { + this.idpSingleLogOutUrl = slos.getLocation(); + } + } + + } catch (MetadataProviderException e) { + s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage()); + s_logger.error("SAML2 Authentication may be unavailable"); + } + + if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) { + s_logger.error("The current IDP does not support HTTP redirected authentication, SAML based authentication cannot work with this IDP"); + } + + return true; + } + + @Override + public List> getAuthCommands() { + List> cmdList = new ArrayList>(); + cmdList.add(SAML2LoginAPIAuthenticatorCmd.class); + cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class); + return cmdList; + } + + public String getServiceProviderId() { + return serviceProviderId; + } + + public String getIdpSingleSignOnUrl() { + return this.idpSingleSignOnUrl; + } + + public String getIdpSingleLogOutUrl() { + return this.idpSingleLogOutUrl; + } + + public String getSpSingleSignOnUrl() { + return spSingleSignOnUrl; + } + + public String getSpSingleLogOutUrl() { + return spSingleLogOutUrl; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java deleted file mode 100644 index 44e29ca7ac6..00000000000 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthServiceImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -package org.apache.cloudstack.saml; - -import com.cloud.utils.component.AdapterBase; -import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; -import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd; -import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; - -import javax.ejb.Local; -import java.util.ArrayList; -import java.util.List; - -@Component -@Local(value = PluggableAPIAuthenticator.class) -public class SAML2AuthServiceImpl extends AdapterBase implements PluggableAPIAuthenticator { - private static final Logger s_logger = Logger.getLogger(SAML2AuthServiceImpl.class); - - protected SAML2AuthServiceImpl() { - super(); - } - - @Override - public boolean start() { - return true; - } - - @Override - public List> getAuthCommands() { - List> cmdList = new ArrayList>(); - cmdList.add(SAML2LoginAPIAuthenticatorCmd.class); - cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class); - return cmdList; - } -} From 0444bfb65350c71ddcdcd0c58968e4534ceb8a89 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 18:28:06 +0200 Subject: [PATCH 26/49] server: Add IDP entity ID config param Signed-off-by: Rohit Yadav --- server/src/com/cloud/configuration/Config.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index 3f42a525dc5..b2fb85f4b28 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1403,6 +1403,14 @@ public enum Config { "http://localhost:8080/client/api?command=samlslo", "SAML2 CloudStack Service Provider Single Log Out URL", null), + SAMLIdentityProviderID( + "Advanced", + ManagementServer.class, + String.class, + "saml2.idp.id", + "https://openidp.feide.no", + "SAML2 Identity Provider Identifier String", + null), SAMLIdentityProviderMetadataURL( "Advanced", ManagementServer.class, From 8e6cb044801d151c5fd56eb03d8dbca22a976f39 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 20:46:20 +0200 Subject: [PATCH 27/49] SAMLUtils: leave bootstrapping to upper layers Signed-off-by: Rohit Yadav --- utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index 9c54053a223..fc0ca0954f0 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -23,7 +23,6 @@ import com.cloud.utils.HttpUtils; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.opensaml.Configuration; -import org.opensaml.DefaultBootstrap; import org.opensaml.common.SAMLVersion; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.AuthnContextClassRef; @@ -54,7 +53,6 @@ import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import javax.xml.stream.FactoryConfigurationError; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -144,11 +142,6 @@ public class SAMLUtils { public static Response decodeSAMLResponse(String responseMessage) throws ConfigurationException, ParserConfigurationException, SAXException, IOException, UnmarshallingException { - try { - DefaultBootstrap.bootstrap(); - } catch (ConfigurationException | FactoryConfigurationError e) { - s_logger.error("SAML response message decoding error: " + e.getMessage()); - } DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); From 47ccce85a12ef4932b67359e20c2fba7c192ff1c Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 20:47:43 +0200 Subject: [PATCH 28/49] api: add method to pass on api authenticators to cmd classes Signed-off-by: Rohit Yadav --- .../cloudstack/api/auth/APIAuthenticator.java | 4 ++ .../SAML2LoginAPIAuthenticatorCmd.java | 38 ++++++++++++++++--- .../auth/APIAuthenticationManagerImpl.java | 1 + .../auth/DefaultLoginAPIAuthenticatorCmd.java | 6 +++ .../DefaultLogoutAPIAuthenticatorCmd.java | 6 +++ .../src/com/cloud/configuration/Config.java | 2 +- 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java index b008f00aed1..67fa1d8816e 100644 --- a/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java @@ -20,6 +20,7 @@ import org.apache.cloudstack.api.ServerApiException; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.util.List; import java.util.Map; /* @@ -36,5 +37,8 @@ public interface APIAuthenticator { public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException; + public APIAuthenticationType getAPIType(); + + public void setAuthenticators(List authenticators); } 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 ec3a4d242bb..88acfe1e916 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 @@ -32,6 +32,7 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.saml.SAML2AuthManager; @@ -49,7 +50,10 @@ import org.opensaml.saml2.core.StatusCode; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.io.MarshallingException; import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.security.x509.BasicX509Credential; import org.opensaml.xml.signature.Signature; +import org.opensaml.xml.signature.SignatureValidator; +import org.opensaml.xml.validation.ValidationException; import org.xml.sax.SAXException; import javax.inject.Inject; @@ -80,7 +84,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent ApiServerService _apiServer; @Inject EntityManager _entityMgr; - @Inject + SAML2AuthManager _samlAuthManager; ///////////////////////////////////////////////////// @@ -135,9 +139,10 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent public Response processSAMLResponse(String responseMessage) { Response responseObject = null; try { + DefaultBootstrap.bootstrap(); responseObject = SAMLUtils.decodeSAMLResponse(responseMessage); - } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { + } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { s_logger.error("SAMLResponse processing error: " + e.getMessage()); } return responseObject; @@ -165,9 +170,20 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent params, responseType)); } - Signature sig = processedSAMLResponse.getSignature(); - //SignatureValidator validator = new SignatureValidator(credential); - //validator.validate(sig); + if (_samlAuthManager.getIdpSigningKey() != null) { + Signature sig = processedSAMLResponse.getSignature(); + BasicX509Credential credential = new BasicX509Credential(); + credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey()); + SignatureValidator validator = new SignatureValidator(credential); + try { + validator.validate(sig); + } catch (ValidationException e) { + s_logger.error("SAML Response's signature failed to be validated by IDP signing key:" + e.getMessage()); + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "SAML Response's signature failed to be validated by IDP signing key", + params, responseType)); + } + } String uniqueUserId = null; String accountName = "admin"; //GET from config, try, fail @@ -251,4 +267,16 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent public APIAuthenticationType getAPIType() { return APIAuthenticationType.LOGIN_API; } + + @Override + public void setAuthenticators(List authenticators) { + for (PluggableAPIAuthenticator authManager: authenticators) { + if (authManager instanceof SAML2AuthManager) { + _samlAuthManager = (SAML2AuthManager) authManager; + } + } + if (_samlAuthManager == null) { + s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd"); + } + } } diff --git a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java index 790b6d90e67..24ccbeb6032 100644 --- a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java +++ b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java @@ -81,6 +81,7 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth try { apiAuthenticator = (APIAuthenticator) s_authenticators.get(name).newInstance(); apiAuthenticator = ComponentContext.inject(apiAuthenticator); + apiAuthenticator.setAuthenticators(_apiAuthenticators); } catch (InstantiationException | IllegalAccessException e) { if (s_logger.isDebugEnabled()) { s_logger.debug("APIAuthenticationManagerImpl::getAPIAuthenticator failed: " + e.getMessage()); diff --git a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java index 2fb3f560175..fa23abd43e4 100644 --- a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java @@ -28,12 +28,14 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.log4j.Logger; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.util.List; import java.util.Map; @APICommand(name = "login", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) @@ -172,4 +174,8 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe public APIAuthenticationType getAPIType() { return APIAuthenticationType.LOGIN_API; } + + @Override + public void setAuthenticators(List authenticators) { + } } diff --git a/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java index 999cefd8e3f..ee7936ad734 100644 --- a/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java @@ -24,11 +24,13 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LogoutCmdResponse; import org.apache.log4j.Logger; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.util.List; import java.util.Map; @APICommand(name = "logout", description = "Logs out the user", responseObject = LogoutCmdResponse.class, entityType = {}) @@ -70,4 +72,8 @@ public class DefaultLogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuth public APIAuthenticationType getAPIType() { return APIAuthenticationType.LOGOUT_API; } + + @Override + public void setAuthenticators(List authenticators) { + } } diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index b2fb85f4b28..de4aaed56a4 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1384,7 +1384,7 @@ public enum Config { ManagementServer.class, String.class, "saml2.sp.id", - "Apache CloudStack", + "org.apache.cloudstack", "SAML2 Service Provider Identifier String", null), SAMLServiceProviderSingleSignOnURL( From 7687b7311a9813a2c422e592e8ad1d50a4f8284e Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Sun, 24 Aug 2014 20:48:25 +0200 Subject: [PATCH 29/49] saml: Implement logic to check response against X509 keys Signed-off-by: Rohit Yadav --- .../SAML2LogoutAPIAuthenticatorCmd.java | 6 ++ .../cloudstack/saml/SAML2AuthManager.java | 17 +++- .../cloudstack/saml/SAML2AuthManagerImpl.java | 89 +++++++++++++++---- 3 files changed, 90 insertions(+), 22 deletions(-) diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 32e2f99ba88..723209f80f8 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -24,11 +24,13 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LogoutCmdResponse; import org.apache.log4j.Logger; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.util.List; import java.util.Map; @APICommand(name = "samlslo", description = "SAML Global Log Out API", responseObject = LogoutCmdResponse.class, entityType = {}) @@ -70,4 +72,8 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen public APIAuthenticationType getAPIType() { return APIAuthenticationType.LOGOUT_API; } + + @Override + public void setAuthenticators(List authenticators) { + } } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java index c01cf21926b..507fa04409c 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java @@ -17,11 +17,20 @@ package org.apache.cloudstack.saml; -public interface SAML2AuthManager { - public String getServiceProviderId(); - public String getSpSingleSignOnUrl(); - public String getSpSingleLogOutUrl(); +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import java.security.cert.X509Certificate; + +public interface SAML2AuthManager extends PluggableAPIAuthenticator { + public String getServiceProviderId(); + public String getIdentityProviderId(); + + public X509Certificate getIdpSigningKey(); + public X509Certificate getIdpEncryptionKey(); + + public String getSpSingleSignOnUrl(); public String getIdpSingleSignOnUrl(); + + public String getSpSingleLogOutUrl(); public String getIdpSingleLogOutUrl(); } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index 41595b6108a..7ef126a64bf 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -23,32 +23,48 @@ import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd; import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.log4j.Logger; +import org.opensaml.DefaultBootstrap; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml2.metadata.KeyDescriptor; import org.opensaml.saml2.metadata.SingleLogoutService; import org.opensaml.saml2.metadata.SingleSignOnService; import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider; import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.parse.BasicParserPool; +import org.opensaml.xml.security.credential.UsageType; +import org.opensaml.xml.security.keyinfo.KeyInfoHelper; import org.springframework.stereotype.Component; import javax.ejb.Local; import javax.inject.Inject; +import javax.xml.stream.FactoryConfigurationError; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; @Component -@Local(value = {PluggableAPIAuthenticator.class, SAML2AuthManager.class}) -public class SAML2AuthManagerImpl extends AdapterBase implements PluggableAPIAuthenticator, SAML2AuthManager { +@Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class}) +public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager { private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class); private String serviceProviderId; - private String spSingleSignOnUrl; - private String spSingleLogOutUrl; + private String identityProviderId; + private X509Certificate idpSigningKey; + private X509Certificate idpEncryptionKey; + + private String spSingleSignOnUrl; private String idpSingleSignOnUrl; + + private String spSingleLogOutUrl; private String idpSingleLogOutUrl; + private HTTPMetadataProvider idpMetaDataProvider; + @Inject ConfigurationDao _configDao; @@ -59,6 +75,8 @@ public class SAML2AuthManagerImpl extends AdapterBase implements PluggableAPIAut @Override public boolean start() { this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key()); + this.identityProviderId = _configDao.getValue(Config.SAMLIdentityProviderID.key()); + this.spSingleSignOnUrl = _configDao.getValue(Config.SAMLServiceProviderSingleSignOnURL.key()); this.spSingleLogOutUrl = _configDao.getValue(Config.SAMLServiceProviderSingleLogOutURL.key()); @@ -71,31 +89,54 @@ public class SAML2AuthManagerImpl extends AdapterBase implements PluggableAPIAut } try { - HTTPMetadataProvider idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance); - + DefaultBootstrap.bootstrap(); + idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance); idpMetaDataProvider.setRequireValidMetadata(true); idpMetaDataProvider.setParserPool(new BasicParserPool()); idpMetaDataProvider.initialize(); - EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor("Some entity id"); - for (SingleSignOnService ssos: idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleSignOnServices()) { - if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { - this.idpSingleSignOnUrl = ssos.getLocation(); - } - } - for (SingleLogoutService slos: idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleLogoutServices()) { - if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { - this.idpSingleLogOutUrl = slos.getLocation(); - } - } + EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor(this.identityProviderId); + IDPSSODescriptor idpssoDescriptor = idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + if (idpssoDescriptor != null) { + for (SingleSignOnService ssos: idpssoDescriptor.getSingleSignOnServices()) { + if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { + this.idpSingleSignOnUrl = ssos.getLocation(); + } + } + + for (SingleLogoutService slos: idpssoDescriptor.getSingleLogoutServices()) { + if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { + this.idpSingleLogOutUrl = slos.getLocation(); + } + } + + for (KeyDescriptor kd: idpssoDescriptor.getKeyDescriptors()) { + if (kd.getUse() == UsageType.SIGNING) { + try { + this.idpSigningKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0); + } catch (CertificateException ignored) { + } + } + if (kd.getUse() == UsageType.ENCRYPTION) { + try { + this.idpEncryptionKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0); + } catch (CertificateException ignored) { + } + } + } + } else { + s_logger.warn("Provided IDP XML Metadata does not contain IDPSSODescriptor, SAML authentication may not work"); + } } catch (MetadataProviderException e) { s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage()); s_logger.error("SAML2 Authentication may be unavailable"); + } catch (ConfigurationException | FactoryConfigurationError e) { + s_logger.error("OpenSAML bootstrapping failed: error: " + e.getMessage()); } if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) { - s_logger.error("The current IDP does not support HTTP redirected authentication, SAML based authentication cannot work with this IDP"); + s_logger.error("SAML based authentication won't work"); } return true; @@ -128,4 +169,16 @@ public class SAML2AuthManagerImpl extends AdapterBase implements PluggableAPIAut public String getSpSingleLogOutUrl() { return spSingleLogOutUrl; } + + public String getIdentityProviderId() { + return identityProviderId; + } + + public X509Certificate getIdpSigningKey() { + return idpSigningKey; + } + + public X509Certificate getIdpEncryptionKey() { + return idpEncryptionKey; + } } From a13da8f9e0b0a693ef996810606154181ecbd568 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 00:13:32 +0200 Subject: [PATCH 30/49] saml2: Add GetServiceProviderMetaDataCmd that returns SP metadata XML This adds GetServiceProviderMetaDataCmd which returns SP metadata XML, since this information should be public for IdPs to discover, we implement this as a login/cmd api so this does not require any kind of authentication to GET this Signed-off-by: Rohit Yadav --- .../GetServiceProviderMetaDataCmd.java | 202 ++++++++++++++++++ .../api/response/SAMLMetaDataResponse.java | 40 ++++ .../cloudstack/saml/SAML2AuthManagerImpl.java | 2 + 3 files changed, 244 insertions(+) create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SAMLMetaDataResponse.java 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 new file mode 100644 index 00000000000..16ee0889e99 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java @@ -0,0 +1,202 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ApiServerService; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.SAMLMetaDataResponse; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.log4j.Logger; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.metadata.AssertionConsumerService; +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml2.metadata.NameIDFormat; +import org.opensaml.saml2.metadata.SPSSODescriptor; +import org.opensaml.saml2.metadata.SingleLogoutService; +import org.opensaml.saml2.metadata.impl.AssertionConsumerServiceBuilder; +import org.opensaml.saml2.metadata.impl.EntityDescriptorBuilder; +import org.opensaml.saml2.metadata.impl.KeyDescriptorBuilder; +import org.opensaml.saml2.metadata.impl.NameIDFormatBuilder; +import org.opensaml.saml2.metadata.impl.SPSSODescriptorBuilder; +import org.opensaml.saml2.metadata.impl.SingleLogoutServiceBuilder; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.security.credential.UsageType; +import org.opensaml.xml.security.keyinfo.KeyInfoGenerator; +import org.opensaml.xml.security.x509.BasicX509Credential; +import org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory; +import org.w3c.dom.Document; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; + +@APICommand(name = "getSPMetadata", description = "Returns SAML2 CloudStack Service Provider MetaData", responseObject = SAMLMetaDataResponse.class, entityType = {}) +public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(GetServiceProviderMetaDataCmd.class.getName()); + private static final String s_name = "spmetadataresponse"; + + @Inject + ApiServerService _apiServer; + + SAML2AuthManager _samlAuthManager; + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication plugin api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletResponse resp) throws ServerApiException { + SAMLMetaDataResponse response = new SAMLMetaDataResponse(); + response.setResponseName(getCommandName()); + + try { + DefaultBootstrap.bootstrap(); + } catch (ConfigurationException | FactoryConfigurationError e) { + s_logger.error("OpenSAML Bootstrapping error: " + e.getMessage()); + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "OpenSAML Bootstrapping error while creating SP MetaData", + params, responseType)); + } + + EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject(); + spEntityDescriptor.setEntityID(_samlAuthManager.getServiceProviderId()); + + SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject(); + spSSODescriptor.setWantAssertionsSigned(true); + spSSODescriptor.setAuthnRequestsSigned(false); + + X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); + keyInfoGeneratorFactory.setEmitEntityCertificate(true); + KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); + + KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject(); + encKeyDescriptor.setUse(UsageType.ENCRYPTION); + + KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject(); + signKeyDescriptor.setUse(UsageType.SIGNING); + + BasicX509Credential credential = new BasicX509Credential(); + credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey()); + try { + encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); + signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); + //TODO: generate own pub/priv keys + //spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); + //spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); + } catch (SecurityException ignored) { + } + + NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject(); + nameIDFormat.setFormat(NameIDType.PERSISTENT); + spSSODescriptor.getNameIDFormats().add(nameIDFormat); + + AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject(); + assertionConsumerService.setIndex(0); + assertionConsumerService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + assertionConsumerService.setLocation(_samlAuthManager.getSpSingleSignOnUrl()); + + SingleLogoutService ssoService = new SingleLogoutServiceBuilder().buildObject(); + ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + ssoService.setLocation(_samlAuthManager.getSpSingleLogOutUrl()); + + spSSODescriptor.getSingleLogoutServices().add(ssoService); + spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); + spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); + spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor); + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + Marshaller out = Configuration.getMarshallerFactory().getMarshaller(spEntityDescriptor); + out.marshall(spEntityDescriptor, document); + + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + StringWriter stringWriter = new StringWriter(); + StreamResult streamResult = new StreamResult(stringWriter); + DOMSource source = new DOMSource(document); + transformer.transform(source, streamResult); + stringWriter.close(); + response.setMetadata(stringWriter.toString()); + } catch (ParserConfigurationException | IOException | MarshallingException | TransformerException e) { + response.setMetadata("Error creating Service Provider MetaData XML: " + e.getMessage()); + } + + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } + + @Override + public void setAuthenticators(List authenticators) { + for (PluggableAPIAuthenticator authManager: authenticators) { + if (authManager instanceof SAML2AuthManager) { + _samlAuthManager = (SAML2AuthManager) authManager; + } + } + if (_samlAuthManager == null) { + s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd"); + } + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SAMLMetaDataResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SAMLMetaDataResponse.java new file mode 100644 index 00000000000..e091ea49a94 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SAMLMetaDataResponse.java @@ -0,0 +1,40 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.BaseResponse; + +public class SAMLMetaDataResponse extends BaseResponse { + + @SerializedName("metadata") + @Param(description = "The Metadata XML") + private String metadata; + + public SAMLMetaDataResponse() { + super(); + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index 7ef126a64bf..22d99cbbde5 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.saml; import com.cloud.configuration.Config; import com.cloud.utils.component.AdapterBase; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd; import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -147,6 +148,7 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage List> cmdList = new ArrayList>(); cmdList.add(SAML2LoginAPIAuthenticatorCmd.class); cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class); + cmdList.add(GetServiceProviderMetaDataCmd.class); return cmdList; } From 2a264cc7267f9dbb0b74e665274f200ec5ad9ab5 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 00:15:18 +0200 Subject: [PATCH 31/49] client: add getSPMetadata API in commands.properties Signed-off-by: Rohit Yadav --- client/tomcatconf/commands.properties.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in index 006a4ff1b08..09b7ddc8ee9 100644 --- a/client/tomcatconf/commands.properties.in +++ b/client/tomcatconf/commands.properties.in @@ -21,8 +21,11 @@ ### CloudStack authentication commands login=15 logout=15 + +### SAML SSO/SLO commands samlsso=15 samlslo=15 +getSPMetadata=15 ### Account commands createAccount=7 From 8fa9d2c276309eba231a5bce8b68e525f72787d4 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 00:15:41 +0200 Subject: [PATCH 32/49] utils: fix SAMLUtils's authnrequest maker Signed-off-by: Rohit Yadav --- .../org/apache/cloudstack/utils/auth/SAMLUtils.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index fc0ca0954f0..8a2f93b1053 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -64,7 +64,9 @@ import java.util.zip.DeflaterOutputStream; public class SAMLUtils { public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); - public static final String SAML_NS = "saml-"; + public static final String SAML_NS = "saml://"; + + public static final String CERTIFICATE_NAME = "SAMLSP_CERTIFICATE"; public static String createSAMLId(String uid) { return SAML_NS + uid; @@ -108,15 +110,15 @@ public class SAMLUtils { authnRequest.setID(authnId); authnRequest.setDestination(idpUrl); authnRequest.setVersion(SAMLVersion.VERSION_20); - authnRequest.setForceAuthn(true); + authnRequest.setForceAuthn(false); authnRequest.setIsPassive(false); authnRequest.setIssuer(issuer); authnRequest.setIssueInstant(new DateTime()); - authnRequest.setProviderName(spId); authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); authnRequest.setAssertionConsumerServiceURL(consumerUrl); - authnRequest.setNameIDPolicy(nameIdPolicy); - authnRequest.setRequestedAuthnContext(requestedAuthnContext); + //authnRequest.setProviderName(spId); + //authnRequest.setNameIDPolicy(nameIdPolicy); + //authnRequest.setRequestedAuthnContext(requestedAuthnContext); return authnRequest; } From 2f6fa268f4fd97429224075f19e6314c38888ded Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:02:09 +0200 Subject: [PATCH 33/49] apidocs: add entry for getSPMetaData Signed-off-by: Rohit Yadav --- tools/apidoc/gen_toc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 256870441e2..95f06c8c1aa 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -114,6 +114,7 @@ known_categories = { 'login': 'Authentication', 'logout': 'Authentication', 'saml': 'Authentication', + 'getSPMetadata': 'Authentication', 'Capacity': 'System Capacity', 'NetworkDevice': 'Network Device', 'ExternalLoadBalancer': 'Ext Load Balancer', From b0f3d66f9df597d4d96bd6966e6bfa533445f9ac Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:02:32 +0200 Subject: [PATCH 34/49] Config: add config for saml user account, domain and redirected URL to ACS UI Signed-off-by: Rohit Yadav --- .../src/com/cloud/configuration/Config.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index de4aaed56a4..530303ac595 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1379,6 +1379,30 @@ public enum Config { "300000", "The allowable clock difference in milliseconds between when an SSO login request is made and when it is received.", null), + SAMLUserAccountName( + "Advanced", + ManagementServer.class, + String.class, + "saml2.default.accountname", + "admin", + "The name of the default account to use when creating users from SAML SSO", + null), + SAMLUserDomain( + "Advanced", + ManagementServer.class, + String.class, + "saml2.default.domainid", + "1", + "The default domain UUID to use when creating users from SAML SSO", + null), + SAMLCloudStackRedirectionUrl( + "Advanced", + ManagementServer.class, + String.class, + "saml2.redirect.url", + "http://localhost:8080/client", + "The CloudStack UI url the SSO should redirected to when successful", + null), SAMLServiceProviderID( "Advanced", ManagementServer.class, From b401828aef17b051fcc7874aee81201b305bb5b1 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:03:05 +0200 Subject: [PATCH 35/49] saml: use values from config for user account, domain and redirected url Signed-off-by: Rohit Yadav --- .../SAML2LoginAPIAuthenticatorCmd.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) 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 88acfe1e916..e1d95ef2a7f 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 @@ -18,8 +18,11 @@ package org.apache.cloudstack.api.command; import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.configuration.Config; +import com.cloud.domain.Domain; import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; +import com.cloud.user.DomainManager; import com.cloud.user.User; import com.cloud.utils.HttpUtils; import com.cloud.utils.db.EntityManager; @@ -35,6 +38,7 @@ import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.utils.auth.SAMLUtils; import org.apache.log4j.Logger; @@ -84,6 +88,10 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent ApiServerService _apiServer; @Inject EntityManager _entityMgr; + @Inject + ConfigurationDao _configDao; + @Inject + private DomainManager _domainMgr; SAML2AuthManager _samlAuthManager; @@ -186,8 +194,23 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent } String uniqueUserId = null; - String accountName = "admin"; //GET from config, try, fail - Long domainId = 1L; // GET from config, try, fail + String accountName = _configDao.getValue(Config.SAMLUserAccountName.key()); + String domainString = _configDao.getValue(Config.SAMLUserDomain.key()); + + Long domainId = -1L; + Domain domain = _domainMgr.getDomain(domainString); + if (domain != null) { + domainId = domain.getId(); + } else { + try { + domainId = Long.parseLong(domainString); + } catch (NumberFormatException ignore) { + } + } + if (domainId == -1L) { + s_logger.error("The default domain ID for SAML users is not set correct, it should be a UUID"); + } + String username = null; String password = ""; String firstName = ""; @@ -246,7 +269,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName())); - resp.sendRedirect("http://localhost:8080/client"); + resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); return ApiResponseSerializer.toSerializedString(loginResponse, responseType); } From 230e9705692705c5ec06d8f822c91892a4a026fc Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:50:47 +0200 Subject: [PATCH 36/49] SAMLUtils: add logout request utility for saml slo Signed-off-by: Rohit Yadav --- .../cloudstack/utils/auth/SAMLUtils.java | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index 8a2f93b1053..9eafe55cf9b 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -29,16 +29,22 @@ import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.LogoutRequest; +import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.NameIDPolicy; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.RequestedAuthnContext; import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.SessionIndex; import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; import org.opensaml.saml2.core.impl.AuthnRequestBuilder; import org.opensaml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml2.core.impl.LogoutRequestBuilder; import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; +import org.opensaml.saml2.core.impl.SessionIndexBuilder; import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.XMLObject; import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.io.MarshallingException; import org.opensaml.xml.io.Unmarshaller; @@ -57,7 +63,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; +import java.math.BigInteger; import java.net.URLEncoder; +import java.security.SecureRandom; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -65,6 +73,8 @@ public class SAMLUtils { public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); public static final String SAML_NS = "saml://"; + public static final String SAML_NAMEID = "SAML_NAMEID"; + public static final String SAML_SESSION = "SAML_SESSION"; public static final String CERTIFICATE_NAME = "SAMLSP_CERTIFICATE"; @@ -76,7 +86,12 @@ public class SAMLUtils { return uuid.startsWith(SAML_NS); } - public static AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) { + public static String generateSecureRandomId() { + return new BigInteger(130, new SecureRandom()).toString(32); + } + + public static AuthnRequest buildAuthnRequestObject(String spId, String idpUrl, String consumerUrl) { + String authnId = generateSecureRandomId(); // Issuer object IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject(); @@ -123,7 +138,26 @@ public class SAMLUtils { return authnRequest; } - public static String encodeSAMLRequest(AuthnRequest authnRequest) + public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID nameId, String sessionIndex) { + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(spId); + + SessionIndex sessionIndexElement = new SessionIndexBuilder().buildObject(); + sessionIndexElement.setSessionIndex(sessionIndex); + + LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject(); + logoutRequest.setID(generateSecureRandomId()); + logoutRequest.setDestination(logoutUrl); + logoutRequest.setVersion(SAMLVersion.VERSION_20); + logoutRequest.setIssueInstant(new DateTime()); + logoutRequest.setIssuer(issuer); + logoutRequest.getSessionIndexes().add(sessionIndexElement); + logoutRequest.setNameID(nameId); + return logoutRequest; + } + + public static String encodeSAMLRequest(XMLObject authnRequest) throws MarshallingException, IOException { Marshaller marshaller = Configuration.getMarshallerFactory() .getMarshaller(authnRequest); From b1946e8c13a04dbf262f0ee7cf94b55453b711cc Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:51:16 +0200 Subject: [PATCH 37/49] SAML2LoginAPIAuthenticatorCmd: store nameid and session index in user's session Signed-off-by: Rohit Yadav --- .../api/command/SAML2LoginAPIAuthenticatorCmd.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 e1d95ef2a7f..8456872556e 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 @@ -124,7 +124,6 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent } public String buildAuthnRequestUrl(String idpUrl) { - String randomSecureId = new BigInteger(130, new SecureRandom()).toString(32); String spId = _samlAuthManager.getServiceProviderId(); String consumerUrl = _samlAuthManager.getSpSingleSignOnUrl(); String identityProviderUrl = _samlAuthManager.getIdpSingleSignOnUrl(); @@ -136,7 +135,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent String redirectUrl = ""; try { DefaultBootstrap.bootstrap(); - AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(randomSecureId, spId, identityProviderUrl, consumerUrl); + AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(spId, identityProviderUrl, consumerUrl); redirectUrl = identityProviderUrl + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest); } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); @@ -220,6 +219,9 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent Assertion assertion = processedSAMLResponse.getAssertions().get(0); NameID nameId = assertion.getSubject().getNameID(); + String sessionIndex = assertion.getAuthnStatements().get(0).getSessionIndex(); + session.setAttribute(SAMLUtils.SAML_NAMEID, nameId); + session.setAttribute(SAMLUtils.SAML_SESSION, sessionIndex); if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) { username = nameId.getValue(); From 7ee4176c7a8b3f552e48c00dbdb9858dc544c6b3 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:52:05 +0200 Subject: [PATCH 38/49] SAML2LogoutAPIAuthenticatorCmd: implement single log out Signed-off-by: Rohit Yadav --- .../SAML2LogoutAPIAuthenticatorCmd.java | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 723209f80f8..99100741680 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -20,16 +20,27 @@ import com.cloud.api.response.ApiResponseSerializer; import com.cloud.user.Account; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LogoutCmdResponse; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.cloudstack.utils.auth.SAMLUtils; import org.apache.log4j.Logger; +import org.opensaml.DefaultBootstrap; +import org.opensaml.saml2.core.LogoutRequest; +import org.opensaml.saml2.core.NameID; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.io.MarshallingException; +import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.xml.stream.FactoryConfigurationError; +import java.io.IOException; import java.util.List; import java.util.Map; @@ -38,6 +49,10 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName()); private static final String s_name = "logoutresponse"; + @Inject + ApiServerService _apiServer; + SAML2AuthManager _samlAuthManager; + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -60,11 +75,34 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen @Override public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { - auditTrailSb.append("=== Logging out ==="); - // TODO: check global config and do either local or global log out + auditTrailSb.append("=== SAML SLO Logging out ==="); LogoutCmdResponse response = new LogoutCmdResponse(); response.setDescription("success"); response.setResponseName(getCommandName()); + + try { + DefaultBootstrap.bootstrap(); + } catch (ConfigurationException | FactoryConfigurationError e) { + s_logger.error("OpenSAML Bootstrapping error: " + e.getMessage()); + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "OpenSAML Bootstrapping error while creating SP MetaData", + params, responseType)); + } + + NameID nameId = (NameID) session.getAttribute(SAMLUtils.SAML_NAMEID); + String sessionIndex = (String) session.getAttribute(SAMLUtils.SAML_SESSION); + LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex); + + try { + String redirectUrl = _samlAuthManager.getIdpSingleLogOutUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest); + resp.sendRedirect(redirectUrl); + } catch (MarshallingException | IOException e) { + s_logger.error("SAML SLO error: " + e.getMessage()); + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "SAML Single Logout Error", + params, responseType)); + } + return ApiResponseSerializer.toSerializedString(response, responseType); } @@ -75,5 +113,13 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen @Override public void setAuthenticators(List authenticators) { + for (PluggableAPIAuthenticator authManager: authenticators) { + if (authManager instanceof SAML2AuthManager) { + _samlAuthManager = (SAML2AuthManager) authManager; + } + } + if (_samlAuthManager == null) { + s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd"); + } } } From ad13d3d7472bcb2361ba97914fa5c5c90b5429ca Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:57:24 +0200 Subject: [PATCH 39/49] SAML2UserAuthenticator: check that request params has SAMLResponse Signed-off-by: Rohit Yadav --- .../org/apache/cloudstack/saml/SAML2UserAuthenticator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java index a4902d10312..5cd9b524a95 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java @@ -48,8 +48,8 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator { return new Pair(false, null); } else { User user = _userDao.getUser(userAccount.getId()); - // TODO: check SAMLRequest, signature etc. from requestParameters - if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid())) { + if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid()) && + requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) { return new Pair(true, null); } } From 8dc50927f9cfe994e2c2a828aedf77826f2599d9 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 01:58:24 +0200 Subject: [PATCH 40/49] saml: use SAML_RESPONSE from SAMLUtils Signed-off-by: Rohit Yadav --- .../api/command/SAML2LoginAPIAuthenticatorCmd.java | 8 +++----- utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) 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 8456872556e..6c46b044a85 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 @@ -67,9 +67,7 @@ import javax.servlet.http.HttpSession; import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.FactoryConfigurationError; import java.io.IOException; -import java.math.BigInteger; import java.net.URLEncoder; -import java.security.SecureRandom; import java.util.List; import java.util.Map; @@ -123,7 +121,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); } - public String buildAuthnRequestUrl(String idpUrl) { + private String buildAuthnRequestUrl(String idpUrl) { String spId = _samlAuthManager.getServiceProviderId(); String consumerUrl = _samlAuthManager.getSpSingleSignOnUrl(); String identityProviderUrl = _samlAuthManager.getIdpSingleSignOnUrl(); @@ -143,7 +141,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent return redirectUrl; } - public Response processSAMLResponse(String responseMessage) { + private Response processSAMLResponse(String responseMessage) { Response responseObject = null; try { DefaultBootstrap.bootstrap(); @@ -168,7 +166,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent resp.sendRedirect(redirectUrl); return ""; } else { - final String samlResponse = ((String[])params.get("SAMLResponse"))[0]; + final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0]; Response processedSAMLResponse = processSAMLResponse(samlResponse); String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); if (!statusCode.equals(StatusCode.SUCCESS_URI)) { diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index 9eafe55cf9b..a238c82bf4c 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -72,10 +72,10 @@ import java.util.zip.DeflaterOutputStream; public class SAMLUtils { public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); + public static final String SAML_RESPONSE = "SAMLResponse"; public static final String SAML_NS = "saml://"; public static final String SAML_NAMEID = "SAML_NAMEID"; public static final String SAML_SESSION = "SAML_SESSION"; - public static final String CERTIFICATE_NAME = "SAMLSP_CERTIFICATE"; public static String createSAMLId(String uid) { From 3bf387c8828fdd388155704fd64f9bcd84bc3e7a Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 02:39:50 +0200 Subject: [PATCH 41/49] SAMLUtils: Create new NameID using passed nameId taking just id and session idx Signed-off-by: Rohit Yadav --- .../src/org/apache/cloudstack/utils/auth/SAMLUtils.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index a238c82bf4c..51cf5074bd1 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -40,6 +40,7 @@ import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; import org.opensaml.saml2.core.impl.AuthnRequestBuilder; import org.opensaml.saml2.core.impl.IssuerBuilder; import org.opensaml.saml2.core.impl.LogoutRequestBuilder; +import org.opensaml.saml2.core.impl.NameIDBuilder; import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; import org.opensaml.saml2.core.impl.SessionIndexBuilder; @@ -138,7 +139,7 @@ public class SAMLUtils { return authnRequest; } - public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID nameId, String sessionIndex) { + public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID sessionNameId, String sessionIndex) { IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject(); issuer.setValue(spId); @@ -146,6 +147,10 @@ public class SAMLUtils { SessionIndex sessionIndexElement = new SessionIndexBuilder().buildObject(); sessionIndexElement.setSessionIndex(sessionIndex); + NameID nameID = new NameIDBuilder().buildObject(); + nameID.setValue(sessionNameId.getValue()); + nameID.setFormat(sessionNameId.getFormat()); + LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject(); logoutRequest.setID(generateSecureRandomId()); logoutRequest.setDestination(logoutUrl); @@ -153,7 +158,7 @@ public class SAMLUtils { logoutRequest.setIssueInstant(new DateTime()); logoutRequest.setIssuer(issuer); logoutRequest.getSessionIndexes().add(sessionIndexElement); - logoutRequest.setNameID(nameId); + logoutRequest.setNameID(nameID); return logoutRequest; } From 15fdc1744c42c0e70b3cde31ca4b163c7983bec2 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 02:41:26 +0200 Subject: [PATCH 42/49] SAML2LogoutAPIAuthenticatorCmd: check logout response and redirect to UI Signed-off-by: Rohit Yadav --- .../SAML2LogoutAPIAuthenticatorCmd.java | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 99100741680..1c96f0bebc0 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command; import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.configuration.Config; import com.cloud.user.Account; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiErrorCode; @@ -27,18 +28,24 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LogoutCmdResponse; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.utils.auth.SAMLUtils; import org.apache.log4j.Logger; import org.opensaml.DefaultBootstrap; import org.opensaml.saml2.core.LogoutRequest; import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.StatusCode; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.io.UnmarshallingException; +import org.xml.sax.SAXException; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.FactoryConfigurationError; import java.io.IOException; import java.util.List; @@ -51,6 +58,8 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen @Inject ApiServerService _apiServer; + @Inject + ConfigurationDao _configDao; SAML2AuthManager _samlAuthManager; ///////////////////////////////////////////////////// @@ -79,6 +88,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen LogoutCmdResponse response = new LogoutCmdResponse(); response.setDescription("success"); response.setResponseName(getCommandName()); + String responseString = ApiResponseSerializer.toSerializedString(response, responseType); try { DefaultBootstrap.bootstrap(); @@ -89,8 +99,35 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen params, responseType)); } + if (params.containsKey("SAMLResponse")) { + try { + final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0]; + Response processedSAMLResponse = SAMLUtils.decodeSAMLResponse(samlResponse); + String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); + if (!statusCode.equals(StatusCode.SUCCESS_URI)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.INTERNAL_ERROR.getHttpCode(), + "SAML SLO LogoutResponse status is not Success", + params, responseType)); + } + } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { + s_logger.error("SAMLResponse processing error: " + e.getMessage()); + } + try { + resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); + } catch (IOException ignored) { + } + return responseString; + } + NameID nameId = (NameID) session.getAttribute(SAMLUtils.SAML_NAMEID); String sessionIndex = (String) session.getAttribute(SAMLUtils.SAML_SESSION); + if (nameId == null || sessionIndex == null) { + try { + resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); + } catch (IOException ignored) { + } + return responseString; + } LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex); try { @@ -102,8 +139,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen "SAML Single Logout Error", params, responseType)); } - - return ApiResponseSerializer.toSerializedString(response, responseType); + return responseString; } @Override From 1ed532fb2011b2a6f203cfa000df5466d7924f25 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 17:31:01 +0200 Subject: [PATCH 43/49] SAMLUtils: add unit test for SAMLUtils and method to randomly generate X509 certs Signed-off-by: Rohit Yadav --- .../cloudstack/utils/auth/SAMLUtils.java | 37 +++++++++- .../cloudstack/utils/auth/SAMLUtilsTest.java | 67 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index 51cf5074bd1..a562d4840ae 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -21,6 +21,8 @@ package org.apache.cloudstack.utils.auth; import com.cloud.utils.HttpUtils; import org.apache.log4j.Logger; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.x509.X509V1CertificateGenerator; import org.joda.time.DateTime; import org.opensaml.Configuration; import org.opensaml.common.SAMLVersion; @@ -57,6 +59,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; +import javax.security.auth.x500.X500Principal; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -66,7 +69,17 @@ import java.io.IOException; import java.io.StringWriter; import java.math.BigInteger; import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Date; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -88,7 +101,7 @@ public class SAMLUtils { } public static String generateSecureRandomId() { - return new BigInteger(130, new SecureRandom()).toString(32); + return new BigInteger(160, new SecureRandom()).toString(32); } public static AuthnRequest buildAuthnRequestObject(String spId, String idpUrl, String consumerUrl) { @@ -194,4 +207,26 @@ public class SAMLUtils { return (Response) unmarshaller.unmarshall(element); } + public static X509Certificate generateRandomX509Certification() throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { + Date validityBeginDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); + Date validityEndDate = new Date(System.currentTimeMillis() + 2 * 365 * 24 * 60 * 60 * 1000); + + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(1024, new SecureRandom()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + X500Principal dnName = new X500Principal("CN=John Doe"); + X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setSubjectDN(dnName); + certGen.setIssuerDN(dnName); // use the same + certGen.setNotBefore(validityBeginDate); + certGen.setNotAfter(validityEndDate); + certGen.setPublicKey(keyPair.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + + return certGen.generate(keyPair.getPrivate(), "BC"); + } + } diff --git a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java b/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java new file mode 100644 index 00000000000..1d34ba1b92b --- /dev/null +++ b/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java @@ -0,0 +1,67 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.auth; + +import junit.framework.TestCase; +import org.junit.Test; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.LogoutRequest; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.impl.NameIDBuilder; + +public class SAMLUtilsTest extends TestCase { + + @Test + public void testSAMLId() throws Exception { + assertTrue(SAMLUtils.checkSAMLUserId(SAMLUtils.createSAMLId("someUID"))); + assertFalse(SAMLUtils.checkSAMLUserId("randomUID")); + } + + @Test + public void testGenerateSecureRandomId() throws Exception { + assertTrue(SAMLUtils.generateSecureRandomId().length() == 32); + } + + @Test + public void testBuildAuthnRequestObject() throws Exception { + String consumerUrl = "http://someurl.com"; + String idpUrl = "http://idp.domain.example"; + String spId = "cloudstack"; + AuthnRequest req = SAMLUtils.buildAuthnRequestObject(spId, idpUrl, consumerUrl); + assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl); + assertEquals(req.getDestination(), idpUrl); + assertEquals(req.getIssuer().getValue(), spId); + } + + @Test + public void testBuildLogoutRequest() throws Exception { + String logoutUrl = "http://logoutUrl"; + String spId = "cloudstack"; + String sessionIndex = "12345"; + String nameIdString = "someNameID"; + NameID sessionNameId = new NameIDBuilder().buildObject(); + sessionNameId.setValue(nameIdString); + LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, sessionNameId, sessionIndex); + assertEquals(req.getDestination(), logoutUrl); + assertEquals(req.getIssuer().getValue(), spId); + assertEquals(req.getNameID().getValue(), nameIdString); + assertEquals(req.getSessionIndexes().get(0).getSessionIndex(), sessionIndex); + } +} \ No newline at end of file From de4e74b2b462773cb2866aa976e349e3f7151e9d Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 25 Aug 2014 17:32:13 +0200 Subject: [PATCH 44/49] saml: Add unit tests for saml plugin - Fixes signatures on plugin manager for ease of testing - Fixes authenticator - Adds unit testing for getType and authenticate methods for all cmd classes - Adds SAMLAuthenticator test Signed-off-by: Rohit Yadav --- .../SAML2LoginAPIAuthenticatorCmd.java | 12 +- .../SAML2LogoutAPIAuthenticatorCmd.java | 2 +- .../saml/SAML2UserAuthenticator.java | 6 +- .../SAML2UserAuthenticatorTest.java | 51 ++++- .../GetServiceProviderMetaDataCmdTest.java | 94 ++++++++++ .../SAML2LoginAPIAuthenticatorCmdTest.java | 175 ++++++++++++++++++ .../SAML2LogoutAPIAuthenticatorCmdTest.java | 93 ++++++++++ 7 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java create mode 100644 plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java create mode 100644 plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java 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 6c46b044a85..07cfa394e7b 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 @@ -89,7 +89,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent @Inject ConfigurationDao _configDao; @Inject - private DomainManager _domainMgr; + DomainManager _domainMgr; SAML2AuthManager _samlAuthManager; @@ -141,7 +141,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent return redirectUrl; } - private Response processSAMLResponse(String responseMessage) { + public Response processSAMLResponse(String responseMessage) { Response responseObject = null; try { DefaultBootstrap.bootstrap(); @@ -162,12 +162,12 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent if (idps != null && idps.length > 0) { idpUrl = idps[0]; } - String redirectUrl = buildAuthnRequestUrl(idpUrl); + String redirectUrl = this.buildAuthnRequestUrl(idpUrl); resp.sendRedirect(redirectUrl); return ""; } else { final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0]; - Response processedSAMLResponse = processSAMLResponse(samlResponse); + Response processedSAMLResponse = this.processSAMLResponse(samlResponse); String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); if (!statusCode.equals(StatusCode.SUCCESS_URI)) { throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), @@ -209,7 +209,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent } String username = null; - String password = ""; + String password = SAMLUtils.generateSecureRandomId(); // Random password String firstName = ""; String lastName = ""; String timeZone = ""; @@ -229,8 +229,6 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent } } - String issuer = assertion.getIssuer().getValue(); - String audience = assertion.getConditions().getAudienceRestrictions().get(0).getAudiences().get(0).getAudienceURI(); AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0); List attributes = attributeStatement.getAttributes(); diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 1c96f0bebc0..5b94766f95a 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -99,7 +99,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen params, responseType)); } - if (params.containsKey("SAMLResponse")) { + if (params != null && params.containsKey("SAMLResponse")) { try { final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0]; Response processedSAMLResponse = SAMLUtils.decodeSAMLResponse(samlResponse); diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java index 5cd9b524a95..e623fc21798 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java @@ -49,7 +49,7 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator { } else { User user = _userDao.getUser(userAccount.getId()); if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid()) && - requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) { + requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) { return new Pair(true, null); } } @@ -59,8 +59,6 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator { @Override public String encode(final String password) { - // TODO: Complete method - StringBuilder sb = new StringBuilder(32); - return sb.toString(); + return SAMLUtils.generateSecureRandomId(); } } diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java index 8298c6c13dd..6f5150b5c52 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java @@ -19,21 +19,68 @@ package org.apache.cloudstack; +import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication; +import com.cloud.user.UserAccountVO; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.Pair; +import org.apache.cloudstack.saml.SAML2UserAuthenticator; +import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; - +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + @RunWith(MockitoJUnitRunner.class) public class SAML2UserAuthenticatorTest { + @Mock + UserAccountDao userAccountDao; + @Mock + UserDao userDao; + @Test public void encode() { - + Assert.assertTrue(new SAML2UserAuthenticator().encode("random String").length() == 32); } @Test public void authenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + SAML2UserAuthenticator authenticator = new SAML2UserAuthenticator(); + Field daoField = SAML2UserAuthenticator.class.getDeclaredField("_userAccountDao"); + daoField.setAccessible(true); + daoField.set(authenticator, userAccountDao); + + Field userDaoField = SAML2UserAuthenticator.class.getDeclaredField("_userDao"); + userDaoField.setAccessible(true); + userDaoField.set(authenticator, userDao); + + UserAccountVO account = new UserAccountVO(); + account.setPassword("5f4dcc3b5aa765d61d8327deb882cf99"); + account.setId(1L); + + UserVO user = new UserVO(); + user.setUuid(SAMLUtils.createSAMLId("someUID")); + + Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account); + Mockito.when(userDao.getUser(Mockito.anyLong())).thenReturn(user); + + // When there is no SAMLRequest in params + Pair pair1 = authenticator.authenticate(SAMLUtils.createSAMLId("user1234"), "random", 1l, null); + Assert.assertFalse(pair1.first()); + + // When there is SAMLRequest in params + Map params = new HashMap(); + params.put(SAMLUtils.SAML_RESPONSE, new Object[]{}); + Pair pair2 = authenticator.authenticate(SAMLUtils.createSAMLId("user1234"), "random", 1l, params); + Assert.assertTrue(pair2.first()); } } diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java new file mode 100644 index 00000000000..0f956ae10e2 --- /dev/null +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command; + +import com.cloud.utils.HttpUtils; +import org.apache.cloudstack.api.ApiServerService; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.lang.reflect.Field; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +@RunWith(MockitoJUnitRunner.class) +public class GetServiceProviderMetaDataCmdTest { + + @Mock + ApiServerService apiServer; + + @Mock + SAML2AuthManager samlAuthManager; + + @Mock + HttpSession session; + + @Mock + HttpServletResponse resp; + + @Test + public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd(); + + Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer"); + apiServerField.setAccessible(true); + apiServerField.set(cmd, apiServer); + + Field managerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_samlAuthManager"); + managerField.setAccessible(true); + managerField.set(cmd, samlAuthManager); + + String spId = "someSPID"; + String url = "someUrl"; + X509Certificate cert = SAMLUtils.generateRandomX509Certification(); + Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); + Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); + Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); + Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url); + + String result = cmd.authenticate("command", null, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp); + Assert.assertTrue(result.contains("md:EntityDescriptor")); + + Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getServiceProviderId(); + Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getSpSingleSignOnUrl(); + Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getSpSingleLogOutUrl(); + Mockito.verify(samlAuthManager, Mockito.never()).getIdpSingleSignOnUrl(); + Mockito.verify(samlAuthManager, Mockito.never()).getIdpSingleLogOutUrl(); + } + + @Test + public void testGetAPIType() { + Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..7747065f783 --- /dev/null +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java @@ -0,0 +1,175 @@ +package org.apache.cloudstack.api.command; + +import com.cloud.domain.Domain; +import com.cloud.user.AccountService; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.utils.HttpUtils; +import com.cloud.utils.db.EntityManager; +import org.apache.cloudstack.api.ApiServerService; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.joda.time.DateTime; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.opensaml.common.SAMLVersion; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.AttributeStatement; +import org.opensaml.saml2.core.AuthnStatement; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.Status; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.Subject; +import org.opensaml.saml2.core.impl.AssertionBuilder; +import org.opensaml.saml2.core.impl.AttributeStatementBuilder; +import org.opensaml.saml2.core.impl.AuthnStatementBuilder; +import org.opensaml.saml2.core.impl.NameIDBuilder; +import org.opensaml.saml2.core.impl.ResponseBuilder; +import org.opensaml.saml2.core.impl.StatusBuilder; +import org.opensaml.saml2.core.impl.StatusCodeBuilder; +import org.opensaml.saml2.core.impl.SubjectBuilder; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.lang.reflect.Field; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class SAML2LoginAPIAuthenticatorCmdTest { + + @Mock + ApiServerService apiServer; + + @Mock + SAML2AuthManager samlAuthManager; + + @Mock + ConfigurationDao configDao; + + @Mock + EntityManager entityMgr; + + @Mock + DomainManager domainMgr; + + @Mock + AccountService accountService; + + @Mock + Domain domain; + + @Mock + HttpSession session; + + @Mock + HttpServletResponse resp; + + private Response buildMockResponse() throws Exception { + Response samlMessage = new ResponseBuilder().buildObject(); + samlMessage.setID("foo"); + samlMessage.setVersion(SAMLVersion.VERSION_20); + samlMessage.setIssueInstant(new DateTime(0)); + Status status = new StatusBuilder().buildObject(); + StatusCode statusCode = new StatusCodeBuilder().buildObject(); + statusCode.setValue(StatusCode.SUCCESS_URI); + status.setStatusCode(statusCode); + samlMessage.setStatus(status); + Assertion assertion = new AssertionBuilder().buildObject(); + Subject subject = new SubjectBuilder().buildObject(); + NameID nameID = new NameIDBuilder().buildObject(); + nameID.setValue("SOME-UNIQUE-ID"); + nameID.setFormat(NameIDType.PERSISTENT); + subject.setNameID(nameID); + assertion.setSubject(subject); + AuthnStatement authnStatement = new AuthnStatementBuilder().buildObject(); + authnStatement.setSessionIndex("Some Session String"); + assertion.getAuthnStatements().add(authnStatement); + AttributeStatement attributeStatement = new AttributeStatementBuilder().buildObject(); + assertion.getAttributeStatements().add(attributeStatement); + samlMessage.getAssertions().add(assertion); + return samlMessage; + } + + @Test + public void testAuthenticate() throws Exception { + SAML2LoginAPIAuthenticatorCmd cmd = Mockito.spy(new SAML2LoginAPIAuthenticatorCmd()); + + Field apiServerField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_apiServer"); + apiServerField.setAccessible(true); + apiServerField.set(cmd, apiServer); + + Field managerField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager"); + managerField.setAccessible(true); + managerField.set(cmd, samlAuthManager); + + Field accountServiceField = BaseCmd.class.getDeclaredField("_accountService"); + accountServiceField.setAccessible(true); + accountServiceField.set(cmd, accountService); + + Field entityMgrField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_entityMgr"); + entityMgrField.setAccessible(true); + entityMgrField.set(cmd, entityMgr); + + Field domainMgrField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_domainMgr"); + domainMgrField.setAccessible(true); + domainMgrField.set(cmd, domainMgr); + + Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao"); + configDaoField.setAccessible(true); + configDaoField.set(cmd, configDao); + + String spId = "someSPID"; + String url = "someUrl"; + X509Certificate cert = SAMLUtils.generateRandomX509Certification(); + Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); + Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null); + Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url); + Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url); + + Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); + Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString"); + + Mockito.when(domain.getId()).thenReturn(1L); + Mockito.when(domainMgr.getDomain(Mockito.anyString())).thenReturn(domain); + UserVO user = new UserVO(); + user.setUuid(SAMLUtils.createSAMLId("someUID")); + Mockito.when(entityMgr.findByUuid(Mockito.eq(User.class), Mockito.anyString())).thenReturn((User) user); + Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false); + + Map params = new HashMap(); + + // SSO redirection test + cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp); + Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString()); + + // SSO SAMLResponse verification test, this should throw ServerApiException for auth failure + params.put(SAMLUtils.SAML_RESPONSE, new String[]{"Some String"}); + Mockito.stub(cmd.processSAMLResponse(Mockito.anyString())).toReturn(buildMockResponse()); + try { + cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp); + } catch (ServerApiException ignored) { + } + Mockito.verify(configDao, Mockito.atLeastOnce()).getValue(Mockito.anyString()); + Mockito.verify(domainMgr, Mockito.times(1)).getDomain(Mockito.anyString()); + Mockito.verify(entityMgr, Mockito.times(1)).findByUuid(Mockito.eq(User.class), Mockito.anyString()); + Mockito.verify(apiServer, Mockito.times(1)).verifyUser(Mockito.anyLong()); + } + + @Test + public void testGetAPIType() { + Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java new file mode 100644 index 00000000000..d309abe3ae7 --- /dev/null +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command; + +import com.cloud.utils.HttpUtils; +import org.apache.cloudstack.api.ApiServerService; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.lang.reflect.Field; +import java.security.cert.X509Certificate; + +@RunWith(MockitoJUnitRunner.class) +public class SAML2LogoutAPIAuthenticatorCmdTest { + + @Mock + ApiServerService apiServer; + + @Mock + SAML2AuthManager samlAuthManager; + + @Mock + ConfigurationDao configDao; + + @Mock + HttpSession session; + + @Mock + HttpServletResponse resp; + + @Test + public void testAuthenticate() throws Exception { + SAML2LogoutAPIAuthenticatorCmd cmd = new SAML2LogoutAPIAuthenticatorCmd(); + + Field apiServerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_apiServer"); + apiServerField.setAccessible(true); + apiServerField.set(cmd, apiServer); + + Field managerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager"); + managerField.setAccessible(true); + managerField.set(cmd, samlAuthManager); + + Field configDaoField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_configDao"); + configDaoField.setAccessible(true); + configDaoField.set(cmd, configDao); + + String spId = "someSPID"; + String url = "someUrl"; + X509Certificate cert = SAMLUtils.generateRandomX509Certification(); + Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); + Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); + Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); + Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url); + Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); + Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString"); + + cmd.authenticate("command", null, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp); + Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString()); + Mockito.verify(session, Mockito.atLeastOnce()).getAttribute(Mockito.anyString()); + } + + @Test + public void testGetAPIType() throws Exception { + Assert.assertTrue(new SAML2LogoutAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGOUT_API); + } +} \ No newline at end of file From 0402f68b127df1ae7bdb0b299e462711db8d8030 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Tue, 26 Aug 2014 23:06:17 +0200 Subject: [PATCH 45/49] SAML2LogoutAPIAuthenticatorCmd: if session is null, redirect to login page If session is null, probably logout (local) happened removing the name id and session index which is needed for global logout. The limitation by design is that local logout will void possibility of global logout. To globally logout, one use the SLO api which would logout locally as well. Signed-off-by: Rohit Yadav --- .../api/command/SAML2LogoutAPIAuthenticatorCmd.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 5b94766f95a..4fa7fb31b6f 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -90,6 +90,14 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen response.setResponseName(getCommandName()); String responseString = ApiResponseSerializer.toSerializedString(response, responseType); + if (session == null) { + try { + resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); + } catch (IOException ignored) { + } + return responseString; + } + try { DefaultBootstrap.bootstrap(); } catch (ConfigurationException | FactoryConfigurationError e) { From 5e7928bcb94be56fa3b9da68bc963d09bcace815 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 28 Aug 2014 18:39:28 +0200 Subject: [PATCH 46/49] utils: fix static certificate value string in SAMLUtils Signed-off-by: Rohit Yadav --- .../org/apache/cloudstack/utils/auth/SAMLUtils.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java index a562d4840ae..1f31dcafd8f 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java @@ -90,7 +90,7 @@ public class SAMLUtils { public static final String SAML_NS = "saml://"; public static final String SAML_NAMEID = "SAML_NAMEID"; public static final String SAML_SESSION = "SAML_SESSION"; - public static final String CERTIFICATE_NAME = "SAMLSP_CERTIFICATE"; + public static final String CERTIFICATE_NAME = "SAMLSP_X509CERTIFICATE"; public static String createSAMLId(String uid) { return SAML_NS + uid; @@ -207,20 +207,20 @@ public class SAMLUtils { return (Response) unmarshaller.unmarshall(element); } - public static X509Certificate generateRandomX509Certification() throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { + public static X509Certificate generateRandomX509Certificate() throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { Date validityBeginDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); - Date validityEndDate = new Date(System.currentTimeMillis() + 2 * 365 * 24 * 60 * 60 * 1000); + Date validityEndDate = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000); Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); keyPairGenerator.initialize(1024, new SecureRandom()); KeyPair keyPair = keyPairGenerator.generateKeyPair(); - X500Principal dnName = new X500Principal("CN=John Doe"); + X500Principal dnName = new X500Principal("CN=Apache CloudStack"); X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); certGen.setSubjectDN(dnName); - certGen.setIssuerDN(dnName); // use the same + certGen.setIssuerDN(dnName); certGen.setNotBefore(validityBeginDate); certGen.setNotAfter(validityEndDate); certGen.setPublicKey(keyPair.getPublic()); From 249446dc521a273fe14b3e9e49b397a363ef577d Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 28 Aug 2014 18:40:05 +0200 Subject: [PATCH 47/49] server: add config to enable/disable SAML SSO/SLO plugin Signed-off-by: Rohit Yadav --- server/src/com/cloud/configuration/Config.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index 530303ac595..2c78f1bea35 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1379,6 +1379,14 @@ public enum Config { "300000", "The allowable clock difference in milliseconds between when an SSO login request is made and when it is received.", null), + SAMLIsPluginEnabled( + "Advanced", + ManagementServer.class, + Boolean.class, + "saml2.enabled", + "true", + "Set it to true to enable SAML SSO plugin", + null), SAMLUserAccountName( "Advanced", ManagementServer.class, From aa02e30e9502d0bbb175a5367bce0282b035d5b6 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 28 Aug 2014 18:40:51 +0200 Subject: [PATCH 48/49] saml: fix tests and update method signature that generates random certs Signed-off-by: Rohit Yadav --- .../api/command/GetServiceProviderMetaDataCmdTest.java | 2 +- .../api/command/SAML2LoginAPIAuthenticatorCmdTest.java | 2 +- .../api/command/SAML2LogoutAPIAuthenticatorCmdTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java index 0f956ae10e2..fbd381d8ac8 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java @@ -71,7 +71,7 @@ public class GetServiceProviderMetaDataCmdTest { String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certification(); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(); Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); 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 7747065f783..5769a8fd0ec 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 @@ -133,7 +133,7 @@ public class SAML2LoginAPIAuthenticatorCmdTest { String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certification(); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(); Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null); Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java index d309abe3ae7..820132b9a20 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java @@ -73,7 +73,7 @@ public class SAML2LogoutAPIAuthenticatorCmdTest { String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certification(); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(); Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); From 6eae9b859692417182103d06f5215fff11289942 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 28 Aug 2014 18:47:08 +0200 Subject: [PATCH 49/49] saml: disable plugin by default and don't initiate if not enabled Signed-off-by: Rohit Yadav --- .../cloudstack/saml/SAML2AuthManagerImpl.java | 17 +++++++++++++---- server/src/com/cloud/configuration/Config.java | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index 22d99cbbde5..8480c0e57c0 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -69,12 +69,14 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage @Inject ConfigurationDao _configDao; - protected SAML2AuthManagerImpl() { - super(); - } - @Override public boolean start() { + return isSAMLPluginEnabled() && setup(); + } + + private boolean setup() { + // TODO: In future if need added logic to get SP X509 cert for Idps that need signed requests + this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key()); this.identityProviderId = _configDao.getValue(Config.SAMLIdentityProviderID.key()); @@ -145,6 +147,9 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage @Override public List> getAuthCommands() { + if (!isSAMLPluginEnabled()) { + return null; + } List> cmdList = new ArrayList>(); cmdList.add(SAML2LoginAPIAuthenticatorCmd.class); cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class); @@ -183,4 +188,8 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage public X509Certificate getIdpEncryptionKey() { return idpEncryptionKey; } + + public Boolean isSAMLPluginEnabled() { + return Boolean.valueOf(_configDao.getValue(Config.SAMLIsPluginEnabled.key())); + } } diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index 2c78f1bea35..85277386f82 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1384,7 +1384,7 @@ public enum Config { ManagementServer.class, Boolean.class, "saml2.enabled", - "true", + "false", "Set it to true to enable SAML SSO plugin", null), SAMLUserAccountName(