http://git-wip-us.apache.org/repos/asf/cloudstack/blob/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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", "[email protected]", + "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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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/20ce346f/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
