blob: ba85b151eea9f30f228a0142075a0b3ed81ff246 [file] [log] [blame]
// 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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
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;
import javax.inject.Inject;
import javax.xml.stream.FactoryConfigurationError;
import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd;
import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
import org.apache.cloudstack.api.command.ListAndSwitchSAMLAccountCmd;
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.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.security.CertUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.log4j.Logger;
import org.bouncycastle.operator.OperatorCreationException;
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.security.credential.UsageType;
import org.opensaml.xml.security.keyinfo.KeyInfoHelper;
import org.springframework.stereotype.Component;
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;
@Component
public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable {
private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class);
private SAMLProviderMetadata _spMetadata = new SAMLProviderMetadata();
private Map<String, SAMLProviderMetadata> _idpMetadataMap = new HashMap<String, SAMLProviderMetadata>();
private String idpSingleSignOnUrl;
private String idpSingleLogOutUrl;
private Timer _timer;
private int _refreshInterval = SAMLPluginConstants.SAML_REFRESH_INTERVAL;
private AbstractReloadingMetadataProvider _idpMetaDataProvider;
public String getSAMLIdentityProviderMetadataURL(){
return SAMLIdentityProviderMetadataURL.value();
}
@Inject
private KeystoreDao _ksDao;
@Inject
private SAMLTokenDao _samlTokenDao;
@Inject
private UserDao _userDao;
@Inject
DomainManager _domainMgr;
@Override
public boolean start() {
if (isSAMLPluginEnabled()) {
s_logger.info("SAML auth plugin loaded");
return setup();
} else {
s_logger.info("SAML auth plugin not enabled so not loading");
return super.start();
}
}
@Override
public boolean stop() {
if (_timer != null) {
_timer.cancel();
}
return super.stop();
}
protected boolean initSP() {
KeystoreVO keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR);
if (keyStoreVO == null) {
try {
KeyPair keyPair = CertUtils.generateRandomKeyPair(4096);
_ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR,
SAMLUtils.encodePrivateKey(keyPair.getPrivate()),
SAMLUtils.encodePublicKey(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 (final NoSuchProviderException | NoSuchAlgorithmException e) {
s_logger.error("Unable to create and save SAML keypair, due to: ", e);
}
}
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) {
final PrivateKey privateKey = SAMLUtils.decodePrivateKey(keyStoreVO.getCertificate());
final PublicKey publicKey = SAMLUtils.decodePublicKey(keyStoreVO.getKey());
if (privateKey != null && publicKey != null) {
spKeyPair = new KeyPair(publicKey, privateKey);
KeystoreVO x509VO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_X509CERT);
if (x509VO == null) {
try {
spX509Key = SAMLUtils.generateRandomX509Certificate(spKeyPair);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutput out = new ObjectOutputStream(bos);
out.writeObject(spX509Key);
out.flush();
_ksDao.save(SAMLPluginConstants.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert");
bos.close();
} catch (final NoSuchAlgorithmException | NoSuchProviderException | CertificateException | SignatureException | InvalidKeyException | IOException | OperatorCreationException e) {
s_logger.error("SAML plugin won't be able to use X509 signed authentication", e);
}
} else {
try {
ByteArrayInputStream bi = new ByteArrayInputStream(Base64.decodeBase64(x509VO.getCertificate()));
ObjectInputStream si = new ObjectInputStream(bi);
spX509Key = (X509Certificate) si.readObject();
bi.close();
} catch (IOException | ClassNotFoundException ignored) {
s_logger.error("SAML Plugin won't be able to use X509 signed authentication. Failed to load X509 Certificate from Database.");
}
}
}
}
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;
}
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());
} 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 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)) {
idpMetadata.setSsoUrl(ssos.getLocation());
}
}
}
if (idpDescriptor.getSingleLogoutServices() != null) {
for (SingleLogoutService slos : idpDescriptor.getSingleLogoutServices()) {
if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
idpMetadata.setSloUrl(slos.getLocation());
}
}
}
X509Certificate unspecifiedKey = null;
if (idpDescriptor.getKeyDescriptors() != null) {
for (KeyDescriptor kd : idpDescriptor.getKeyDescriptors()) {
if (kd.getUse() == UsageType.SIGNING) {
try {
idpMetadata.setSigningCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
} catch (CertificateException ignored) {
s_logger.info("[ignored] encountered invalid certificate signing.", ignored);
}
}
if (kd.getUse() == UsageType.ENCRYPTION) {
try {
idpMetadata.setEncryptionCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
} catch (CertificateException ignored) {
s_logger.info("[ignored] encountered invalid certificate encryption.", ignored);
}
}
if (kd.getUse() == UsageType.UNSPECIFIED) {
try {
unspecifiedKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
} catch (CertificateException ignored) {
s_logger.info("[ignored] encountered invalid certificate.", 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 = getSAMLIdentityProviderMetadataURL();
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 {
File metadataFile = PropertiesUtil.findConfigFile(idpMetaDataUrl);
if (metadataFile == null) {
s_logger.error("Provided Metadata is not a URL, Unable to locate metadata file from local path: " + idpMetaDataUrl);
return false;
}
else{
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(SAMLUtils.getSaferParserPool());
_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");
return false;
} catch (ConfigurationException | FactoryConfigurationError e) {
s_logger.error("OpenSAML bootstrapping failed: error: " + e.getMessage());
return false;
} catch (NullPointerException e) {
s_logger.error("Unable to setup SAML Auth Plugin due to NullPointerException" +
" please check the SAML global settings: " + e.getMessage());
return false;
}
return true;
}
@Override
public SAMLProviderMetadata getSPMetadata() {
return _spMetadata;
}
@Override
public SAMLProviderMetadata getIdPMetadata(String entityId) {
if (entityId != null && _idpMetadataMap.containsKey(entityId)) {
return _idpMetadataMap.get(entityId);
}
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;
}
@Override
public Collection<SAMLProviderMetadata> getAllIdPMetadata() {
return _idpMetadataMap.values();
}
@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;
}
@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;
}
@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);
}
}
@Override
public SAMLTokenVO getToken(String authnId) {
return _samlTokenDao.findByUuid(authnId);
}
@Override
public void expireTokens() {
_samlTokenDao.expireTokens();
}
public Boolean isSAMLPluginEnabled() {
return SAMLIsPluginEnabled.value();
}
@Override
public String getConfigComponentName() {
return "SAML2-PLUGIN";
}
@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);
cmdList.add(ListAndSwitchSAMLAccountCmd.class);
return cmdList;
}
@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 ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {
SAMLIsPluginEnabled, SAMLServiceProviderID,
SAMLServiceProviderContactPersonName, SAMLServiceProviderContactEmail,
SAMLServiceProviderOrgName, SAMLServiceProviderOrgUrl,
SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout};
}
}