You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by bh...@apache.org on 2015/06/26 18:11:30 UTC
[3/4] git commit: updated refs/heads/saml-pp-squashed to 7310c2d
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
index 992e431..a012431 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
@@ -17,7 +17,6 @@
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;
@@ -28,13 +27,13 @@ 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.cloudstack.saml.SAMLPluginConstants;
+import org.apache.cloudstack.saml.SAMLProviderMetadata;
+import org.apache.cloudstack.saml.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;
@@ -59,8 +58,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
@Inject
ApiServerService _apiServer;
- @Inject
- ConfigurationDao _configDao;
+
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
@@ -93,7 +91,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
if (session == null) {
try {
- resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
+ resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
} catch (IOException ignored) {
}
return responseString;
@@ -110,7 +108,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
if (params != null && params.containsKey("SAMLResponse")) {
try {
- final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
+ final String samlResponse = ((String[])params.get(SAMLPluginConstants.SAML_RESPONSE))[0];
Response processedSAMLResponse = SAMLUtils.decodeSAMLResponse(samlResponse);
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
@@ -122,25 +120,26 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
s_logger.error("SAMLResponse processing error: " + e.getMessage());
}
try {
- resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
+ resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
} 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) {
+ String idpId = (String) session.getAttribute(SAMLPluginConstants.SAML_IDPID);
+ SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(idpId);
+ String nameId = (String) session.getAttribute(SAMLPluginConstants.SAML_NAMEID);
+ if (idpMetadata == null || nameId == null || nameId.isEmpty()) {
try {
- resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
+ resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
} catch (IOException ignored) {
}
return responseString;
}
- LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex);
+ LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(idpMetadata.getSloUrl(), _samlAuthManager.getSPMetadata().getEntityId(), nameId);
try {
- String redirectUrl = _samlAuthManager.getIdpSingleLogOutUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
+ String redirectUrl = idpMetadata.getSloUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
resp.sendRedirect(redirectUrl);
} catch (MarshallingException | IOException e) {
s_logger.error("SAML SLO error: " + e.getMessage());
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java
new file mode 100644
index 0000000..d95cc33
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java
@@ -0,0 +1,62 @@
+// 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;
+
+public class IdpResponse extends AuthenticationCmdResponse {
+ @SerializedName("id")
+ @Param(description = "The IdP Entity ID")
+ private String id;
+
+ @SerializedName("orgName")
+ @Param(description = "The IdP Organization Name")
+ private String orgName;
+
+ @SerializedName("orgUrl")
+ @Param(description = "The IdP Organization URL")
+ private String orgUrl;
+
+ public IdpResponse() {
+ super();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getOrgName() {
+ return orgName;
+ }
+
+ public void setOrgName(String orgName) {
+ this.orgName = orgName;
+ }
+
+ public String getOrgUrl() {
+ return orgUrl;
+ }
+
+ public void setOrgUrl(String orgUrl) {
+ this.orgUrl = orgUrl;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java
new file mode 100644
index 0000000..445ee88
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java
@@ -0,0 +1,68 @@
+// 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.cloud.user.User;
+import com.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+
+@EntityReference(value = User.class)
+public class SamlAuthorizationResponse extends BaseResponse {
+ @SerializedName("userid")
+ @Param(description = "the user ID")
+ private String userId;
+
+ @SerializedName("status")
+ @Param(description = "the SAML authorization status")
+ private Boolean status;
+
+ @SerializedName("idpid")
+ @Param(description = "the authorized Identity Provider ID")
+ private String idpId;
+
+ public SamlAuthorizationResponse(String userId, Boolean status, String idpId) {
+ this.userId = userId;
+ this.status = status;
+ this.idpId = idpId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public Boolean getStatus() {
+ return status;
+ }
+
+ public void setStatus(Boolean status) {
+ this.status = status;
+ }
+
+ public String getIdpId() {
+ return idpId;
+ }
+
+ public void setIdpId(String idpId) {
+ this.idpId = idpId;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
index 9c0d4b4..fc9a6db 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
@@ -17,23 +17,64 @@
package org.apache.cloudstack.saml;
+import com.cloud.utils.component.PluggableService;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
+import org.apache.cloudstack.framework.config.ConfigKey;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
+import java.util.Collection;
-public interface SAML2AuthManager extends PluggableAPIAuthenticator {
- public String getServiceProviderId();
- public String getIdentityProviderId();
+public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableService {
- public X509Certificate getIdpSigningKey();
- public X509Certificate getIdpEncryptionKey();
- public X509Certificate getSpX509Certificate();
- public KeyPair getSpKeyPair();
+ public static final ConfigKey<Boolean> SAMLIsPluginEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.enabled", "false",
+ "Indicates whether SAML SSO plugin is enabled or not", true);
- public String getSpSingleSignOnUrl();
- public String getIdpSingleSignOnUrl();
+ public static final ConfigKey<String> SAMLServiceProviderID = new ConfigKey<String>("Advanced", String.class, "saml2.sp.id", "org.apache.cloudstack",
+ "SAML2 Service Provider Identifier String", true);
- public String getSpSingleLogOutUrl();
- public String getIdpSingleLogOutUrl();
+ public static final ConfigKey<String> SAMLServiceProviderContactPersonName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers",
+ "SAML2 Service Provider Contact Person Name", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderContactEmail = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org",
+ "SAML2 Service Provider Contact Email Address", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderOrgName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack",
+ "SAML2 Service Provider Organization Name", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderOrgUrl = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org",
+ "SAML2 Service Provider Organization URL", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderSingleSignOnURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.sso.url", "http://localhost:8080/client/api?command=samlSso",
+ "SAML2 CloudStack Service Provider Single Sign On URL", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderSingleLogOutURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/",
+ "SAML2 CloudStack Service Provider Single Log Out URL", true);
+
+ public static final ConfigKey<String> SAMLCloudStackRedirectionUrl = new ConfigKey<String>("Advanced", String.class, "saml2.redirect.url", "http://localhost:8080/client",
+ "The CloudStack UI url the SSO should redirected to when successful", true);
+
+ public static final ConfigKey<String> SAMLUserAttributeName = new ConfigKey<String>("Advanced", String.class, "saml2.user.attribute", "uid",
+ "Attribute name to be looked for in SAML response that will contain the username", true);
+
+ public static final ConfigKey<String> SAMLIdentityProviderMetadataURL = new ConfigKey<String>("Advanced", String.class, "saml2.idp.metadata.url", "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php",
+ "SAML2 Identity Provider Metadata XML Url", true);
+
+ public static final ConfigKey<String> SAMLDefaultIdentityProviderId = new ConfigKey<String>("Advanced", String.class, "saml2.default.idpid", "https://openidp.feide.no",
+ "The default IdP entity ID to use only in case of multiple IdPs", true);
+
+ public static final ConfigKey<String> SAMLSignatureAlgorithm = new ConfigKey<String>("Advanced", String.class, "saml2.sigalg", "SHA1",
+ "The algorithm to use to when signing a SAML request. Default is SHA1, allowed algorithms: SHA1, SHA256, SHA384, SHA512", true);
+
+ public static final ConfigKey<Integer> SAMLTimeout = new ConfigKey<Integer>("Advanced", Integer.class, "saml2.timeout", "1800",
+ "SAML2 IDP Metadata refresh interval in seconds, minimum value is set to 300", true);
+
+ public SAMLProviderMetadata getSPMetadata();
+ public SAMLProviderMetadata getIdPMetadata(String entityId);
+ public Collection<SAMLProviderMetadata> getAllIdPMetadata();
+
+ public boolean isUserAuthorized(Long userId, String entityId);
+ public boolean authorizeUser(Long userId, String entityId, boolean enable);
+
+ public void saveToken(String authnId, String domain, String entity);
+ public SAMLTokenVO getToken(String authnId);
+ public void expireTokens();
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
index 36c9da5..185955c 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
@@ -16,28 +16,46 @@
// under the License.
package org.apache.cloudstack.saml;
-import com.cloud.configuration.Config;
+import com.cloud.domain.Domain;
+import com.cloud.user.DomainManager;
+import com.cloud.user.User;
+import com.cloud.user.UserVO;
+import com.cloud.user.dao.UserDao;
+import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.component.AdapterBase;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
+import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd;
import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
+import org.apache.cloudstack.api.command.ListIdpsCmd;
+import org.apache.cloudstack.api.command.ListSamlAuthorizationCmd;
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.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.security.keystore.KeystoreDao;
import org.apache.cloudstack.framework.security.keystore.KeystoreVO;
-import org.apache.cloudstack.utils.auth.SAMLUtils;
-import org.apache.log4j.Logger;
import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.log4j.Logger;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.metadata.ContactPerson;
+import org.opensaml.saml2.metadata.EmailAddress;
+import org.opensaml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.KeyDescriptor;
+import org.opensaml.saml2.metadata.OrganizationDisplayName;
+import org.opensaml.saml2.metadata.OrganizationName;
+import org.opensaml.saml2.metadata.OrganizationURL;
import org.opensaml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml2.metadata.SingleSignOnService;
+import org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider;
+import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.ConfigurationException;
+import org.opensaml.xml.XMLObject;
import org.opensaml.xml.parse.BasicParserPool;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.keyinfo.KeyInfoHelper;
@@ -48,6 +66,7 @@ import javax.inject.Inject;
import javax.xml.stream.FactoryConfigurationError;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
@@ -63,61 +82,87 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
@Component
@Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class})
-public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager {
+public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable {
private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class);
- private String serviceProviderId;
- private String identityProviderId;
+ private SAMLProviderMetadata _spMetadata = new SAMLProviderMetadata();
+ private Map<String, SAMLProviderMetadata> _idpMetadataMap = new HashMap<String, SAMLProviderMetadata>();
- private X509Certificate idpSigningKey;
- private X509Certificate idpEncryptionKey;
- private X509Certificate spX509Key;
- private KeyPair spKeyPair;
-
- private String spSingleSignOnUrl;
private String idpSingleSignOnUrl;
-
- private String spSingleLogOutUrl;
private String idpSingleLogOutUrl;
- private HTTPMetadataProvider idpMetaDataProvider;
+ private Timer _timer;
+ private int _refreshInterval = SAMLPluginConstants.SAML_REFRESH_INTERVAL;
+ private AbstractReloadingMetadataProvider _idpMetaDataProvider;
@Inject
- ConfigurationDao _configDao;
+ private KeystoreDao _ksDao;
@Inject
- private KeystoreDao _ksDao;
+ private SAMLTokenDao _samlTokenDao;
+
+ @Inject
+ private UserDao _userDao;
+
+ @Inject
+ DomainManager _domainMgr;
@Override
public boolean start() {
if (isSAMLPluginEnabled()) {
setup();
+ s_logger.info("SAML auth plugin loaded");
+ } else {
+ s_logger.info("SAML auth plugin not enabled so not loading");
}
return super.start();
}
- private boolean setup() {
- KeystoreVO keyStoreVO = _ksDao.findByName(SAMLUtils.SAMLSP_KEYPAIR);
+ @Override
+ public boolean stop() {
+ if (_timer != null) {
+ _timer.cancel();
+ }
+ return super.stop();
+ }
+
+ private boolean initSP() {
+ KeystoreVO keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR);
if (keyStoreVO == null) {
try {
KeyPair keyPair = SAMLUtils.generateRandomKeyPair();
- _ksDao.save(SAMLUtils.SAMLSP_KEYPAIR, SAMLUtils.savePrivateKey(keyPair.getPrivate()), SAMLUtils.savePublicKey(keyPair.getPublic()), "samlsp-keypair");
- keyStoreVO = _ksDao.findByName(SAMLUtils.SAMLSP_KEYPAIR);
+ _ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR, SAMLUtils.savePrivateKey(keyPair.getPrivate()), SAMLUtils.savePublicKey(keyPair.getPublic()), "samlsp-keypair");
+ keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR);
+ s_logger.info("No SAML keystore found, created and saved a new Service Provider keypair");
} catch (NoSuchProviderException | NoSuchAlgorithmException e) {
- s_logger.error("Unable to create and save SAML keypair");
+ s_logger.error("Unable to create and save SAML keypair: " + e.toString());
}
}
+ String spId = SAMLServiceProviderID.value();
+ String spSsoUrl = SAMLServiceProviderSingleSignOnURL.value();
+ String spSloUrl = SAMLServiceProviderSingleLogOutURL.value();
+ String spOrgName = SAMLServiceProviderOrgName.value();
+ String spOrgUrl = SAMLServiceProviderOrgUrl.value();
+ String spContactPersonName = SAMLServiceProviderContactPersonName.value();
+ String spContactPersonEmail = SAMLServiceProviderContactEmail.value();
+ KeyPair spKeyPair = null;
+ X509Certificate spX509Key = null;
if (keyStoreVO != null) {
PrivateKey privateKey = SAMLUtils.loadPrivateKey(keyStoreVO.getCertificate());
PublicKey publicKey = SAMLUtils.loadPublicKey(keyStoreVO.getKey());
if (privateKey != null && publicKey != null) {
spKeyPair = new KeyPair(publicKey, privateKey);
- KeystoreVO x509VO = _ksDao.findByName(SAMLUtils.SAMLSP_X509CERT);
+ KeystoreVO x509VO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_X509CERT);
if (x509VO == null) {
try {
spX509Key = SAMLUtils.generateRandomX509Certificate(spKeyPair);
@@ -125,7 +170,7 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
ObjectOutput out = new ObjectOutputStream(bos);
out.writeObject(spX509Key);
out.flush();
- _ksDao.save(SAMLUtils.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert");
+ _ksDao.save(SAMLPluginConstants.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert");
bos.close();
} catch (NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException | SignatureException | InvalidKeyException | IOException e) {
s_logger.error("SAML Plugin won't be able to use X509 signed authentication");
@@ -142,61 +187,194 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
}
}
}
-
- 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);
+ if (spKeyPair != null && spX509Key != null
+ && spId != null && spSsoUrl != null && spSloUrl != null
+ && spOrgName != null && spOrgUrl != null
+ && spContactPersonName != null && spContactPersonEmail != null) {
+ _spMetadata.setEntityId(spId);
+ _spMetadata.setOrganizationName(spOrgName);
+ _spMetadata.setOrganizationUrl(spOrgUrl);
+ _spMetadata.setContactPersonName(spContactPersonName);
+ _spMetadata.setContactPersonEmail(spContactPersonEmail);
+ _spMetadata.setSsoUrl(spSsoUrl);
+ _spMetadata.setSloUrl(spSloUrl);
+ _spMetadata.setKeyPair(spKeyPair);
+ _spMetadata.setSigningCertificate(spX509Key);
+ _spMetadata.setEncryptionCertificate(spX509Key);
+ return true;
}
+ return false;
+ }
- try {
- DefaultBootstrap.bootstrap();
- idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance);
- idpMetaDataProvider.setRequireValidMetadata(true);
- idpMetaDataProvider.setParserPool(new BasicParserPool());
- idpMetaDataProvider.initialize();
+ private void addIdpToMap(EntityDescriptor descriptor, Map<String, SAMLProviderMetadata> idpMap) {
+ SAMLProviderMetadata idpMetadata = new SAMLProviderMetadata();
+ idpMetadata.setEntityId(descriptor.getEntityID());
+ s_logger.debug("Adding IdP to the list of discovered IdPs: " + descriptor.getEntityID());
+ if (descriptor.getOrganization() != null) {
+ if (descriptor.getOrganization().getDisplayNames() != null) {
+ for (OrganizationDisplayName orgName : descriptor.getOrganization().getDisplayNames()) {
+ if (orgName != null && orgName.getName() != null) {
+ idpMetadata.setOrganizationName(orgName.getName().getLocalString());
+ break;
+ }
+ }
+ }
+ if (idpMetadata.getOrganizationName() == null && descriptor.getOrganization().getOrganizationNames() != null) {
+ for (OrganizationName orgName : descriptor.getOrganization().getOrganizationNames()) {
+ if (orgName != null && orgName.getName() != null) {
+ idpMetadata.setOrganizationName(orgName.getName().getLocalString());
+ break;
+ }
+ }
+ }
+ if (descriptor.getOrganization().getURLs() != null) {
+ for (OrganizationURL organizationURL : descriptor.getOrganization().getURLs()) {
+ if (organizationURL != null && organizationURL.getURL() != null) {
+ idpMetadata.setOrganizationUrl(organizationURL.getURL().getLocalString());
+ break;
+ }
+ }
+ }
+ }
+ if (descriptor.getContactPersons() != null) {
+ for (ContactPerson person : descriptor.getContactPersons()) {
+ if (person == null || (person.getGivenName() == null && person.getSurName() == null)
+ || person.getEmailAddresses() == null) {
+ continue;
+ }
+ if (person.getGivenName() != null) {
+ idpMetadata.setContactPersonName(person.getGivenName().getName());
- EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor(this.identityProviderId);
+ } else if (person.getSurName() != null) {
+ idpMetadata.setContactPersonName(person.getSurName().getName());
+ }
+ for (EmailAddress emailAddress : person.getEmailAddresses()) {
+ if (emailAddress != null && emailAddress.getAddress() != null) {
+ idpMetadata.setContactPersonEmail(emailAddress.getAddress());
+ }
+ }
+ if (idpMetadata.getContactPersonName() != null && idpMetadata.getContactPersonEmail() != null) {
+ break;
+ }
+ }
+ }
- IDPSSODescriptor idpssoDescriptor = idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
- if (idpssoDescriptor != null) {
- for (SingleSignOnService ssos: idpssoDescriptor.getSingleSignOnServices()) {
+ IDPSSODescriptor idpDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
+ if (idpDescriptor != null) {
+ if (idpDescriptor.getSingleSignOnServices() != null) {
+ for (SingleSignOnService ssos : idpDescriptor.getSingleSignOnServices()) {
if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
- this.idpSingleSignOnUrl = ssos.getLocation();
+ idpMetadata.setSsoUrl(ssos.getLocation());
}
}
-
- for (SingleLogoutService slos: idpssoDescriptor.getSingleLogoutServices()) {
+ }
+ if (idpDescriptor.getSingleLogoutServices() != null) {
+ for (SingleLogoutService slos : idpDescriptor.getSingleLogoutServices()) {
if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
- this.idpSingleLogOutUrl = slos.getLocation();
+ idpMetadata.setSloUrl(slos.getLocation());
}
}
+ }
- for (KeyDescriptor kd: idpssoDescriptor.getKeyDescriptors()) {
+ X509Certificate unspecifiedKey = null;
+ if (idpDescriptor.getKeyDescriptors() != null) {
+ for (KeyDescriptor kd : idpDescriptor.getKeyDescriptors()) {
if (kd.getUse() == UsageType.SIGNING) {
try {
- this.idpSigningKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
+ idpMetadata.setSigningCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
} catch (CertificateException ignored) {
}
}
if (kd.getUse() == UsageType.ENCRYPTION) {
try {
- this.idpEncryptionKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
+ idpMetadata.setEncryptionCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
+ } catch (CertificateException ignored) {
+ }
+ }
+ if (kd.getUse() == UsageType.UNSPECIFIED) {
+ try {
+ unspecifiedKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
} catch (CertificateException ignored) {
}
}
}
+ }
+ if (idpMetadata.getSigningCertificate() == null && unspecifiedKey != null) {
+ idpMetadata.setSigningCertificate(unspecifiedKey);
+ }
+ if (idpMetadata.getEncryptionCertificate() == null && unspecifiedKey != null) {
+ idpMetadata.setEncryptionCertificate(unspecifiedKey);
+ }
+ if (idpMap.containsKey(idpMetadata.getEntityId())) {
+ s_logger.warn("Duplicate IdP metadata found with entity Id: " + idpMetadata.getEntityId());
+ }
+ idpMap.put(idpMetadata.getEntityId(), idpMetadata);
+ }
+ }
+
+ private void discoverAndAddIdp(XMLObject metadata, Map<String, SAMLProviderMetadata> idpMap) {
+ if (metadata instanceof EntityDescriptor) {
+ EntityDescriptor entityDescriptor = (EntityDescriptor) metadata;
+ addIdpToMap(entityDescriptor, idpMap);
+ } else if (metadata instanceof EntitiesDescriptor) {
+ EntitiesDescriptor entitiesDescriptor = (EntitiesDescriptor) metadata;
+ if (entitiesDescriptor.getEntityDescriptors() != null) {
+ for (EntityDescriptor entityDescriptor: entitiesDescriptor.getEntityDescriptors()) {
+ addIdpToMap(entityDescriptor, idpMap);
+ }
+ }
+ if (entitiesDescriptor.getEntitiesDescriptors() != null) {
+ for (EntitiesDescriptor entitiesDescriptorInner: entitiesDescriptor.getEntitiesDescriptors()) {
+ discoverAndAddIdp(entitiesDescriptorInner, idpMap);
+ }
+ }
+ }
+ }
+
+ class MetadataRefreshTask extends TimerTask {
+ @Override
+ public void run() {
+ if (_idpMetaDataProvider == null) {
+ return;
+ }
+ s_logger.debug("Starting SAML IDP Metadata Refresh Task");
+ Map <String, SAMLProviderMetadata> metadataMap = new HashMap<String, SAMLProviderMetadata>();
+ try {
+ discoverAndAddIdp(_idpMetaDataProvider.getMetadata(), metadataMap);
+ _idpMetadataMap = metadataMap;
+ expireTokens();
+ s_logger.debug("Finished refreshing SAML Metadata and expiring old auth tokens");
+ } catch (MetadataProviderException e) {
+ s_logger.warn("SAML Metadata Refresh task failed with exception: " + e.getMessage());
+ }
+
+ }
+ }
+
+ private boolean setup() {
+ if (!initSP()) {
+ s_logger.error("SAML Plugin failed to initialize, please fix the configuration and restart management server");
+ return false;
+ }
+ _timer = new Timer();
+ final HttpClient client = new HttpClient();
+ final String idpMetaDataUrl = SAMLIdentityProviderMetadataURL.value();
+ if (SAMLTimeout.value() != null && SAMLTimeout.value() > SAMLPluginConstants.SAML_REFRESH_INTERVAL) {
+ _refreshInterval = SAMLTimeout.value();
+ }
+ try {
+ DefaultBootstrap.bootstrap();
+ if (idpMetaDataUrl.startsWith("http")) {
+ _idpMetaDataProvider = new HTTPMetadataProvider(_timer, client, idpMetaDataUrl);
} else {
- s_logger.warn("Provided IDP XML Metadata does not contain IDPSSODescriptor, SAML authentication may not work");
+ File metadataFile = PropertiesUtil.findConfigFile(idpMetaDataUrl);
+ s_logger.debug("Provided Metadata is not a URL, trying to read metadata file from local path: " + metadataFile.getAbsolutePath());
+ _idpMetaDataProvider = new FilesystemMetadataProvider(_timer, metadataFile);
}
+ _idpMetaDataProvider.setRequireValidMetadata(true);
+ _idpMetaDataProvider.setParserPool(new BasicParserPool());
+ _idpMetaDataProvider.initialize();
+ _timer.scheduleAtFixedRate(new MetadataRefreshTask(), 0, _refreshInterval * 1000);
} catch (MetadataProviderException e) {
s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage());
s_logger.error("SAML2 Authentication may be unavailable");
@@ -204,70 +382,138 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
s_logger.error("OpenSAML bootstrapping failed: error: " + e.getMessage());
} catch (NullPointerException e) {
s_logger.error("Unable to setup SAML Auth Plugin due to NullPointerException" +
- " please check the SAML IDP metadata URL and entity ID in global settings: " + e.getMessage());
- }
-
- if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) {
- s_logger.error("SAML based authentication won't work");
+ " please check the SAML global settings: " + e.getMessage());
}
-
return true;
}
@Override
- public List<Class<?>> getAuthCommands() {
- List<Class<?>> cmdList = new ArrayList<Class<?>>();
- if (!isSAMLPluginEnabled()) {
- return cmdList;
+ public SAMLProviderMetadata getSPMetadata() {
+ return _spMetadata;
+ }
+
+ @Override
+ public SAMLProviderMetadata getIdPMetadata(String entityId) {
+ if (entityId != null && _idpMetadataMap.containsKey(entityId)) {
+ return _idpMetadataMap.get(entityId);
}
- cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
- cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
- cmdList.add(GetServiceProviderMetaDataCmd.class);
- return cmdList;
+ String defaultIdpId = SAMLDefaultIdentityProviderId.value();
+ if (defaultIdpId != null && _idpMetadataMap.containsKey(defaultIdpId)) {
+ return _idpMetadataMap.get(defaultIdpId);
+ }
+ // In case of a single IdP, return that as default
+ if (_idpMetadataMap.size() == 1) {
+ return _idpMetadataMap.values().iterator().next();
+ }
+ return null;
}
- public String getServiceProviderId() {
- return serviceProviderId;
+ @Override
+ public Collection<SAMLProviderMetadata> getAllIdPMetadata() {
+ return _idpMetadataMap.values();
}
- public String getIdpSingleSignOnUrl() {
- return this.idpSingleSignOnUrl;
+ @Override
+ public boolean isUserAuthorized(Long userId, String entityId) {
+ UserVO user = _userDao.getUser(userId);
+ if (user != null) {
+ if (user.getSource().equals(User.Source.SAML2) &&
+ user.getExternalEntity().equalsIgnoreCase(entityId)) {
+ return true;
+ }
+ }
+ return false;
}
- public String getIdpSingleLogOutUrl() {
- return this.idpSingleLogOutUrl;
+ @Override
+ public boolean authorizeUser(Long userId, String entityId, boolean enable) {
+ UserVO user = _userDao.getUser(userId);
+ if (user != null) {
+ if (enable) {
+ user.setExternalEntity(entityId);
+ user.setSource(User.Source.SAML2);
+ } else {
+ if (user.getSource().equals(User.Source.SAML2)) {
+ user.setSource(User.Source.SAML2DISABLED);
+ } else {
+ return false;
+ }
+ }
+ _userDao.update(user.getId(), user);
+ return true;
+ }
+ return false;
}
- public String getSpSingleSignOnUrl() {
- return spSingleSignOnUrl;
+ @Override
+ public void saveToken(String authnId, String domainPath, String entity) {
+ Long domainId = null;
+ if (domainPath != null) {
+ Domain domain = _domainMgr.findDomainByPath(domainPath);
+ if (domain != null) {
+ domainId = domain.getId();
+ }
+ }
+ SAMLTokenVO token = new SAMLTokenVO(authnId, domainId, entity);
+ if (_samlTokenDao.findByUuid(authnId) == null) {
+ _samlTokenDao.persist(token);
+ } else {
+ s_logger.warn("Duplicate SAML token for entity=" + entity + " token id=" + authnId + " domain=" + domainPath);
+ }
}
- public String getSpSingleLogOutUrl() {
- return spSingleLogOutUrl;
+ @Override
+ public SAMLTokenVO getToken(String authnId) {
+ return _samlTokenDao.findByUuid(authnId);
}
- public String getIdentityProviderId() {
- return identityProviderId;
+ @Override
+ public void expireTokens() {
+ _samlTokenDao.expireTokens();
}
- public X509Certificate getIdpSigningKey() {
- return idpSigningKey;
+ public Boolean isSAMLPluginEnabled() {
+ return SAMLIsPluginEnabled.value();
}
- public X509Certificate getIdpEncryptionKey() {
- return idpEncryptionKey;
+ @Override
+ public String getConfigComponentName() {
+ return "SAML2-PLUGIN";
}
- public Boolean isSAMLPluginEnabled() {
- return Boolean.valueOf(_configDao.getValue(Config.SAMLIsPluginEnabled.key()));
+ @Override
+ public List<Class<?>> getAuthCommands() {
+ List<Class<?>> cmdList = new ArrayList<Class<?>>();
+ if (!isSAMLPluginEnabled()) {
+ return cmdList;
+ }
+ cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
+ cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
+ cmdList.add(GetServiceProviderMetaDataCmd.class);
+ cmdList.add(ListIdpsCmd.class);
+ return cmdList;
}
- public X509Certificate getSpX509Certificate() {
- return spX509Key;
+ @Override
+ public List<Class<?>> getCommands() {
+ List<Class<?>> cmdList = new ArrayList<Class<?>>();
+ if (!isSAMLPluginEnabled()) {
+ return cmdList;
+ }
+ cmdList.add(AuthorizeSAMLSSOCmd.class);
+ cmdList.add(ListSamlAuthorizationCmd.class);
+ return cmdList;
}
@Override
- public KeyPair getSpKeyPair() {
- return spKeyPair;
+ public ConfigKey<?>[] getConfigKeys() {
+ return new ConfigKey<?>[] {
+ SAMLIsPluginEnabled, SAMLServiceProviderID,
+ SAMLServiceProviderContactPersonName, SAMLServiceProviderContactEmail,
+ SAMLServiceProviderOrgName, SAMLServiceProviderOrgUrl,
+ SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
+ SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
+ SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
+ SAMLSignatureAlgorithm, SAMLTimeout};
}
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
index 68bd81c..5c8a390 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
@@ -21,12 +21,20 @@ 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.cxf.common.util.StringUtils;
import org.apache.log4j.Logger;
+import org.opensaml.DefaultBootstrap;
+import org.opensaml.saml2.core.Response;
+import org.opensaml.saml2.core.StatusCode;
+import org.opensaml.xml.ConfigurationException;
+import org.opensaml.xml.io.UnmarshallingException;
+import org.xml.sax.SAXException;
import javax.ejb.Local;
import javax.inject.Inject;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.FactoryConfigurationError;
+import java.io.IOException;
import java.util.Map;
@Local(value = {UserAuthenticator.class})
@@ -50,13 +58,23 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
}
final UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId);
- if (userAccount == null) {
- s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
+ if (userAccount == null || userAccount.getSource() != User.Source.SAML2) {
+ s_logger.debug("Unable to find user with " + username + " in domain " + domainId + ", or user source is not SAML2");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
} else {
User user = _userDao.getUser(userAccount.getId());
- if (user != null && SAMLUtils.checkSAMLUser(user.getUuid(), username) &&
- requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) {
+ if (user != null && requestParameters != null && requestParameters.containsKey(SAMLPluginConstants.SAML_RESPONSE)) {
+ final String samlResponse = ((String[])requestParameters.get(SAMLPluginConstants.SAML_RESPONSE))[0];
+ Response responseObject = null;
+ try {
+ DefaultBootstrap.bootstrap();
+ responseObject = SAMLUtils.decodeSAMLResponse(samlResponse);
+ } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
+ return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
+ }
+ if (!responseObject.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
+ return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
+ }
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
}
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java
new file mode 100644
index 0000000..5f806e2
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java
@@ -0,0 +1,30 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package org.apache.cloudstack.saml;
+
+public class SAMLPluginConstants {
+ public static final int SAML_REFRESH_INTERVAL = 300;
+
+ public static final String SAML_RESPONSE = "SAMLResponse";
+ public static final String SAML_IDPID = "SAML_IDPID";
+ public static final String SAML_SESSIONID = "SAML_SESSIONID";
+ public static final String SAML_NAMEID = "SAML_NAMEID";
+ public static final String SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR";
+ public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT";
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java
new file mode 100644
index 0000000..c7138a1
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java
@@ -0,0 +1,122 @@
+// 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 java.security.KeyPair;
+import java.security.cert.X509Certificate;
+
+public class SAMLProviderMetadata {
+ private String entityId;
+ private String organizationName;
+ private String organizationUrl;
+ private String contactPersonName;
+ private String contactPersonEmail;
+ private String ssoUrl;
+ private String sloUrl;
+ private KeyPair keyPair;
+ private X509Certificate signingCertificate;
+ private X509Certificate encryptionCertificate;
+
+ public SAMLProviderMetadata() {
+ }
+
+ public void setCommonCertificate(X509Certificate certificate) {
+ this.signingCertificate = certificate;
+ this.encryptionCertificate = certificate;
+ }
+
+ public String getEntityId() {
+ return entityId;
+ }
+
+ public void setEntityId(String entityId) {
+ this.entityId = entityId;
+ }
+
+ public String getContactPersonName() {
+ return contactPersonName;
+ }
+
+ public void setContactPersonName(String contactPersonName) {
+ this.contactPersonName = contactPersonName;
+ }
+
+ public String getContactPersonEmail() {
+ return contactPersonEmail;
+ }
+
+ public void setContactPersonEmail(String contactPersonEmail) {
+ this.contactPersonEmail = contactPersonEmail;
+ }
+
+ public String getOrganizationName() {
+ return organizationName;
+ }
+
+ public void setOrganizationName(String organizationName) {
+ this.organizationName = organizationName;
+ }
+
+ public String getOrganizationUrl() {
+ return organizationUrl;
+ }
+
+ public void setOrganizationUrl(String organizationUrl) {
+ this.organizationUrl = organizationUrl;
+ }
+
+ public KeyPair getKeyPair() {
+ return keyPair;
+ }
+
+ public void setKeyPair(KeyPair keyPair) {
+ this.keyPair = keyPair;
+ }
+
+ public X509Certificate getSigningCertificate() {
+ return signingCertificate;
+ }
+
+ public void setSigningCertificate(X509Certificate signingCertificate) {
+ this.signingCertificate = signingCertificate;
+ }
+
+ public X509Certificate getEncryptionCertificate() {
+ return encryptionCertificate;
+ }
+
+ public void setEncryptionCertificate(X509Certificate encryptionCertificate) {
+ this.encryptionCertificate = encryptionCertificate;
+ }
+
+ public String getSsoUrl() {
+ return ssoUrl;
+ }
+
+ public void setSsoUrl(String ssoUrl) {
+ this.ssoUrl = ssoUrl;
+ }
+
+ public String getSloUrl() {
+ return sloUrl;
+ }
+
+ public void setSloUrl(String sloUrl) {
+ this.sloUrl = sloUrl;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java
new file mode 100644
index 0000000..b045562
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.saml;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface SAMLTokenDao extends GenericDao<SAMLTokenVO, Long> {
+ public void expireTokens();
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java
new file mode 100644
index 0000000..eb106d9
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.saml;
+
+import com.cloud.utils.db.DB;
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.exception.CloudRuntimeException;
+import org.springframework.stereotype.Component;
+
+import javax.ejb.Local;
+import java.sql.PreparedStatement;
+
+@DB
+@Component
+@Local(value = {SAMLTokenDao.class})
+public class SAMLTokenDaoImpl extends GenericDaoBase<SAMLTokenVO, Long> implements SAMLTokenDao {
+
+ public SAMLTokenDaoImpl() {
+ super();
+ }
+
+ @Override
+ public void expireTokens() {
+ TransactionLegacy txn = TransactionLegacy.currentTxn();
+ try {
+ txn.start();
+ String sql = "DELETE FROM `saml_token` WHERE `created` < (NOW() - INTERVAL 1 HOUR)";
+ PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql);
+ pstmt.executeUpdate();
+ txn.commit();
+ } catch (Exception e) {
+ txn.rollback();
+ throw new CloudRuntimeException("Unable to flush old SAML tokens due to exception", e);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java
new file mode 100644
index 0000000..c8ac2f1
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java
@@ -0,0 +1,97 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.saml;
+
+import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.util.Date;
+
+@Entity
+@Table(name = "saml_token")
+public class SAMLTokenVO implements Identity, InternalIdentity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "uuid")
+ private String uuid;
+
+ @Column(name = "domain_id")
+ private Long domainId = null;
+
+ @Column(name = "entity")
+ private String entity = null;
+
+ @Column(name = GenericDao.CREATED_COLUMN)
+ private Date created;
+
+ public SAMLTokenVO() {
+ }
+
+ public SAMLTokenVO(String uuid, Long domainId, String entity) {
+ this.uuid = uuid;
+ this.domainId = domainId;
+ this.entity = entity;
+ }
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public Long getDomainId() {
+ return domainId;
+ }
+
+ public void setDomainId(long domainId) {
+ this.domainId = domainId;
+ }
+
+ public String getEntity() {
+ return entity;
+ }
+
+ public void setEntity(String entity) {
+ this.entity = entity;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
new file mode 100644
index 0000000..0216ad7
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
@@ -0,0 +1,354 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package org.apache.cloudstack.saml;
+
+import com.cloud.utils.HttpUtils;
+import org.apache.log4j.Logger;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.x509.X509V1CertificateGenerator;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.opensaml.Configuration;
+import org.opensaml.DefaultBootstrap;
+import org.opensaml.common.SAMLVersion;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.Attribute;
+import org.opensaml.saml2.core.AttributeStatement;
+import org.opensaml.saml2.core.AuthnContext;
+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.RequestedAuthnContext;
+import org.opensaml.saml2.core.Response;
+import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
+import org.opensaml.saml2.core.impl.AuthnRequestBuilder;
+import org.opensaml.saml2.core.impl.IssuerBuilder;
+import org.opensaml.saml2.core.impl.LogoutRequestBuilder;
+import org.opensaml.saml2.core.impl.NameIDBuilder;
+import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder;
+import org.opensaml.xml.ConfigurationException;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.io.Marshaller;
+import org.opensaml.xml.io.MarshallingException;
+import org.opensaml.xml.io.Unmarshaller;
+import org.opensaml.xml.io.UnmarshallerFactory;
+import org.opensaml.xml.io.UnmarshallingException;
+import org.opensaml.xml.signature.SignatureConstants;
+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 javax.xml.stream.FactoryConfigurationError;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.List;
+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 String generateSecureRandomId() {
+ return new BigInteger(160, new SecureRandom()).toString(32);
+ }
+
+ public static String getValueFromAttributeStatements(final List<AttributeStatement> attributeStatements, final String attributeKey) {
+ if (attributeStatements == null || attributeStatements.size() < 1 || attributeKey == null) {
+ return null;
+ }
+ for (AttributeStatement attributeStatement : attributeStatements) {
+ if (attributeStatement == null || attributeStatements.size() < 1) {
+ continue;
+ }
+ for (Attribute attribute : attributeStatement.getAttributes()) {
+ if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) {
+ String value = attribute.getAttributeValues().get(0).getDOM().getTextContent();
+ s_logger.debug("SAML attribute name: " + attribute.getName() + " friendly-name:" + attribute.getFriendlyName() + " value:" + value);
+ if (attributeKey.equals(attribute.getName()) || attributeKey.equals(attribute.getFriendlyName())) {
+ return value;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public static String getValueFromAssertions(final List<Assertion> assertions, final String attributeKey) {
+ if (assertions == null || attributeKey == null) {
+ return null;
+ }
+ for (Assertion assertion : assertions) {
+ String value = getValueFromAttributeStatements(assertion.getAttributeStatements(), attributeKey);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public static String buildAuthnRequestUrl(final String authnId, final SAMLProviderMetadata spMetadata, final SAMLProviderMetadata idpMetadata, final String signatureAlgorithm) {
+ String redirectUrl = "";
+ try {
+ DefaultBootstrap.bootstrap();
+ AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(authnId, spMetadata.getEntityId(), idpMetadata.getSsoUrl(), spMetadata.getSsoUrl());
+ PrivateKey privateKey = null;
+ if (spMetadata.getKeyPair() != null) {
+ privateKey = spMetadata.getKeyPair().getPrivate();
+ }
+ redirectUrl = idpMetadata.getSsoUrl() + "?" + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey, signatureAlgorithm);
+ } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException | NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) {
+ s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
+ }
+ return redirectUrl;
+ }
+
+ public static AuthnRequest buildAuthnRequestObject(final String authnId, final String spId, final String idpUrl, final String consumerUrl) {
+ // Issuer object
+ IssuerBuilder issuerBuilder = new IssuerBuilder();
+ Issuer issuer = issuerBuilder.buildObject();
+ issuer.setValue(spId);
+
+ // AuthnContextClass
+ AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
+ AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(
+ SAMLConstants.SAML20_NS,
+ "AuthnContextClassRef", "saml");
+ authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
+
+ // AuthnContext
+ RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
+ RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
+ requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
+ requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
+
+ // Creation of AuthRequestObject
+ AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
+ AuthnRequest authnRequest = authRequestBuilder.buildObject();
+ authnRequest.setID(authnId);
+ authnRequest.setDestination(idpUrl);
+ authnRequest.setVersion(SAMLVersion.VERSION_20);
+ authnRequest.setForceAuthn(false);
+ authnRequest.setIsPassive(false);
+ authnRequest.setIssueInstant(new DateTime());
+ authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
+ authnRequest.setAssertionConsumerServiceURL(consumerUrl);
+ authnRequest.setProviderName(spId);
+ authnRequest.setIssuer(issuer);
+ authnRequest.setRequestedAuthnContext(requestedAuthnContext);
+
+ return authnRequest;
+ }
+
+ public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, String nameIdString) {
+ Issuer issuer = new IssuerBuilder().buildObject();
+ issuer.setValue(spId);
+ NameID nameID = new NameIDBuilder().buildObject();
+ nameID.setValue(nameIdString);
+ LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
+ logoutRequest.setID(generateSecureRandomId());
+ logoutRequest.setDestination(logoutUrl);
+ logoutRequest.setVersion(SAMLVersion.VERSION_20);
+ logoutRequest.setIssueInstant(new DateTime());
+ logoutRequest.setIssuer(issuer);
+ 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 String generateSAMLRequestSignature(final String urlEncodedString, final PrivateKey signingKey, final String sigAlgorithmName)
+ throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException {
+ if (signingKey == null) {
+ return urlEncodedString;
+ }
+
+ String opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1;
+ String javaSignatureAlgorithmName = "SHA1withRSA";
+
+ if (sigAlgorithmName.equalsIgnoreCase("SHA256")) {
+ opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
+ javaSignatureAlgorithmName = "SHA256withRSA";
+ } else if (sigAlgorithmName.equalsIgnoreCase("SHA384")) {
+ opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA384;
+ javaSignatureAlgorithmName = "SHA384withRSA";
+ } else if (sigAlgorithmName.equalsIgnoreCase("SHA512")) {
+ opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512;
+ javaSignatureAlgorithmName = "SHA512withRSA";
+ }
+
+ String url = urlEncodedString + "&SigAlg=" + URLEncoder.encode(opensamlAlgoIdSignature, HttpUtils.UTF_8);
+ Signature signature = Signature.getInstance(javaSignatureAlgorithmName);
+ signature.initSign(signingKey);
+ signature.update(url.getBytes());
+ String signatureString = Base64.encodeBytes(signature.sign(), Base64.DONT_BREAK_LINES);
+ if (signatureString != null) {
+ return url + "&Signature=" + URLEncoder.encode(signatureString, HttpUtils.UTF_8);
+ }
+ return url;
+ }
+
+ public static KeyFactory getKeyFactory() {
+ KeyFactory keyFactory = null;
+ try {
+ Security.addProvider(new BouncyCastleProvider());
+ keyFactory = KeyFactory.getInstance("RSA", "BC");
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ s_logger.error("Unable to create KeyFactory:" + e.getMessage());
+ }
+ return keyFactory;
+ }
+
+ public static String savePublicKey(PublicKey key) {
+ try {
+ KeyFactory keyFactory = SAMLUtils.getKeyFactory();
+ if (keyFactory == null) return null;
+ X509EncodedKeySpec spec = keyFactory.getKeySpec(key, X509EncodedKeySpec.class);
+ return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()));
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create KeyFactory:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static String savePrivateKey(PrivateKey key) {
+ try {
+ KeyFactory keyFactory = SAMLUtils.getKeyFactory();
+ if (keyFactory == null) return null;
+ PKCS8EncodedKeySpec spec = keyFactory.getKeySpec(key,
+ PKCS8EncodedKeySpec.class);
+ return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()));
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create KeyFactory:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static PublicKey loadPublicKey(String publicKey) {
+ byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(publicKey);
+ X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(sigBytes);
+ KeyFactory keyFact = SAMLUtils.getKeyFactory();
+ if (keyFact == null)
+ return null;
+ try {
+ return keyFact.generatePublic(x509KeySpec);
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static PrivateKey loadPrivateKey(String privateKey) {
+ byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(privateKey);
+ PKCS8EncodedKeySpec pkscs8KeySpec = new PKCS8EncodedKeySpec(sigBytes);
+ KeyFactory keyFact = SAMLUtils.getKeyFactory();
+ if (keyFact == null)
+ return null;
+ try {
+ return keyFact.generatePrivate(pkscs8KeySpec);
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static KeyPair generateRandomKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException {
+ Security.addProvider(new BouncyCastleProvider());
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+ keyPairGenerator.initialize(4096, new SecureRandom());
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException {
+ DateTime now = DateTime.now(DateTimeZone.UTC);
+ X500Principal dnName = new X500Principal("CN=ApacheCloudStack");
+ X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+ certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+ certGen.setSubjectDN(dnName);
+ certGen.setIssuerDN(dnName);
+ certGen.setNotBefore(now.minusDays(1).toDate());
+ certGen.setNotAfter(now.plusYears(3).toDate());
+ certGen.setPublicKey(keyPair.getPublic());
+ certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+ return certGen.generate(keyPair.getPrivate(), "BC");
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java
new file mode 100644
index 0000000..5b4d552
--- /dev/null
+++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+import com.cloud.utils.HttpUtils;
+import org.apache.cloudstack.api.ApiServerService;
+import org.apache.cloudstack.api.auth.APIAuthenticationType;
+import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
+import org.apache.cloudstack.saml.SAML2AuthManager;
+import org.apache.cloudstack.saml.SAMLProviderMetadata;
+import org.apache.cloudstack.saml.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.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.lang.reflect.Field;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+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;
+
+ @Mock
+ HttpServletRequest req;
+
+ @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";
+ KeyPair kp = SAMLUtils.generateRandomKeyPair();
+ X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp);
+
+ SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata();
+ providerMetadata.setEntityId("random");
+ providerMetadata.setSigningCertificate(cert);
+ providerMetadata.setEncryptionCertificate(cert);
+ providerMetadata.setKeyPair(kp);
+ providerMetadata.setSsoUrl("http://test.local");
+ providerMetadata.setSloUrl("http://test.local");
+
+ Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata);
+
+ String result = cmd.authenticate("command", null, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
+ Assert.assertTrue(result.contains("md:EntityDescriptor"));
+ }
+
+ @Test
+ public void testGetAPIType() {
+ Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java
----------------------------------------------------------------------
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 83792c6..5b37388 100644
--- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java
+++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java
@@ -25,8 +25,8 @@ 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.SAMLPluginConstants;
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;
@@ -68,8 +68,6 @@ public class SAML2UserAuthenticatorTest {
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);
@@ -81,9 +79,9 @@ public class SAML2UserAuthenticatorTest {
Assert.assertFalse(pair.first());
// When there is SAMLRequest in params and user is same as the mocked one
- params.put(SAMLUtils.SAML_RESPONSE, new Object[]{});
+ params.put(SAMLPluginConstants.SAML_RESPONSE, new String[]{"RandomString"});
pair = authenticator.authenticate("someUID", "random", 1l, params);
- Assert.assertTrue(pair.first());
+ Assert.assertFalse(pair.first());
// When there is SAMLRequest in params but username is null
pair = authenticator.authenticate(null, "random", 1l, params);
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/7310c2d4/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java
new file mode 100644
index 0000000..bd87831
--- /dev/null
+++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java
@@ -0,0 +1,74 @@
+//
+// 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;
+
+import junit.framework.TestCase;
+import org.apache.cloudstack.saml.SAMLUtils;
+import org.junit.Test;
+import org.opensaml.saml2.core.AuthnRequest;
+import org.opensaml.saml2.core.LogoutRequest;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+public class SAMLUtilsTest extends TestCase {
+
+ @Test
+ public void testGenerateSecureRandomId() throws Exception {
+ assertTrue(SAMLUtils.generateSecureRandomId().length() > 0);
+ }
+
+ @Test
+ public void testBuildAuthnRequestObject() throws Exception {
+ String consumerUrl = "http://someurl.com";
+ String idpUrl = "http://idp.domain.example";
+ String spId = "cloudstack";
+ String authnId = SAMLUtils.generateSecureRandomId();
+ AuthnRequest req = SAMLUtils.buildAuthnRequestObject(authnId, 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 nameId = "_12345";
+ LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, nameId);
+ assertEquals(req.getDestination(), logoutUrl);
+ assertEquals(req.getIssuer().getValue(), spId);
+ }
+
+ @Test
+ public void testX509Helpers() throws Exception {
+ KeyPair keyPair = SAMLUtils.generateRandomKeyPair();
+
+ String privateKeyString = SAMLUtils.savePrivateKey(keyPair.getPrivate());
+ String publicKeyString = SAMLUtils.savePublicKey(keyPair.getPublic());
+
+ PrivateKey privateKey = SAMLUtils.loadPrivateKey(privateKeyString);
+ PublicKey publicKey = SAMLUtils.loadPublicKey(publicKeyString);
+
+ assertTrue(privateKey.equals(keyPair.getPrivate()));
+ assertTrue(publicKey.equals(keyPair.getPublic()));
+ }
+}
\ No newline at end of file