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/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/ApiServerService.java b/api/src/org/apache/cloudstack/api/ApiServerService.java
similarity index 84%
rename from server/src/com/cloud/api/ApiServerService.java
rename to api/src/org/apache/cloudstack/api/ApiServerService.java
index 5d078c31605..69215c51658 100644
--- a/server/src/com/cloud/api/ApiServerService.java
+++ b/api/src/org/apache/cloudstack/api/ApiServerService.java
@@ -14,23 +14,19 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.api;
-
-import java.util.Map;
-
-import javax.servlet.http.HttpSession;
-
-import org.apache.cloudstack.api.ServerApiException;
+package org.apache.cloudstack.api;
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/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 92%
rename from server/src/com/cloud/api/auth/APIAuthenticator.java
rename to api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java
index 90cd7ec7a8a..67fa1d8816e 100644
--- a/server/src/com/cloud/api/auth/APIAuthenticator.java
+++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java
@@ -14,12 +14,13 @@
// 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;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import java.util.List;
import java.util.Map;
/*
@@ -36,6 +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/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
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
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" />
+
+
+
+
+
-
+
+
+
+
+
+
+
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..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
@@ -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/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/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
new file mode 100644
index 00000000000..07cfa394e7b
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
@@ -0,0 +1,303 @@
+// 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.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;
+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;
+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.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.Assertion;
+import org.opensaml.saml2.core.Attribute;
+import org.opensaml.saml2.core.AttributeStatement;
+import org.opensaml.saml2.core.AuthnRequest;
+import org.opensaml.saml2.core.NameID;
+import org.opensaml.saml2.core.NameIDType;
+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.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;
+import javax.servlet.http.Cookie;
+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.net.URLEncoder;
+import java.util.List;
+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;
+
+ @Inject
+ ApiServerService _apiServer;
+ @Inject
+ EntityManager _entityMgr;
+ @Inject
+ ConfigurationDao _configDao;
+ @Inject
+ DomainManager _domainMgr;
+
+ SAML2AuthManager _samlAuthManager;
+
+ /////////////////////////////////////////////////////
+ /////////////////// 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");
+ }
+
+ private String buildAuthnRequestUrl(String idpUrl) {
+ 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(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;
+ }
+
+ public Response processSAMLResponse(String responseMessage) {
+ Response responseObject = null;
+ try {
+ DefaultBootstrap.bootstrap();
+ responseObject = SAMLUtils.decodeSAMLResponse(responseMessage);
+
+ } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
+ s_logger.error("SAMLResponse processing error: " + e.getMessage());
+ }
+ return responseObject;
+ }
+
+ @Override
+ 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")) {
+ String idpUrl = null;
+ final String[] idps = (String[])params.get(ApiConstants.IDP_URL);
+ if (idps != null && idps.length > 0) {
+ idpUrl = idps[0];
+ }
+ String redirectUrl = this.buildAuthnRequestUrl(idpUrl);
+ resp.sendRedirect(redirectUrl);
+ return "";
+ } else {
+ final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
+ 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(),
+ "Identity Provider send a non-successful authentication status code",
+ params, responseType));
+ }
+
+ 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 = _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 = SAMLUtils.generateSecureRandomId(); // Random password
+ String firstName = "";
+ String lastName = "";
+ String timeZone = "";
+ String email = "";
+
+ 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();
+ uniqueUserId = SAMLUtils.createSAMLId(username);
+ if (nameId.getFormat().equals(NameIDType.EMAIL)) {
+ email = username;
+ }
+ }
+
+ 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 = SAMLUtils.createSAMLId(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.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8)));
+ resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName()));
+ resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
+ 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());
+ }
+ 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
+ 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/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
new file mode 100644
index 00000000000..4fa7fb31b6f
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
@@ -0,0 +1,169 @@
+// 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.configuration.Config;
+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.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;
+import java.util.Map;
+
+@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";
+
+ @Inject
+ ApiServerService _apiServer;
+ @Inject
+ ConfigurationDao _configDao;
+ 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 {
+ // 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("=== SAML SLO Logging out ===");
+ LogoutCmdResponse response = new LogoutCmdResponse();
+ response.setDescription("success");
+ 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) {
+ 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));
+ }
+
+ if (params != null && 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 {
+ 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 responseString;
+ }
+
+ @Override
+ public APIAuthenticationType getAPIType() {
+ return APIAuthenticationType.LOGOUT_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/SAML2AuthManager.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
new file mode 100644
index 00000000000..507fa04409c
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
@@ -0,0 +1,36 @@
+// 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 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
new file mode 100644
index 00000000000..8480c0e57c0
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
@@ -0,0 +1,195 @@
+// 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.GetServiceProviderMetaDataCmd;
+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 = {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 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;
+
+ @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());
+
+ 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 {
+ DefaultBootstrap.bootstrap();
+ idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance);
+ idpMetaDataProvider.setRequireValidMetadata(true);
+ idpMetaDataProvider.setParserPool(new BasicParserPool());
+ idpMetaDataProvider.initialize();
+
+ 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("SAML based authentication won't work");
+ }
+
+ return true;
+ }
+
+ @Override
+ public List> getAuthCommands() {
+ if (!isSAMLPluginEnabled()) {
+ return null;
+ }
+ List> cmdList = new ArrayList>();
+ cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
+ cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
+ cmdList.add(GetServiceProviderMetaDataCmd.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;
+ }
+
+ public String getIdentityProviderId() {
+ return identityProviderId;
+ }
+
+ public X509Certificate getIdpSigningKey() {
+ return idpSigningKey;
+ }
+
+ public X509Certificate getIdpEncryptionKey() {
+ return idpEncryptionKey;
+ }
+
+ public Boolean isSAMLPluginEnabled() {
+ return Boolean.valueOf(_configDao.getValue(Config.SAMLIsPluginEnabled.key()));
+ }
+}
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 88%
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..e623fc21798 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;
@@ -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;
@@ -47,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 && user.getUuid().startsWith("saml")) {
+ if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid()) &&
+ requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) {
return new Pair(true, null);
}
}
@@ -58,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..fbd381d8ac8
--- /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.generateRandomX509Certificate();
+ 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..5769a8fd0ec
--- /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.generateRandomX509Certificate();
+ 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..820132b9a20
--- /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.generateRandomX509Certificate();
+ 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
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
diff --git a/server/pom.xml b/server/pom.xml
index 1b21ebde579..0e517f7854e 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -133,6 +133,11 @@
cloud-engine-components-api
${project.version}
+
+ org.opensaml
+ opensaml
+ ${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"
>
-
+
+
+
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 +1050,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/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java
index 8dff6ebc952..454fc8b1ddd 100644
--- a/server/src/com/cloud/api/ApiServlet.java
+++ b/server/src/com/cloud/api/ApiServlet.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.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
@@ -26,6 +26,7 @@ import com.cloud.utils.HttpUtils;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.EntityManager;
import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;
diff --git a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java
index 886d277f15b..24ccbeb6032 100644
--- a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java
+++ b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java
@@ -19,6 +19,9 @@ package com.cloud.api.auth;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.auth.APIAuthenticationManager;
+import org.apache.cloudstack.api.auth.APIAuthenticator;
+import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.log4j.Logger;
import javax.ejb.Local;
@@ -32,12 +35,21 @@ import java.util.Map;
public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager {
public static final Logger s_logger = Logger.getLogger(APIAuthenticationManagerImpl.class.getName());
+ private List _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,12 +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);
+ 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
@@ -68,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 f5d633e025f..fa23abd43e4 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;
@@ -25,15 +25,17 @@ 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.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.Enumeration;
+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 = {})
@@ -100,54 +102,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 +151,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 {
@@ -222,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 a5802bfcbc6..ee7936ad734 100644
--- a/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java
+++ b/server/src/com/cloud/api/auth/DefaultLogoutAPIAuthenticatorCmd.java
@@ -22,11 +22,15 @@ 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.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 = {})
@@ -68,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 b499df58bd4..85277386f82 100755
--- a/server/src/com/cloud/configuration/Config.java
+++ b/server/src/com/cloud/configuration/Config.java
@@ -1379,6 +1379,86 @@ 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",
+ "false",
+ "Set it to true to enable SAML SSO plugin",
+ 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,
+ String.class,
+ "saml2.sp.id",
+ "org.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),
+ SAMLIdentityProviderID(
+ "Advanced",
+ ManagementServer.class,
+ String.class,
+ "saml2.idp.id",
+ "https://openidp.feide.no",
+ "SAML2 Identity Provider Identifier String",
+ 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),
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;
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',
diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js
index b6dd5593345..38cf501f1aa 100644
--- a/ui/scripts/cloudStack.js
+++ b/ui/scripts/cloudStack.js
@@ -129,17 +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');
- 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)
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..1f31dcafd8f
--- /dev/null
+++ b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java
@@ -0,0 +1,232 @@
+//
+// 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 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;
+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.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.NameIDBuilder;
+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;
+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.security.auth.x500.X500Principal;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+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.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;
+
+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_X509CERTIFICATE";
+
+ public static String createSAMLId(String uid) {
+ return SAML_NS + uid;
+ }
+
+ public static Boolean checkSAMLUserId(String uuid) {
+ return uuid.startsWith(SAML_NS);
+ }
+
+ public static String generateSecureRandomId() {
+ return new BigInteger(160, 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();
+ issuer.setValue(spId);
+
+ // NameIDPolicy
+ NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
+ NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
+ nameIdPolicy.setFormat(NameIDType.PERSISTENT);
+ nameIdPolicy.setSPNameQualifier(spId);
+ 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(false);
+ authnRequest.setIsPassive(false);
+ authnRequest.setIssuer(issuer);
+ authnRequest.setIssueInstant(new DateTime());
+ authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
+ authnRequest.setAssertionConsumerServiceURL(consumerUrl);
+ //authnRequest.setProviderName(spId);
+ //authnRequest.setNameIDPolicy(nameIdPolicy);
+ //authnRequest.setRequestedAuthnContext(requestedAuthnContext);
+
+ return authnRequest;
+ }
+
+ public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID sessionNameId, String sessionIndex) {
+ IssuerBuilder issuerBuilder = new IssuerBuilder();
+ Issuer issuer = issuerBuilder.buildObject();
+ issuer.setValue(spId);
+
+ 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);
+ 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);
+ 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, HttpUtils.UTF_8).trim();
+ return encodedRequestMessage;
+ }
+
+ public static Response decodeSAMLResponse(String responseMessage)
+ throws ConfigurationException, ParserConfigurationException,
+ SAXException, IOException, UnmarshallingException {
+ 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);
+ }
+
+ 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() + 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=Apache CloudStack");
+ X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+ certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+ certGen.setSubjectDN(dnName);
+ certGen.setIssuerDN(dnName);
+ 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