From b93c87aa210097539a5778448cec5ca281b5de17 Mon Sep 17 00:00:00 2001 From: amiri Date: Sun, 31 Jan 2016 19:22:39 -0800 Subject: [PATCH] Added feature that turns UAA into a SAML Identity Provider. --- .../identity/uaa/zone/SamlConfig.java | 27 + .../identity/uaa/audit/AuditEventType.java | 4 +- .../SamlServiceProviderEndpoints.java | 148 +++ .../saml/ZoneAwareMetadataManager.java | 39 +- .../saml/idp/IdpExtendedMetadata.java | 39 + .../saml/idp/IdpMetadataGenerator.java | 887 ++++++++++++++++++ .../saml/idp/IdpMetadataGeneratorFilter.java | 229 +++++ .../provider/saml/idp/IdpMetadataManager.java | 30 + .../saml/idp/IdpSamlAuthentication.java | 83 ++ .../idp/IdpSamlAuthenticationProvider.java | 33 + .../IdpSamlAuthenticationSuccessHandler.java | 125 +++ .../saml/idp/IdpSamlContextProviderImpl.java | 26 + .../saml/idp/IdpWebSSOProfileOptions.java | 33 + .../provider/saml/idp/IdpWebSsoProfile.java | 20 + .../saml/idp/IdpWebSsoProfileImpl.java | 300 ++++++ .../JdbcSamlServiceProviderProvisioning.java | 215 +++++ .../saml/idp/SamlServiceProvider.java | 307 ++++++ .../SamlServiceProviderChangedListener.java | 79 ++ .../idp/SamlServiceProviderConfigurator.java | 298 ++++++ .../idp/SamlServiceProviderDefinition.java | 294 ++++++ .../idp/SamlServiceProviderDeletable.java | 33 + .../idp/SamlServiceProviderProvisioning.java | 20 + .../idp/SamlSpAlreadyExistsException.java | 15 + .../idp/ZoneAwareIdpMetadataGenerator.java | 75 ++ .../saml/idp/ZoneAwareIdpMetadataManager.java | 738 +++++++++++++++ .../event/ServiceProviderEventPublisher.java | 41 + .../event/ServiceProviderModifiedEvent.java | 50 + server/src/main/resources/login-ui.xml | 10 +- .../db/hsqldb/V2_7_6__SAML_SP_Management.sql | 13 + .../db/mysql/V2_7_6__SAML_SP_Management.sql | 13 + .../postgresql/V2_7_6__SAML_SP_Management.sql | 13 + .../uaa/audit/AuditEventTypeTests.java | 2 +- ...pSamlAuthenticationSuccessHandlerTest.java | 171 ++++ .../saml/idp/IdpWebSsoProfileImplTest.java | 81 ++ ...bcSamlServiceProviderProvisioningTest.java | 222 +++++ .../SamlServiceProviderConfiguratorTest.java | 100 ++ .../SamlServiceProviderDefinitionTest.java | 90 ++ .../uaa/provider/saml/idp/SamlTestUtils.java | 414 ++++++++ .../ZoneAwareIdpMetadataGeneratorTest.java | 53 ++ .../main/webapp/WEB-INF/spring-servlet.xml | 1 + .../WEB-INF/spring/multitenant-endpoints.xml | 48 + .../main/webapp/WEB-INF/spring/saml-idp.xml | 145 +++ .../webapp/WEB-INF/spring/saml-providers.xml | 30 +- .../feature/SamlLoginWithLocalIdpIT.java | 561 +++++++++++ .../util/IntegrationTestUtils.java | 75 +- 45 files changed, 6201 insertions(+), 29 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/SamlServiceProviderEndpoints.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpExtendedMetadata.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGenerator.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGeneratorFilter.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataManager.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthentication.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationProvider.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandler.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlContextProviderImpl.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSSOProfileOptions.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfile.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImpl.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioning.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProvider.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderChangedListener.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinition.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDeletable.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderProvisioning.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlSpAlreadyExistsException.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGenerator.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataManager.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderEventPublisher.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderModifiedEvent.java create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_6__SAML_SP_Management.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_6__SAML_SP_Management.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_6__SAML_SP_Management.sql create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandlerTest.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImplTest.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioningTest.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfiguratorTest.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinitionTest.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlTestUtils.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGeneratorTest.java create mode 100644 uaa/src/main/webapp/WEB-INF/spring/saml-idp.xml create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginWithLocalIdpIT.java diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/SamlConfig.java b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/SamlConfig.java index bc8fdd61d79..024d73ab2c4 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/SamlConfig.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/SamlConfig.java @@ -22,8 +22,11 @@ import java.security.cert.X509Certificate; public class SamlConfig { + private boolean assertionSigned = true; private boolean requestSigned = true; private boolean wantAssertionSigned = false; + private boolean wantAuthnRequestSigned = false; + private int assertionTimeToLiveSeconds = 600; private String certificate; private String privateKey; private String privateKeyPassword; @@ -31,6 +34,14 @@ public class SamlConfig { @JsonIgnore private KeyWithCert keyCert; + public boolean isAssertionSigned() { + return assertionSigned; + } + + public void setAssertionSigned(boolean assertionSigned) { + this.assertionSigned = assertionSigned; + } + public boolean isRequestSigned() { return requestSigned; } @@ -55,6 +66,22 @@ public void setCertificate(String certificate) throws CertificateException { } } + public boolean isWantAuthnRequestSigned() { + return wantAuthnRequestSigned; + } + + public void setWantAuthnRequestSigned(boolean wantAuthnRequestSigned) { + this.wantAuthnRequestSigned = wantAuthnRequestSigned; + } + + public int getAssertionTimeToLiveSeconds() { + return assertionTimeToLiveSeconds; + } + + public void setAssertionTimeToLiveSeconds(int assertionTimeToLiveSeconds) { + this.assertionTimeToLiveSeconds = assertionTimeToLiveSeconds; + } + public String getCertificate() { return certificate; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java index e87132f9e0c..0dc123f0950 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java @@ -53,7 +53,9 @@ public enum AuditEventType { IdentityProviderModifiedEvent(29), IdentityZoneCreatedEvent(30), IdentityZoneModifiedEvent(31), - EntityDeletedEvent(32); + EntityDeletedEvent(32), + ServiceProviderCreatedEvent(33), + ServiceProviderModifiedEvent(34); private final int code; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/SamlServiceProviderEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/SamlServiceProviderEndpoints.java new file mode 100644 index 00000000000..b1e500a2f33 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/SamlServiceProviderEndpoints.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.provider; + +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.authentication.manager.LdapLoginAuthenticationManager; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProvider; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderConfigurator; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderProvisioning; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.ObjectUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/service-providers") +@RestController +public class SamlServiceProviderEndpoints { + + protected static Log logger = LogFactory.getLog(SamlServiceProviderEndpoints.class); + + private final SamlServiceProviderProvisioning serviceProviderProvisioning; + private final SamlServiceProviderConfigurator samlConfigurator; + + public SamlServiceProviderEndpoints(SamlServiceProviderProvisioning serviceProviderProvisioning, + SamlServiceProviderConfigurator samlConfigurator) { + this.serviceProviderProvisioning = serviceProviderProvisioning; + this.samlConfigurator = samlConfigurator; + } + + @RequestMapping(method = POST) + public ResponseEntity createServiceProvider(@RequestBody SamlServiceProvider body) + throws MetadataProviderException { + String zoneId = IdentityZoneHolder.get().getId(); + body.setIdentityZoneId(zoneId); + + SamlServiceProviderDefinition definition = ObjectUtils.castInstance(body.getConfig(), + SamlServiceProviderDefinition.class); + definition.setZoneId(zoneId); + definition.setSpEntityId(body.getEntityId()); + samlConfigurator.addSamlServiceProviderDefinition(definition); + body.setConfig(definition); + + SamlServiceProvider createdSp = serviceProviderProvisioning.create(body); + return new ResponseEntity<>(createdSp, HttpStatus.CREATED); + } + + @RequestMapping(value = "{id}", method = PUT) + public ResponseEntity updateServiceProvider(@PathVariable String id, + @RequestBody SamlServiceProvider body) throws MetadataProviderException { + SamlServiceProvider existing = serviceProviderProvisioning.retrieve(id); + String zoneId = IdentityZoneHolder.get().getId(); + body.setId(id); + body.setIdentityZoneId(zoneId); + if (!body.configIsValid()) { + return new ResponseEntity<>(UNPROCESSABLE_ENTITY); + } + body.setEntityId(existing.getEntityId()); + SamlServiceProviderDefinition definition = ObjectUtils.castInstance(body.getConfig(), + SamlServiceProviderDefinition.class); + definition.setZoneId(zoneId); + definition.setSpEntityId(body.getEntityId()); + samlConfigurator.addSamlServiceProviderDefinition(definition); + body.setConfig(definition); + + SamlServiceProvider updatedSp = serviceProviderProvisioning.update(body); + return new ResponseEntity<>(updatedSp, OK); + } + + @RequestMapping(method = GET) + public ResponseEntity> retrieveServiceProviders( + @RequestParam(value = "active_only", required = false) String activeOnly) { + Boolean retrieveActiveOnly = Boolean.valueOf(activeOnly); + List serviceProviderList = serviceProviderProvisioning.retrieveAll(retrieveActiveOnly, + IdentityZoneHolder.get().getId()); + return new ResponseEntity<>(serviceProviderList, OK); + } + + @RequestMapping(value = "{id}", method = GET) + public ResponseEntity retrieveServiceProvider(@PathVariable String id) { + SamlServiceProvider serviceProvider = serviceProviderProvisioning.retrieve(id); + return new ResponseEntity<>(serviceProvider, OK); + } + + @ExceptionHandler(MetadataProviderException.class) + public ResponseEntity handleMetadataProviderException(MetadataProviderException e) { + if (e.getMessage().contains("Duplicate")) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT); + } else { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @ExceptionHandler(JsonUtils.JsonUtilException.class) + public ResponseEntity handleMetadataProviderException() { + return new ResponseEntity<>("Invalid provider configuration.", HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(EmptyResultDataAccessException.class) + public ResponseEntity handleProviderNotFoundException() { + return new ResponseEntity<>("Provider not found.", HttpStatus.NOT_FOUND); + } + + protected String getExceptionString(Exception x) { + StringWriter writer = new StringWriter(); + x.printStackTrace(new PrintWriter(writer)); + return writer.getBuffer().toString(); + } + + protected static class NoOpLdapLoginAuthenticationManager extends LdapLoginAuthenticationManager { + @Override + public Authentication authenticate(Authentication request) throws AuthenticationException { + return request; + } + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ZoneAwareMetadataManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ZoneAwareMetadataManager.java index add96683342..bd4deade0ff 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ZoneAwareMetadataManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ZoneAwareMetadataManager.java @@ -14,13 +14,26 @@ package org.cloudfoundry.identity.uaa.provider.saml; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; +import javax.xml.namespace.QName; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; @@ -45,18 +58,6 @@ import org.springframework.security.saml.metadata.MetadataManager; import org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer; -import javax.annotation.PostConstruct; -import javax.xml.namespace.QName; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentHashMap; - public class ZoneAwareMetadataManager extends MetadataManager implements ExtendedMetadataProvider, InitializingBean, DisposableBean, BeanNameAware { private static final Log logger = LogFactory.getLog(ZoneAwareMetadataManager.class); @@ -69,13 +70,11 @@ public class ZoneAwareMetadataManager extends MetadataManager implements Extende private long lastRefresh = 0; private Timer timer; private String beanName = ZoneAwareMetadataManager.class.getName()+"-"+System.identityHashCode(this); - private ProviderChangedListener providerChangedListener; public ZoneAwareMetadataManager(IdentityProviderProvisioning providerDao, IdentityZoneProvisioning zoneDao, SamlIdentityProviderConfigurator configurator, - KeyManager keyManager, - ProviderChangedListener listener) throws MetadataProviderException { + KeyManager keyManager) throws MetadataProviderException { super(Collections.emptyList()); this.providerDao = providerDao; this.zoneDao = zoneDao; @@ -87,7 +86,6 @@ public ZoneAwareMetadataManager(IdentityProviderProvisioning providerDao, if (metadataManagers==null) { metadataManagers = new ConcurrentHashMap<>(); } - providerChangedListener = listener; } private class RefreshTask extends TimerTask { @@ -114,11 +112,11 @@ public void checkAllProviders() throws MetadataProviderException { refreshAllProviders(); timer = new Timer("ZoneAwareMetadataManager.Refresh["+beanName+"]", true); timer.schedule(new RefreshTask(),refreshInterval , refreshInterval); - providerChangedListener.setMetadataManager(this); } protected void refreshAllProviders() throws MetadataProviderException { refreshAllProviders(true); + //refreshAllSamlServiceProviders(true); } protected String getThreadNameAndId() { @@ -175,7 +173,7 @@ protected void removeSamlProvider(IdentityZone zone, ExtensionMetadataManager ma } } - protected ExtensionMetadataManager getManager(IdentityZone zone) { + public ExtensionMetadataManager getManager(IdentityZone zone) { if (metadataManagers==null) { //called during super constructor metadataManagers = new ConcurrentHashMap<>(); } @@ -191,7 +189,7 @@ protected ExtensionMetadataManager getManager(IdentityZone zone) { } return metadataManagers.get(zone); } - protected ExtensionMetadataManager getManager() { + public ExtensionMetadataManager getManager() { return getManager(IdentityZoneHolder.get()); } @@ -454,6 +452,7 @@ protected Set refreshZoneManager(ExtensionMetadataManager ma //just so that we can override protected methods public static class ExtensionMetadataManager extends CachingMetadataManager { + public ExtensionMetadataManager(List providers) throws MetadataProviderException { super(providers); //disable internal timers (they only get created when afterPropertiesSet) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpExtendedMetadata.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpExtendedMetadata.java new file mode 100644 index 00000000000..99efe0f0bfc --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpExtendedMetadata.java @@ -0,0 +1,39 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.springframework.security.saml.metadata.ExtendedMetadata; + +/** + * A SAML IdP needs information beyond what the standard ExtendedMetadata provides. + * This class exists to provide that extra information. + */ +public class IdpExtendedMetadata extends ExtendedMetadata { + + /** + * Generated serialization id. + */ + private static final long serialVersionUID = -7933870052729540864L; + + private boolean assertionsSigned = true; + private int assertionTimeToLiveSeconds = 500; + + public boolean isAssertionsSigned() { + return assertionsSigned; + } + + public void setAssertionsSigned(boolean assertionsSigned) { + this.assertionsSigned = assertionsSigned; + } + + public int getAssertionTimeToLiveSeconds() { + return assertionTimeToLiveSeconds; + } + + public void setAssertionTimeToLiveSeconds(int assertionTimeToLiveSeconds) { + this.assertionTimeToLiveSeconds = assertionTimeToLiveSeconds; + } + + @Override + public IdpExtendedMetadata clone() { + return (IdpExtendedMetadata) super.clone(); + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGenerator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGenerator.java new file mode 100644 index 00000000000..1d56a014975 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGenerator.java @@ -0,0 +1,887 @@ +/* Copyright 2009 Vladimir Schäfer +* +* Licensed 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.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.opensaml.Configuration; +import org.opensaml.common.SAMLObjectBuilder; +import org.opensaml.common.SAMLRuntimeException; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.common.Extensions; +import org.opensaml.saml2.common.impl.ExtensionsBuilder; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.metadata.*; +import org.opensaml.samlext.idpdisco.DiscoveryResponse; +import org.opensaml.util.URLBuilder; +import org.opensaml.xml.XMLObjectBuilderFactory; +import org.opensaml.xml.security.SecurityHelper; +import org.opensaml.xml.security.credential.Credential; +import org.opensaml.xml.security.credential.UsageType; +import org.opensaml.xml.security.keyinfo.KeyInfoGenerator; +import org.opensaml.xml.signature.KeyInfo; +import org.opensaml.xml.util.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.saml.*; +import org.springframework.security.saml.key.KeyManager; +import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.security.saml.util.SAMLUtil; + +import javax.xml.namespace.QName; +import java.util.*; + +/** + * The class is responsible for generating the metadata that describes the identity provider in the current deployment + * environment. All the URLs in the metadata derive from information provided by the ServletContext. + * + * This code for this class is based on org.springframework.security.saml.metadata.MetadataGenerator. + */ +public class IdpMetadataGenerator { + + private String id; + private String entityId; + private String entityBaseURL; + + private boolean wantAuthnRequestSigned = true; + + /** + * Index of the assertion consumer endpoint marked as default. + */ + private int assertionConsumerIndex = 0; + + /** + * Extended metadata with details on metadata generation. + */ + private IdpExtendedMetadata extendedMetadata; + + // List of case-insensitive alias terms + private static TreeMap aliases = new TreeMap(String.CASE_INSENSITIVE_ORDER); + + static { + aliases.put(SAMLConstants.SAML2_POST_BINDING_URI, SAMLConstants.SAML2_POST_BINDING_URI); + aliases.put("post", SAMLConstants.SAML2_POST_BINDING_URI); + aliases.put("http-post", SAMLConstants.SAML2_POST_BINDING_URI); + aliases.put(SAMLConstants.SAML2_PAOS_BINDING_URI, SAMLConstants.SAML2_PAOS_BINDING_URI); + aliases.put("paos", SAMLConstants.SAML2_PAOS_BINDING_URI); + aliases.put(SAMLConstants.SAML2_ARTIFACT_BINDING_URI, SAMLConstants.SAML2_ARTIFACT_BINDING_URI); + aliases.put("artifact", SAMLConstants.SAML2_ARTIFACT_BINDING_URI); + aliases.put("http-artifact", SAMLConstants.SAML2_ARTIFACT_BINDING_URI); + aliases.put(SAMLConstants.SAML2_REDIRECT_BINDING_URI, SAMLConstants.SAML2_REDIRECT_BINDING_URI); + aliases.put("redirect", SAMLConstants.SAML2_REDIRECT_BINDING_URI); + aliases.put("http-redirect", SAMLConstants.SAML2_REDIRECT_BINDING_URI); + aliases.put(SAMLConstants.SAML2_SOAP11_BINDING_URI, SAMLConstants.SAML2_SOAP11_BINDING_URI); + aliases.put("soap", SAMLConstants.SAML2_SOAP11_BINDING_URI); + aliases.put(NameIDType.EMAIL, NameIDType.EMAIL); + aliases.put("email", NameIDType.EMAIL); + aliases.put(NameIDType.TRANSIENT, NameIDType.TRANSIENT); + aliases.put("transient", NameIDType.TRANSIENT); + aliases.put(NameIDType.PERSISTENT, NameIDType.PERSISTENT); + aliases.put("persistent", NameIDType.PERSISTENT); + aliases.put(NameIDType.UNSPECIFIED, NameIDType.UNSPECIFIED); + aliases.put("unspecified", NameIDType.UNSPECIFIED); + aliases.put(NameIDType.X509_SUBJECT, NameIDType.X509_SUBJECT); + aliases.put("x509_subject", NameIDType.X509_SUBJECT); + } + + /** + * Bindings for single sign-on + */ + private Collection bindingsSSO = Arrays.asList("post", "artifact"); + + /** + * Bindings for single sign-on holder of key + */ + private Collection bindingsHoKSSO = Arrays.asList(); + + /** + * Bindings for single logout + */ + private Collection bindingsSLO = Arrays.asList("post", "redirect"); + + /** + * Flag indicates whether to include extension with discovery endpoints in metadata. + */ + private boolean includeDiscoveryExtension; + + /** + * NameIDs to be included in generated metadata. + */ + private Collection nameID = null; + + /** + * Default set of NameIDs included in metadata. + */ + public static final Collection defaultNameID = Arrays.asList(NameIDType.EMAIL, NameIDType.TRANSIENT, + NameIDType.PERSISTENT, NameIDType.UNSPECIFIED, NameIDType.X509_SUBJECT); + + protected XMLObjectBuilderFactory builderFactory; + + /** + * Source of certificates. + */ + protected KeyManager keyManager; + + /** + * Filters for loading of paths. + */ + protected SAMLProcessingFilter samlWebSSOFilter; + protected SAMLWebSSOHoKProcessingFilter samlWebSSOHoKFilter; + protected SAMLLogoutProcessingFilter samlLogoutProcessingFilter; + protected SAMLEntryPoint samlEntryPoint; + protected SAMLDiscovery samlDiscovery; + + /** + * Class logger. + */ + protected final static Logger log = LoggerFactory.getLogger(IdpMetadataGenerator.class); + + /** + * Default constructor. + */ + public IdpMetadataGenerator() { + this.builderFactory = Configuration.getBuilderFactory(); + } + + public EntityDescriptor generateMetadata() { + + boolean wantAuthnRequestSigned = isWantAuthnRequestSigned(); + + Collection includedNameID = getNameID(); + + String entityId = getEntityId(); + String entityBaseURL = getEntityBaseURL(); + String entityAlias = getEntityAlias(); + + validateRequiredAttributes(entityId, entityBaseURL); + + if (id == null) { + // Use entityID cleaned as NCName for ID in case no value is provided + id = SAMLUtil.getNCNameString(entityId); + } + + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory + .getBuilder(EntityDescriptor.DEFAULT_ELEMENT_NAME); + EntityDescriptor descriptor = builder.buildObject(); + if (id != null) { + descriptor.setID(id); + } + descriptor.setEntityID(entityId); + + IDPSSODescriptor ssoDescriptor = buildIDPSSODescriptor(entityBaseURL, entityAlias, wantAuthnRequestSigned, + includedNameID); + if (ssoDescriptor != null) { + descriptor.getRoleDescriptors().add(ssoDescriptor); + } + + return descriptor; + } + + protected void validateRequiredAttributes(String entityId, String entityBaseURL) { + if (entityId == null || entityBaseURL == null) { + throw new RuntimeException("Required attributes entityId or entityBaseURL weren't set"); + } + } + + protected KeyInfo getServerKeyInfo(String alias) { + Credential serverCredential = keyManager.getCredential(alias); + if (serverCredential == null) { + throw new RuntimeException("Key for alias " + alias + " not found"); + } else if (serverCredential.getPrivateKey() == null) { + throw new RuntimeException("Key with alias " + alias + " doesn't have a private key"); + } + return generateKeyInfoForCredential(serverCredential); + } + + /** + * Generates extended metadata. Default extendedMetadata object is cloned if present and used for defaults. The + * following properties are always overriden from the properties of this bean: discoveryUrl, discoveryResponseUrl, + * signingKey, encryptionKey, entityAlias and tlsKey. Property local of the generated metadata is always set to + * true. + * + * @return generated extended metadata + */ + public IdpExtendedMetadata generateExtendedMetadata() { + + IdpExtendedMetadata metadata; + + if (extendedMetadata != null) { + metadata = extendedMetadata.clone(); + } else { + metadata = new IdpExtendedMetadata(); + } + + String entityBaseURL = getEntityBaseURL(); + String entityAlias = getEntityAlias(); + + if (isIncludeDiscovery()) { + metadata.setIdpDiscoveryURL(getDiscoveryURL(entityBaseURL, entityAlias)); + metadata.setIdpDiscoveryResponseURL(getDiscoveryResponseURL(entityBaseURL, entityAlias)); + } else { + metadata.setIdpDiscoveryURL(null); + metadata.setIdpDiscoveryResponseURL(null); + } + + metadata.setLocal(true); + metadata.setAssertionTimeToLiveSeconds(getAssertionTimeToLiveSeconds()); + metadata.setAssertionsSigned(isAssertionsSigned()); + return metadata; + + } + + protected KeyInfo generateKeyInfoForCredential(Credential credential) { + try { + String keyInfoGeneratorName = org.springframework.security.saml.SAMLConstants.SAML_METADATA_KEY_INFO_GENERATOR; + if (extendedMetadata != null && extendedMetadata.getKeyInfoGeneratorName() != null) { + keyInfoGeneratorName = extendedMetadata.getKeyInfoGeneratorName(); + } + KeyInfoGenerator keyInfoGenerator = SecurityHelper.getKeyInfoGenerator(credential, null, + keyInfoGeneratorName); + return keyInfoGenerator.generate(credential); + } catch (org.opensaml.xml.security.SecurityException e) { + log.error("Can't obtain key from the keystore or generate key info for credential: " + credential, e); + throw new SAMLRuntimeException("Can't obtain key from keystore or generate key info", e); + } + } + + protected IDPSSODescriptor buildIDPSSODescriptor(String entityBaseURL, String entityAlias, + boolean wantAuthnRequestSigned, Collection includedNameID) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory + .getBuilder(IDPSSODescriptor.DEFAULT_ELEMENT_NAME); + IDPSSODescriptor idpDescriptor = builder.buildObject(); + idpDescriptor.setWantAuthnRequestsSigned(wantAuthnRequestSigned); + idpDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); + + // Name ID + idpDescriptor.getNameIDFormats().addAll(getNameIDFormat(includedNameID)); + + // Resolve alases + Collection bindingsSSO = mapAliases(getBindingsSSO()); + Collection bindingsSLO = mapAliases(getBindingsSLO()); + + // Assertion consumer MUST NOT be used with HTTP Redirect, Profiles 424, same applies to HoK profile + for (String binding : bindingsSSO) { + if (binding.equals(SAMLConstants.SAML2_ARTIFACT_BINDING_URI)) { + idpDescriptor.getSingleSignOnServices().add(getSingleSignOnService(entityBaseURL, entityAlias, + getSAMLWebSSOProcessingFilterPath(), SAMLConstants.SAML2_ARTIFACT_BINDING_URI)); + } + if (binding.equals(SAMLConstants.SAML2_POST_BINDING_URI)) { + idpDescriptor.getSingleSignOnServices().add(getSingleSignOnService(entityBaseURL, entityAlias, + getSAMLWebSSOProcessingFilterPath(), SAMLConstants.SAML2_POST_BINDING_URI)); + } + if (binding.equals(SAMLConstants.SAML2_PAOS_BINDING_URI)) { + idpDescriptor.getSingleSignOnServices().add(getSingleSignOnService(entityBaseURL, entityAlias, + getSAMLWebSSOProcessingFilterPath(), SAMLConstants.SAML2_PAOS_BINDING_URI)); + } + } + + for (String binding : bindingsSLO) { + if (binding.equals(SAMLConstants.SAML2_POST_BINDING_URI)) { + idpDescriptor.getSingleLogoutServices() + .add(getSingleLogoutService(entityBaseURL, entityAlias, SAMLConstants.SAML2_POST_BINDING_URI)); + } + if (binding.equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { + idpDescriptor.getSingleLogoutServices().add( + getSingleLogoutService(entityBaseURL, entityAlias, SAMLConstants.SAML2_REDIRECT_BINDING_URI)); + } + if (binding.equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) { + idpDescriptor.getSingleLogoutServices().add( + getSingleLogoutService(entityBaseURL, entityAlias, SAMLConstants.SAML2_SOAP11_BINDING_URI)); + } + } + + // Build extensions + Extensions extensions = buildExtensions(entityBaseURL, entityAlias); + if (extensions != null) { + idpDescriptor.setExtensions(extensions); + } + + // Populate key aliases + String signingKey = getSigningKey(); + String encryptionKey = getEncryptionKey(); + String tlsKey = getTLSKey(); + + // Generate key info + if (signingKey != null) { + idpDescriptor.getKeyDescriptors().add(getKeyDescriptor(UsageType.SIGNING, getServerKeyInfo(signingKey))); + } else { + log.info( + "Generating metadata without signing key, KeyStore doesn't contain any default private key, or the signingKey specified in ExtendedMetadata cannot be found"); + } + if (encryptionKey != null) { + idpDescriptor.getKeyDescriptors() + .add(getKeyDescriptor(UsageType.ENCRYPTION, getServerKeyInfo(encryptionKey))); + } else { + log.info( + "Generating metadata without encryption key, KeyStore doesn't contain any default private key, or the encryptionKey specified in ExtendedMetadata cannot be found"); + } + + // Include TLS key with unspecified usage in case it differs from the singing and encryption keys + if (tlsKey != null && !(tlsKey.equals(encryptionKey)) && !(tlsKey.equals(signingKey))) { + idpDescriptor.getKeyDescriptors().add(getKeyDescriptor(UsageType.UNSPECIFIED, getServerKeyInfo(tlsKey))); + } + + return idpDescriptor; + } + + /** + * Method iterates all values in the input, for each tries to resolve correct alias. When alias value is found, it + * is entered into the return collection, otherwise warning is logged. Values are returned in order of input with + * all duplicities removed. + * + * @param values + * input collection + * @return result with resolved aliases + */ + protected Collection mapAliases(Collection values) { + LinkedHashSet result = new LinkedHashSet(); + for (String value : values) { + String alias = aliases.get(value); + if (alias != null) { + result.add(alias); + } else { + log.warn("Unsupported value " + value + " found"); + } + } + return result; + } + + protected Extensions buildExtensions(String entityBaseURL, String entityAlias) { + + boolean include = false; + Extensions extensions = new ExtensionsBuilder().buildObject(); + + // Add discovery + if (isIncludeDiscoveryExtension()) { + DiscoveryResponse discoveryService = getDiscoveryService(entityBaseURL, entityAlias); + extensions.getUnknownXMLObjects().add(discoveryService); + include = true; + } + + if (include) { + return extensions; + } else { + return null; + } + + } + + protected KeyDescriptor getKeyDescriptor(UsageType type, KeyInfo key) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory() + .getBuilder(KeyDescriptor.DEFAULT_ELEMENT_NAME); + KeyDescriptor descriptor = builder.buildObject(); + descriptor.setUse(type); + descriptor.setKeyInfo(key); + return descriptor; + } + + protected Collection getNameIDFormat(Collection includedNameID) { + + // Resolve alases + includedNameID = mapAliases(includedNameID); + Collection formats = new LinkedList(); + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory + .getBuilder(NameIDFormat.DEFAULT_ELEMENT_NAME); + + // Populate nameIDs + for (String nameIDValue : includedNameID) { + + if (nameIDValue.equals(NameIDType.EMAIL)) { + NameIDFormat nameID = builder.buildObject(); + nameID.setFormat(NameIDType.EMAIL); + formats.add(nameID); + } + + if (nameIDValue.equals(NameIDType.TRANSIENT)) { + NameIDFormat nameID = builder.buildObject(); + nameID.setFormat(NameIDType.TRANSIENT); + formats.add(nameID); + } + + if (nameIDValue.equals(NameIDType.PERSISTENT)) { + NameIDFormat nameID = builder.buildObject(); + nameID.setFormat(NameIDType.PERSISTENT); + formats.add(nameID); + } + + if (nameIDValue.equals(NameIDType.UNSPECIFIED)) { + NameIDFormat nameID = builder.buildObject(); + nameID.setFormat(NameIDType.UNSPECIFIED); + formats.add(nameID); + } + + if (nameIDValue.equals(NameIDType.X509_SUBJECT)) { + NameIDFormat nameID = builder.buildObject(); + nameID.setFormat(NameIDType.X509_SUBJECT); + formats.add(nameID); + } + + } + + return formats; + + } + + protected SingleSignOnService getSingleSignOnService(String entityBaseURL, String entityAlias, String filterURL, + String binding) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory + .getBuilder(SingleSignOnService.DEFAULT_ELEMENT_NAME); + SingleSignOnService sso = builder.buildObject(); + sso.setLocation(getServerURL(entityBaseURL, entityAlias, filterURL)); + sso.setBinding(binding); + return sso; + } + + protected SingleSignOnService getHoKSingleSignOnService(String entityBaseURL, String entityAlias, String filterURL, + String binding) { + SingleSignOnService hokSso = getSingleSignOnService(entityBaseURL, entityAlias, filterURL, + org.springframework.security.saml.SAMLConstants.SAML2_HOK_WEBSSO_PROFILE_URI); + QName consumerName = new QName(org.springframework.security.saml.SAMLConstants.SAML2_HOK_WEBSSO_PROFILE_URI, + AuthnRequest.PROTOCOL_BINDING_ATTRIB_NAME, "hoksso"); + hokSso.getUnknownAttributes().put(consumerName, binding); + return hokSso; + } + + protected DiscoveryResponse getDiscoveryService(String entityBaseURL, String entityAlias) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory + .getBuilder(DiscoveryResponse.DEFAULT_ELEMENT_NAME); + DiscoveryResponse discovery = builder.buildObject(DiscoveryResponse.DEFAULT_ELEMENT_NAME); + discovery.setBinding(DiscoveryResponse.IDP_DISCO_NS); + discovery.setLocation(getDiscoveryResponseURL(entityBaseURL, entityAlias)); + return discovery; + } + + protected SingleLogoutService getSingleLogoutService(String entityBaseURL, String entityAlias, String binding) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory + .getBuilder(SingleLogoutService.DEFAULT_ELEMENT_NAME); + SingleLogoutService logoutService = builder.buildObject(); + logoutService.setLocation(getServerURL(entityBaseURL, entityAlias, getSAMLLogoutFilterPath())); + logoutService.setBinding(binding); + return logoutService; + } + + /** + * Creates URL at which the local server is capable of accepting incoming SAML messages. + * + * @param entityBaseURL + * entity ID + * @param processingURL + * local context at which processing filter is waiting + * @return URL of local server + */ + private String getServerURL(String entityBaseURL, String entityAlias, String processingURL) { + + return getServerURL(entityBaseURL, entityAlias, processingURL, null); + + } + + /** + * Creates URL at which the local server is capable of accepting incoming SAML messages. + * + * @param entityBaseURL + * entity ID + * @param processingURL + * local context at which processing filter is waiting + * @param parameters + * key - value pairs to be included as query part of the generated url, can be null + * @return URL of local server + */ + private String getServerURL(String entityBaseURL, String entityAlias, String processingURL, + Map parameters) { + + StringBuilder result = new StringBuilder(); + result.append(entityBaseURL); + if (!processingURL.startsWith("/")) { + result.append("/"); + } + result.append(processingURL); + + if (entityAlias != null) { + if (!processingURL.endsWith("/")) { + result.append("/"); + } + result.append("alias/"); + result.append(entityAlias); + result.append("/idp"); + } + + String resultString = result.toString(); + + if (parameters == null || parameters.size() == 0) { + + return resultString; + + } else { + + // Add parameters + URLBuilder returnUrlBuilder = new URLBuilder(resultString); + for (Map.Entry entry : parameters.entrySet()) { + returnUrlBuilder.getQueryParams().add(new Pair(entry.getKey(), entry.getValue())); + } + return returnUrlBuilder.buildURL(); + + } + + } + + private String getSAMLWebSSOProcessingFilterPath() { + if (samlWebSSOFilter != null) { + return samlWebSSOFilter.getFilterProcessesUrl(); + } else { + return SAMLProcessingFilter.FILTER_URL; + } + } + + private String getSAMLEntryPointPath() { + if (samlEntryPoint != null) { + return samlEntryPoint.getFilterProcessesUrl(); + } else { + return SAMLEntryPoint.FILTER_URL; + } + } + + private String getSAMLDiscoveryPath() { + if (samlDiscovery != null) { + return samlDiscovery.getFilterProcessesUrl(); + } else { + return SAMLDiscovery.FILTER_URL; + } + } + + private String getSAMLLogoutFilterPath() { + if (samlLogoutProcessingFilter != null) { + return samlLogoutProcessingFilter.getFilterProcessesUrl(); + } else { + return SAMLLogoutProcessingFilter.FILTER_URL; + } + } + + @Autowired(required = false) + @Qualifier("samlWebSSOProcessingFilter") + public void setSamlWebSSOFilter(SAMLProcessingFilter samlWebSSOFilter) { + this.samlWebSSOFilter = samlWebSSOFilter; + } + + @Autowired(required = false) + @Qualifier("samlWebSSOHoKProcessingFilter") + public void setSamlWebSSOHoKFilter(SAMLWebSSOHoKProcessingFilter samlWebSSOHoKFilter) { + this.samlWebSSOHoKFilter = samlWebSSOHoKFilter; + } + + @Autowired(required = false) + public void setSamlLogoutProcessingFilter(SAMLLogoutProcessingFilter samlLogoutProcessingFilter) { + this.samlLogoutProcessingFilter = samlLogoutProcessingFilter; + } + + @Autowired(required = false) + public void setSamlEntryPoint(SAMLEntryPoint samlEntryPoint) { + this.samlEntryPoint = samlEntryPoint; + } + + public boolean isWantAuthnRequestSigned() { + return wantAuthnRequestSigned; + } + + public void setWantAuthnRequestSigned(boolean wantAuthnRequestSigned) { + this.wantAuthnRequestSigned = wantAuthnRequestSigned; + } + + public Collection getNameID() { + return nameID == null ? defaultNameID : nameID; + } + + public void setNameID(Collection nameID) { + this.nameID = nameID; + } + + public String getEntityBaseURL() { + return entityBaseURL; + } + + public void setEntityBaseURL(String entityBaseURL) { + this.entityBaseURL = entityBaseURL; + } + + @Autowired + public void setKeyManager(KeyManager keyManager) { + this.keyManager = keyManager; + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public String getEntityId() { + return entityId; + } + + public Collection getBindingsSSO() { + return bindingsSSO; + } + + /** + * List of bindings to be included in the generated metadata for Web Single Sign-On. Ordering of bindings affects + * inclusion in the generated metadata. + * + * Supported values are: "artifact" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"), "post" (or + * "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") and "paos" (or "urn:oasis:names:tc:SAML:2.0:bindings:PAOS"). + * + * The following bindings are included by default: "artifact", "post" + * + * @param bindingsSSO + * bindings for web single sign-on + */ + public void setBindingsSSO(Collection bindingsSSO) { + if (bindingsSSO == null) { + this.bindingsSSO = Collections.emptyList(); + } else { + this.bindingsSSO = bindingsSSO; + } + } + + public Collection getBindingsSLO() { + return bindingsSLO; + } + + /** + * List of bindings to be included in the generated metadata for Single Logout. Ordering of bindings affects + * inclusion in the generated metadata. + * + * Supported values are: "post" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") and "redirect" (or + * "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"). + * + * The following bindings are included by default: "post", "redirect" + * + * @param bindingsSLO + * bindings for single logout + */ + public void setBindingsSLO(Collection bindingsSLO) { + if (bindingsSLO == null) { + this.bindingsSLO = Collections.emptyList(); + } else { + this.bindingsSLO = bindingsSLO; + } + } + + public Collection getBindingsHoKSSO() { + return bindingsHoKSSO; + } + + /** + * List of bindings to be included in the generated metadata for Web Single Sign-On Holder of Key. Ordering of + * bindings affects inclusion in the generated metadata. + * + * Supported values are: "artifact" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact") and "post" (or + * "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"). + * + * By default there are no included bindings for the profile. + * + * @param bindingsHoKSSO + * bindings for web single sign-on holder-of-key + */ + public void setBindingsHoKSSO(Collection bindingsHoKSSO) { + if (bindingsHoKSSO == null) { + this.bindingsHoKSSO = Collections.emptyList(); + } else { + this.bindingsHoKSSO = bindingsHoKSSO; + } + } + + public boolean isIncludeDiscoveryExtension() { + return includeDiscoveryExtension; + } + + /** + * When true discovery profile extension metadata pointing to the default SAMLEntryPoint will be generated and + * stored in the generated metadata document. + * + * @param includeDiscoveryExtension + * flag indicating whether IDP discovery should be enabled + */ + public void setIncludeDiscoveryExtension(boolean includeDiscoveryExtension) { + this.includeDiscoveryExtension = includeDiscoveryExtension; + } + + public int getAssertionConsumerIndex() { + return assertionConsumerIndex; + } + + /** + * Generated assertion consumer service with the index equaling set value will be marked as default. Use negative + * value to skip the default attribute altogether. + * + * @param assertionConsumerIndex + * assertion consumer index of service to mark as default + */ + public void setAssertionConsumerIndex(int assertionConsumerIndex) { + this.assertionConsumerIndex = assertionConsumerIndex; + } + + /** + * True when IDP discovery is enabled either on local property includeDiscovery or property idpDiscoveryEnabled in + * the extended metadata. + * + * @return true when discovery is enabled + */ + protected boolean isIncludeDiscovery() { + return extendedMetadata != null && extendedMetadata.isIdpDiscoveryEnabled(); + } + + /** + * Provides set discovery request url or generates a default when none was provided. Primarily value set on + * extenedMetadata property idpDiscoveryURL is used, when empty local property customDiscoveryURL is used, when + * empty URL is automatically generated. + * + * @param entityBaseURL + * base URL for generation of endpoints + * @param entityAlias + * alias of entity, or null when there's no alias required + * @return URL to use for IDP discovery request + */ + protected String getDiscoveryURL(String entityBaseURL, String entityAlias) { + if (extendedMetadata != null && extendedMetadata.getIdpDiscoveryURL() != null + && extendedMetadata.getIdpDiscoveryURL().length() > 0) { + return extendedMetadata.getIdpDiscoveryURL(); + } else { + return getServerURL(entityBaseURL, entityAlias, getSAMLDiscoveryPath()); + } + } + + /** + * Provides set discovery response url or generates a default when none was provided. Primarily value set on + * extenedMetadata property idpDiscoveryResponseURL is used, when empty local property customDiscoveryResponseURL + * is used, when empty URL is automatically generated. + * + * @param entityBaseURL + * base URL for generation of endpoints + * @param entityAlias + * alias of entity, or null when there's no alias required + * @return URL to use for IDP discovery response + */ + protected String getDiscoveryResponseURL(String entityBaseURL, String entityAlias) { + if (extendedMetadata != null && extendedMetadata.getIdpDiscoveryResponseURL() != null + && extendedMetadata.getIdpDiscoveryResponseURL().length() > 0) { + return extendedMetadata.getIdpDiscoveryResponseURL(); + } else { + Map params = new HashMap(); + params.put(SAMLEntryPoint.DISCOVERY_RESPONSE_PARAMETER, "true"); + return getServerURL(entityBaseURL, entityAlias, getSAMLEntryPointPath(), params); + } + } + + /** + * Provides key used for signing from extended metadata. Uses default key when key is not specified. + * + * @return signing key + */ + protected String getSigningKey() { + if (extendedMetadata != null && extendedMetadata.getSigningKey() != null) { + return extendedMetadata.getSigningKey(); + } else { + return keyManager.getDefaultCredentialName(); + } + } + + /** + * Provides key used for encryption from extended metadata. Uses default when key is not specified. + * + * @return encryption key + */ + protected String getEncryptionKey() { + if (extendedMetadata != null && extendedMetadata.getEncryptionKey() != null) { + return extendedMetadata.getEncryptionKey(); + } else { + return keyManager.getDefaultCredentialName(); + } + } + + /** + * Provides key used for SSL/TLS from extended metadata. Uses null when key is not specified. + * + * @return tls key + */ + protected String getTLSKey() { + if (extendedMetadata != null && extendedMetadata.getTlsKey() != null) { + return extendedMetadata.getTlsKey(); + } else { + return null; + } + } + + /** + * Provides entity alias from extended metadata, or null when metadata isn't specified or contains null. + * + * @return entity alias + */ + protected String getEntityAlias() { + if (extendedMetadata != null) { + return extendedMetadata.getAlias(); + } else { + return null; + } + } + + public boolean isAssertionsSigned() { + if (extendedMetadata != null) { + return extendedMetadata.isAssertionsSigned(); + } else { + return true; + } + } + + public int getAssertionTimeToLiveSeconds() { + if (extendedMetadata != null) { + return extendedMetadata.getAssertionTimeToLiveSeconds(); + } else { + return 600; + } + } + + /** + * Extended metadata which contains details on configuration of the generated service provider metadata. + * + * @return extended metadata + */ + public ExtendedMetadata getExtendedMetadata() { + return extendedMetadata; + } + + /** + * Default value for generation of extended metadata. Value is cloned upon each request to generate new + * ExtendedMetadata object. + * + * @param extendedMetadata + * default extended metadata or null + */ + public void setExtendedMetadata(IdpExtendedMetadata extendedMetadata) { + this.extendedMetadata = extendedMetadata; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGeneratorFilter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGeneratorFilter.java new file mode 100644 index 00000000000..6caf6d40abf --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataGeneratorFilter.java @@ -0,0 +1,229 @@ +/* Copyright 2009 Vladimir Schäfer +* +* Licensed 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.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.provider.MetadataProvider; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.util.SimpleURLCanonicalizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; +import org.springframework.security.saml.metadata.MetadataDisplayFilter; +import org.springframework.security.saml.metadata.MetadataMemoryProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +/** + * The filter expects calls on configured URL and presents user with SAML2 metadata representing this application + * deployment. In case the application is configured to automatically generate metadata, the generation occurs upon + * first invocation of this filter (first request made to the server). + * + * This class is based on org.springframework.security.saml.metadata.MetadataGeneratorFilter. + */ +public class IdpMetadataGeneratorFilter extends GenericFilterBean { + + /** + * Class logger. + */ + protected final static Logger log = LoggerFactory.getLogger(IdpMetadataGeneratorFilter.class); + + /** + * Class storing all SAML metadata documents + */ + protected IdpMetadataManager manager; + + /** + * Class capable of generating new metadata. + */ + protected IdpMetadataGenerator generator; + + /** + * Metadata display filter. + */ + protected MetadataDisplayFilter displayFilter; + + /** + * Flag indicates that in case generated base url is used (when value is not provided in the MetadataGenerator) it + * should be normalized. Normalization includes lower-casing of scheme and server name and removing standar ports + * of 80 for http and 443 for https schemes. + */ + protected boolean normalizeBaseUrl; + + /** + * Default constructor. + * + * @param generator + * generator + */ + public IdpMetadataGeneratorFilter(IdpMetadataGenerator generator) { + this.generator = generator; + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + processMetadataInitialization((HttpServletRequest) request); + chain.doFilter(request, response); + } + + /** + * Verifies whether generation is needed and if so the metadata document is created and stored in metadata + * manager. + * + * @param request + * request + * @throws javax.servlet.ServletException + * error + */ + protected void processMetadataInitialization(HttpServletRequest request) throws ServletException { + + // In case the hosted IdP metadata weren't initialized, let's do it now + if (manager.getHostedIdpName() == null) { + + synchronized (IdpMetadataManager.class) { + + if (manager.getHostedIdpName() == null) { + + try { + + log.info( + "No default metadata configured, generating with default values, please pre-configure metadata for production use"); + + // Defaults + String alias = generator.getEntityAlias(); + String baseURL = getDefaultBaseURL(request); + + // Use default baseURL if not set + if (generator.getEntityBaseURL() == null) { + log.warn( + "Generated default entity base URL {} based on values in the first server request. Please set property entityBaseURL on MetadataGenerator bean to fixate the value.", + baseURL); + generator.setEntityBaseURL(baseURL); + } else { + baseURL = generator.getEntityBaseURL(); + } + + // Use default entityID if not set + if (generator.getEntityId() == null) { + generator.setEntityId(getDefaultEntityID(baseURL, alias)); + } + + EntityDescriptor descriptor = generator.generateMetadata(); + ExtendedMetadata extendedMetadata = generator.generateExtendedMetadata(); + + log.info("Created default metadata for system with entityID: " + descriptor.getEntityID()); + MetadataMemoryProvider memoryProvider = new MetadataMemoryProvider(descriptor); + memoryProvider.initialize(); + MetadataProvider metadataProvider = new ExtendedMetadataDelegate(memoryProvider, + extendedMetadata); + + manager.addMetadataProvider(metadataProvider); + manager.setHostedIdpName(descriptor.getEntityID()); + manager.refreshMetadata(); + + } catch (MetadataProviderException e) { + log.error("Error generating system metadata", e); + throw new ServletException("Error generating system metadata", e); + } + + } + + } + + } + + } + + protected String getDefaultEntityID(String entityBaseUrl, String alias) { + + String displayFilterUrl = MetadataDisplayFilter.FILTER_URL; + if (displayFilter != null) { + displayFilterUrl = displayFilter.getFilterProcessesUrl(); + } + + StringBuilder sb = new StringBuilder(); + sb.append(entityBaseUrl); + sb.append(displayFilterUrl); + + if (StringUtils.hasLength(alias)) { + sb.append("/alias/"); + sb.append(alias); + } + + return sb.toString(); + + } + + protected String getDefaultBaseURL(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + sb.append(request.getScheme()).append("://").append(request.getServerName()).append(":") + .append(request.getServerPort()); + sb.append(request.getContextPath()); + String url = sb.toString(); + if (isNormalizeBaseUrl()) { + return SimpleURLCanonicalizer.canonicalize(url); + } else { + return url; + } + } + + @Autowired(required = false) + public void setDisplayFilter(MetadataDisplayFilter displayFilter) { + this.displayFilter = displayFilter; + } + + @Autowired + public void setManager(IdpMetadataManager manager) { + this.manager = manager; + } + + public boolean isNormalizeBaseUrl() { + return normalizeBaseUrl; + } + + /** + * When true flag indicates that in case generated base url is used (when value is not provided in the + * MetadataGenerator) it should be normalized. Normalization includes lower-casing of scheme and server name and + * removing standar ports of 80 for http and 443 for https schemes. + * + * @param normalizeBaseUrl + * flag + */ + public void setNormalizeBaseUrl(boolean normalizeBaseUrl) { + this.normalizeBaseUrl = normalizeBaseUrl; + } + + /** + * Verifies that required entities were autowired or set. + */ + @Override + public void afterPropertiesSet() throws ServletException { + super.afterPropertiesSet(); + Assert.notNull(generator, "Metadata generator"); + Assert.notNull(manager, "MetadataManager must be set"); + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataManager.java new file mode 100644 index 00000000000..5da97faf9ec --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpMetadataManager.java @@ -0,0 +1,30 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.util.List; + +import org.opensaml.saml2.metadata.provider.MetadataProvider; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.springframework.security.saml.metadata.MetadataManager; + +/** + * MetadataManager has a field that stores the entity id of the local SAML service provider. However, in order to + * support SAML identity provider funcationality we also need to store the entity id of the local SAML identity + * provider. That is what this class provides. + * + */ +public class IdpMetadataManager extends MetadataManager { + + private String hostedIdpName; + + public IdpMetadataManager(final List providers) throws MetadataProviderException { + super(providers); + } + + public String getHostedIdpName() { + return this.hostedIdpName; + } + + public void setHostedIdpName(final String hostedIdpName) { + this.hostedIdpName = hostedIdpName; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthentication.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthentication.java new file mode 100644 index 00000000000..7177b806aa2 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthentication.java @@ -0,0 +1,83 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.util.Collection; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * This authentication object represents an SAML authentication requests that was authenticated using an openId login. + * In other words, when the local SAML identity provider receives an authentication request from an external SAML + * service provider, it authenticates the user using the UAA spring openId login page. UAA stores the result of that + * authentication in an instance of this object. As such this object consists of a holder that contains both a + * SamlAuthenticationToken, which provides the SAML context, and an OpenIdAuthenticationToken, which provides the + * authentication details of the authenticated user. + * + */ +public class IdpSamlAuthentication implements Authentication { + + /** + * Generated serialization id. + */ + private static final long serialVersionUID = -4895486519411522514L; + + private final IdpSamlCredentialsHolder credentials; + + public IdpSamlAuthentication(IdpSamlCredentialsHolder credentials) { + this.credentials = credentials; + } + + @Override + public String getName() { + return credentials.getLoginAuthenticationToken().getName(); + } + + @Override + public Collection getAuthorities() { + return credentials.getLoginAuthenticationToken().getAuthorities(); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getDetails() { + return credentials.getLoginAuthenticationToken().getDetails(); + } + + @Override + public Object getPrincipal() { + return credentials.getLoginAuthenticationToken().getPrincipal(); + } + + @Override + public boolean isAuthenticated() { + return credentials.getLoginAuthenticationToken().isAuthenticated(); + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + // Do nothing. + } + + public static class IdpSamlCredentialsHolder { + + private final Authentication samlAuthenticationToken; + private final Authentication loginAuthenticationToken; + + public IdpSamlCredentialsHolder(Authentication samlAuthenticationToken, Authentication loginAuthenticationToken) { + this.samlAuthenticationToken = samlAuthenticationToken; + this.loginAuthenticationToken = loginAuthenticationToken; + } + + public Authentication getSamlAuthenticationToken() { + return samlAuthenticationToken; + } + + public Authentication getLoginAuthenticationToken() { + return loginAuthenticationToken; + } + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationProvider.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationProvider.java new file mode 100644 index 00000000000..a9e5c795c75 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationProvider.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.cloudfoundry.identity.uaa.provider.saml.idp.IdpSamlAuthentication.IdpSamlCredentialsHolder; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml.SAMLAuthenticationToken; + +/** + * This authentication provider produces a composite authentication object that contains the SamlAuthenticationToken, + * which contains the SAML context, and the OpenIdAuthenticationToken, which contains information about the + * authenticated user. + */ +public class IdpSamlAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication loginAuthenticationToken = securityContext.getAuthentication(); + + IdpSamlCredentialsHolder credentials = new IdpSamlCredentialsHolder(authentication, loginAuthenticationToken); + + return new IdpSamlAuthentication(credentials); + } + + @Override + public boolean supports(Class authentication) { + return SAMLAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandler.java new file mode 100644 index 00000000000..af884b1c6e9 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandler.java @@ -0,0 +1,125 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.namespace.QName; + +import org.cloudfoundry.identity.uaa.provider.saml.idp.IdpSamlAuthentication.IdpSamlCredentialsHolder; +import org.opensaml.common.SAMLException; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.RoleDescriptor; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.signature.SignatureException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml.SAMLAuthenticationToken; +import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.security.saml.metadata.MetadataManager; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.util.Assert; + +/** + * Use this class in conjunction with + * org.springframework.security.saml.SAMLProcessingFilter to create a SAML + * Response after SAMLProcessingFilter successfully processes a SAML + * Authentication Request. + */ +public class IdpSamlAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdpSamlAuthenticationSuccessHandler.class); + + private IdpWebSsoProfile idpWebSsoProfile; + private MetadataManager metadataManager; + + public IdpSamlAuthenticationSuccessHandler() { + super(); + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + IdpSamlCredentialsHolder credentials = (IdpSamlCredentialsHolder) authentication.getCredentials(); + Authentication loginAuthentication = credentials.getLoginAuthenticationToken(); + SAMLAuthenticationToken token = (SAMLAuthenticationToken) credentials.getSamlAuthenticationToken(); + SAMLMessageContext context = token.getCredentials(); + + IdpExtendedMetadata extendedMetadata = null; + try { + extendedMetadata = (IdpExtendedMetadata) metadataManager.getExtendedMetadata(context.getLocalEntityId()); + } catch (MetadataProviderException e) { + throw new ServletException("Failed to obtain local SAML IdP extended metadata.", e); + } + + try { + populatePeerContext(context); + } catch (MetadataProviderException e) { + throw new ServletException("Failed to populate peer SAML SP context.", e); + } + + try { + IdpWebSSOProfileOptions options = new IdpWebSSOProfileOptions(); + options.setAssertionsSigned(extendedMetadata.isAssertionsSigned()); + options.setAssertionTimeToLiveSeconds(extendedMetadata.getAssertionTimeToLiveSeconds()); + idpWebSsoProfile.sendResponse(loginAuthentication, context, options); + } catch (SAMLException e) { + LOGGER.debug("Incoming SAML message is invalid.", e); + throw new AuthenticationServiceException("Incoming SAML message is invalid.", e); + } catch (MetadataProviderException e) { + LOGGER.debug("Error determining metadata contracts.", e); + throw new AuthenticationServiceException("Error determining metadata contracts.", e); + } catch (MessageEncodingException e) { + LOGGER.debug("Error decoding incoming SAML message.", e); + throw new AuthenticationServiceException("Error encoding outgoing SAML message.", e); + } catch (MarshallingException | SecurityException | SignatureException e) { + LOGGER.debug("Error signing SAML assertion.", e); + throw new AuthenticationServiceException("Error signing SAML assertion.", e); + } + } + + protected void populatePeerContext(SAMLMessageContext samlContext) throws MetadataProviderException { + + String peerEntityId = samlContext.getPeerEntityId(); + QName peerEntityRole = samlContext.getPeerEntityRole(); + + if (peerEntityId == null) { + throw new MetadataProviderException("Peer entity ID wasn't specified, but is requested"); + } + + EntityDescriptor entityDescriptor = metadataManager.getEntityDescriptor(peerEntityId); + RoleDescriptor roleDescriptor = metadataManager.getRole(peerEntityId, peerEntityRole, SAMLConstants.SAML20P_NS); + ExtendedMetadata extendedMetadata = metadataManager.getExtendedMetadata(peerEntityId); + + if (entityDescriptor == null || roleDescriptor == null) { + throw new MetadataProviderException( + "Metadata for entity " + peerEntityId + " and role " + peerEntityRole + " wasn't found"); + } + + samlContext.setPeerEntityMetadata(entityDescriptor); + samlContext.setPeerEntityRoleMetadata(roleDescriptor); + samlContext.setPeerExtendedMetadata(extendedMetadata); + } + + @Autowired + public void setIdpWebSsoProfile(IdpWebSsoProfile idpWebSsoProfile) { + Assert.notNull(idpWebSsoProfile, "SAML Web SSO profile can't be null."); + this.idpWebSsoProfile = idpWebSsoProfile; + } + + @Autowired + public void setMetadataManager(MetadataManager metadataManager) { + Assert.notNull(metadataManager, "SAML metadata manager can't be null."); + this.metadataManager = metadataManager; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlContextProviderImpl.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlContextProviderImpl.java new file mode 100644 index 00000000000..be1a2a76c04 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlContextProviderImpl.java @@ -0,0 +1,26 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.opensaml.saml2.metadata.SPSSODescriptor; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.springframework.security.saml.context.SAMLContextProviderImpl; +import org.springframework.security.saml.context.SAMLMessageContext; + +/** + * Use this class in conjuction with + * org.springframework.security.saml.SAMLProcessingFilter to ensure that when + * SAMLProcessingFilter processes a SAML Authentication Request and builds a + * SAMLMessageContext it identifies the peer entity as a SAML SP. + */ +public class IdpSamlContextProviderImpl extends SAMLContextProviderImpl { + + @Override + public SAMLMessageContext getLocalEntity(HttpServletRequest request, HttpServletResponse response) + throws MetadataProviderException { + SAMLMessageContext context = super.getLocalEntity(request, response); + context.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME); + return context; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSSOProfileOptions.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSSOProfileOptions.java new file mode 100644 index 00000000000..c306987eaab --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSSOProfileOptions.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.springframework.security.saml.websso.WebSSOProfileOptions; + +/** + * DTO for SAML IdP Web SSO Profile options. + */ +public class IdpWebSSOProfileOptions extends WebSSOProfileOptions { + + /** + * Generated serialization id. + */ + private static final long serialVersionUID = 531377853538365709L; + + private boolean assertionsSigned; + private int assertionTimeToLiveSeconds; + + public boolean isAssertionsSigned() { + return assertionsSigned; + } + + public void setAssertionsSigned(boolean assertionsSigned) { + this.assertionsSigned = assertionsSigned; + } + + public int getAssertionTimeToLiveSeconds() { + return assertionTimeToLiveSeconds; + } + + public void setAssertionTimeToLiveSeconds(int assertionTimeToLiveSeconds) { + this.assertionTimeToLiveSeconds = assertionTimeToLiveSeconds; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfile.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfile.java new file mode 100644 index 00000000000..421f962fccf --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfile.java @@ -0,0 +1,20 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.opensaml.common.SAMLException; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.signature.SignatureException; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml.context.SAMLMessageContext; + +/** + * Interface for sending a SAML Response to SAML Service Provider during Web Single Sign-On profile. + */ +public interface IdpWebSsoProfile { + + void sendResponse(Authentication authentication, SAMLMessageContext context, IdpWebSSOProfileOptions options) + throws SAMLException, MetadataProviderException, MessageEncodingException, SecurityException, + MarshallingException, SignatureException; +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImpl.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImpl.java new file mode 100644 index 00000000000..87d10ab0950 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImpl.java @@ -0,0 +1,300 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.joda.time.DateTime; +import org.opensaml.Configuration; +import org.opensaml.common.SAMLException; +import org.opensaml.common.SAMLObjectBuilder; +import org.opensaml.common.SAMLVersion; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; +import org.opensaml.saml2.core.AttributeValue; +import org.opensaml.saml2.core.Audience; +import org.opensaml.saml2.core.AudienceRestriction; +import org.opensaml.saml2.core.AuthnContext; +import org.opensaml.saml2.core.AuthnContextClassRef; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.AuthnStatement; +import org.opensaml.saml2.core.Conditions; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.Status; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.Subject; +import org.opensaml.saml2.core.SubjectConfirmation; +import org.opensaml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml2.metadata.AssertionConsumerService; +import org.opensaml.saml2.metadata.Endpoint; +import org.opensaml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml2.metadata.SPSSODescriptor; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +import org.opensaml.xml.XMLObjectBuilder; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.schema.XSString; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.security.SecurityHelper; +import org.opensaml.xml.security.credential.Credential; +import org.opensaml.xml.signature.Signature; +import org.opensaml.xml.signature.SignatureException; +import org.opensaml.xml.signature.Signer; +import org.opensaml.xml.signature.impl.SignatureBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.websso.WebSSOProfileImpl; + +public class IdpWebSsoProfileImpl extends WebSSOProfileImpl implements IdpWebSsoProfile { + + @Override + public void sendResponse(Authentication authentication, SAMLMessageContext context, IdpWebSSOProfileOptions options) + throws SAMLException, MetadataProviderException, MessageEncodingException, SecurityException, + MarshallingException, SignatureException { + + buildResponse(authentication, context, options); + + sendMessage(context, false); + } + + @SuppressWarnings("unchecked") + protected void buildResponse(Authentication authentication, SAMLMessageContext context, + IdpWebSSOProfileOptions options) + throws MetadataProviderException, SecurityException, MarshallingException, SignatureException { + IDPSSODescriptor idpDescriptor = (IDPSSODescriptor) context.getLocalEntityRoleMetadata(); + SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getPeerEntityRoleMetadata(); + AuthnRequest authnRequest = (AuthnRequest) context.getInboundSAMLMessage(); + + AssertionConsumerService assertionConsumerService = getAssertionConsumerService(options, idpDescriptor, + spDescriptor); + + context.setPeerEntityEndpoint(assertionConsumerService); + + Assertion assertion = buildAssertion(authentication, authnRequest, options, context.getPeerEntityId(), + context.getLocalEntityId()); + if (options.isAssertionsSigned() || spDescriptor.getWantAssertionsSigned()) { + signAssertion(assertion, context.getLocalSigningCredential()); + } + Response samlResponse = createResponse(context, assertionConsumerService, assertion); + context.setOutboundMessage(samlResponse); + context.setOutboundSAMLMessage(samlResponse); + } + + private Response createResponse(SAMLMessageContext context, AssertionConsumerService assertionConsumerService, + Assertion assertion) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder responseBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Response.DEFAULT_ELEMENT_NAME); + Response response = responseBuilder.buildObject(); + + buildCommonAttributes(context.getLocalEntityId(), response, assertionConsumerService); + + response.getAssertions().add(assertion); + + buildStatusSuccess(response); + return response; + } + + private void buildCommonAttributes(String localEntityId, Response response, Endpoint service) { + + response.setID(generateID()); + response.setIssuer(getIssuer(localEntityId)); + response.setVersion(SAMLVersion.VERSION_20); + response.setIssueInstant(new DateTime()); + + if (service != null) { + response.setDestination(service.getLocation()); + } + } + + private Assertion buildAssertion(Authentication authentication, AuthnRequest authnRequest, + IdpWebSSOProfileOptions options, String audienceURI, String issuerEntityId) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder assertionBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Assertion.DEFAULT_ELEMENT_NAME); + Assertion assertion = assertionBuilder.buildObject(); + assertion.setID(generateID()); + assertion.setIssueInstant(new DateTime()); + assertion.setVersion(SAMLVersion.VERSION_20); + assertion.setIssuer(getIssuer(issuerEntityId)); + + buildAssertionAuthnStatement(assertion); + buildAssertionConditions(assertion, options.getAssertionTimeToLiveSeconds(), audienceURI); + buildAssertionSubject(assertion, authnRequest, options.getAssertionTimeToLiveSeconds(), + authentication.getName()); + buildAttributeStatement(assertion, authentication); + + return assertion; + } + + private void buildAssertionAuthnStatement(Assertion assertion) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder authnStatementBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(AuthnStatement.DEFAULT_ELEMENT_NAME); + AuthnStatement authnStatement = authnStatementBuilder.buildObject(); + authnStatement.setAuthnInstant(new DateTime()); + authnStatement.setSessionIndex(generateID()); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder authnContextBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(AuthnContext.DEFAULT_ELEMENT_NAME); + AuthnContext authnContext = authnContextBuilder.buildObject(); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder authnContextClassRefBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME); + AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(); + authnContextClassRef.setAuthnContextClassRef(AuthnContext.PASSWORD_AUTHN_CTX); + authnContext.setAuthnContextClassRef(authnContextClassRef); + authnStatement.setAuthnContext(authnContext); + assertion.getAuthnStatements().add(authnStatement); + } + + private void buildAssertionConditions(Assertion assertion, int assertionTtlSeconds, String audienceURI) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder conditionsBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Conditions.DEFAULT_ELEMENT_NAME); + Conditions conditions = conditionsBuilder.buildObject(); + conditions.setNotBefore(new DateTime()); + conditions.setNotOnOrAfter(new DateTime().plusSeconds(assertionTtlSeconds)); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder audienceRestrictionBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(AudienceRestriction.DEFAULT_ELEMENT_NAME); + AudienceRestriction audienceRestriction = audienceRestrictionBuilder.buildObject(); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder audienceBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Audience.DEFAULT_ELEMENT_NAME); + Audience audience = audienceBuilder.buildObject(); + audience.setAudienceURI(audienceURI); + audienceRestriction.getAudiences().add(audience); + conditions.getAudienceRestrictions().add(audienceRestriction); + assertion.setConditions(conditions); + } + + private void buildAssertionSubject(Assertion assertion, AuthnRequest authnRequest, int assertionTtlSeconds, + String nameIdStr) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder subjectBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Subject.DEFAULT_ELEMENT_NAME); + Subject subject = subjectBuilder.buildObject(); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder nameIdBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(NameID.DEFAULT_ELEMENT_NAME); + NameID nameId = nameIdBuilder.buildObject(); + nameId.setValue(nameIdStr); + nameId.setFormat(NameIDType.UNSPECIFIED); + subject.setNameID(nameId); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder subjectConfirmationBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(SubjectConfirmation.DEFAULT_ELEMENT_NAME); + SubjectConfirmation subjectConfirmation = subjectConfirmationBuilder.buildObject(); + subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder subjectConfirmationDataBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(SubjectConfirmationData.DEFAULT_ELEMENT_NAME); + SubjectConfirmationData subjectConfirmationData = subjectConfirmationDataBuilder.buildObject(); + + subjectConfirmationData.setNotOnOrAfter(new DateTime().plusSeconds(assertionTtlSeconds)); + subjectConfirmationData.setInResponseTo(authnRequest.getID()); + subjectConfirmationData.setRecipient(authnRequest.getAssertionConsumerServiceURL()); + subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData); + subject.getSubjectConfirmations().add(subjectConfirmation); + assertion.setSubject(subject); + } + + private void buildAttributeStatement(Assertion assertion, Authentication authentication) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder attributeStatementBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(AttributeStatement.DEFAULT_ELEMENT_NAME); + AttributeStatement attributeStatement = attributeStatementBuilder.buildObject(); + + List authorities = new ArrayList<>(); + for (GrantedAuthority authority : authentication.getAuthorities()) { + authorities.add(authority.getAuthority()); + } + Attribute authoritiesAttribute = buildStringAttribute("authorities", authorities); + attributeStatement.getAttributes().add(authoritiesAttribute); + + UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); + Attribute emailAttribute = buildStringAttribute("email", Arrays.asList(new String[] { principal.getEmail() })); + attributeStatement.getAttributes().add(emailAttribute); + Attribute idAttribute = buildStringAttribute("email", Arrays.asList(new String[] { principal.getId() })); + attributeStatement.getAttributes().add(idAttribute); + Attribute nameAttribute = buildStringAttribute("name", Arrays.asList(new String[] { principal.getName() })); + attributeStatement.getAttributes().add(nameAttribute); + Attribute originAttribute = buildStringAttribute("name", Arrays.asList(new String[] { principal.getOrigin() })); + attributeStatement.getAttributes().add(originAttribute); + Attribute zoneAttribute = buildStringAttribute("name", Arrays.asList(new String[] { principal.getZoneId() })); + attributeStatement.getAttributes().add(zoneAttribute); + + assertion.getAttributeStatements().add(attributeStatement); + } + + public Attribute buildStringAttribute(String name, List values) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder attributeBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Attribute.DEFAULT_ELEMENT_NAME); + Attribute attribute = (Attribute) attributeBuilder.buildObject(); + attribute.setName(name); + + @SuppressWarnings("unchecked") + XMLObjectBuilder xsStringBuilder = (XMLObjectBuilder) builderFactory + .getBuilder(XSString.TYPE_NAME); + for (String value : values) { + // Set custom Attributes + XSString attributeValue = xsStringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, + XSString.TYPE_NAME); + attributeValue.setValue(value); + attribute.getAttributeValues().add(attributeValue); + } + + return attribute; + } + + private void buildStatusSuccess(Response response) { + buildStatus(response, StatusCode.SUCCESS_URI); + } + + private void buildStatus(Response response, String statusCodeStr) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder statusCodeBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(StatusCode.DEFAULT_ELEMENT_NAME); + StatusCode statusCode = statusCodeBuilder.buildObject(); + statusCode.setValue(statusCodeStr); + + @SuppressWarnings("unchecked") + SAMLObjectBuilder statusBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Status.DEFAULT_ELEMENT_NAME); + Status status = statusBuilder.buildObject(); + status.setStatusCode(statusCode); + response.setStatus(status); + } + + private void signAssertion(Assertion assertion, Credential credential) + throws SecurityException, MarshallingException, SignatureException { + SignatureBuilder signatureBuilder = (SignatureBuilder) builderFactory + .getBuilder(Signature.DEFAULT_ELEMENT_NAME); + Signature signature = signatureBuilder.buildObject(); + signature.setSigningCredential(credential); + + SecurityHelper.prepareSignatureParams(signature, credential, null, null); + assertion.setSignature(signature); + + Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(assertion); + marshaller.marshall(assertion); + + Signer.signObject(signature); + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioning.java new file mode 100644 index 00000000000..5e13d23e991 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioning.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.ObjectUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Rest-template-based data access for SAML Service Provider CRUD operations. + */ +public class JdbcSamlServiceProviderProvisioning implements SamlServiceProviderProvisioning, SamlServiceProviderDeletable { + + private static final Log LOGGER = LogFactory.getLog(JdbcIdentityProviderProvisioning.class); + + public static final String SERVICE_PROVIDER_FIELDS = "id,version,created,lastmodified,name,entity_id,config,identity_zone_id,active"; + + public static final String CREATE_SERVICE_PROVIDER_SQL = "insert into service_provider(" + SERVICE_PROVIDER_FIELDS + + ") values (?,?,?,?,?,?,?,?,?)"; + + public static final String DELETE_SERVICE_PROVIDER_SQL = "delete from service_provider where id=? and identity_zone_id=?"; + + public static final String DELETE_SERVICE_PROVIDER_BY_ENTITY_ID_SQL = "delete from service_provider where entity_id = ? and identity_zone_id=?"; + + public static final String DELETE_SERVICE_PROVIDER_BY_ZONE_SQL = "delete from service_provider where identity_zone_id=?"; + + public static final String SERVICE_PROVIDERS_QUERY = "select " + SERVICE_PROVIDER_FIELDS + + " from service_provider where identity_zone_id=?"; + + public static final String ACTIVE_SERVICE_PROVIDERS_QUERY = SERVICE_PROVIDERS_QUERY + " and active"; + + public static final String SERVICE_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,config,active".replace(",", + "=?,") + "=?"; + + public static final String UPDATE_SERVICE_PROVIDER_SQL = "update service_provider set " + + SERVICE_PROVIDER_UPDATE_FIELDS + " where id=? and identity_zone_id=?"; + + public static final String SERVICE_PROVIDER_BY_ID_QUERY = "select " + SERVICE_PROVIDER_FIELDS + + " from service_provider " + "where id=? and identity_zone_id=?"; + + public static final String SERVICE_PROVIDER_BY_ENTITY_ID_QUERY = "select " + SERVICE_PROVIDER_FIELDS + + " from service_provider " + "where entity_id=? and identity_zone_id=? "; + + protected final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = new SamlServiceProviderRowMapper(); + + public JdbcSamlServiceProviderProvisioning(JdbcTemplate jdbcTemplate) { + Assert.notNull(jdbcTemplate); + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public SamlServiceProvider retrieve(String id) { + SamlServiceProvider serviceProvider = jdbcTemplate.queryForObject(SERVICE_PROVIDER_BY_ID_QUERY, mapper, id, + IdentityZoneHolder.get().getId()); + return serviceProvider; + } + + @Override + public void delete(String id) { + jdbcTemplate.update(DELETE_SERVICE_PROVIDER_SQL, id, IdentityZoneHolder.get().getId()); + } + + @Override + public int deleteByEntityId(String entityId, String zoneId) { + return jdbcTemplate.update(DELETE_SERVICE_PROVIDER_BY_ENTITY_ID_SQL, entityId, zoneId); + } + + @Override + public int deleteByIdentityZone(String zoneId) { + return jdbcTemplate.update(DELETE_SERVICE_PROVIDER_BY_ZONE_SQL, zoneId); + } + + @Override + public List retrieveActive(String zoneId) { + return jdbcTemplate.query(ACTIVE_SERVICE_PROVIDERS_QUERY, mapper, zoneId); + } + + @Override + public List retrieveAll(boolean activeOnly, String zoneId) { + if (activeOnly) { + return retrieveActive(zoneId); + } else { + return jdbcTemplate.query(SERVICE_PROVIDERS_QUERY, mapper, zoneId); + } + } + + @Override + public SamlServiceProvider retrieveByEntityId(String entityId, String zoneId) { + SamlServiceProvider serviceProvider = jdbcTemplate.queryForObject(SERVICE_PROVIDER_BY_ENTITY_ID_QUERY, mapper, + entityId, zoneId); + return serviceProvider; + } + + @Override + public SamlServiceProvider create(final SamlServiceProvider serviceProvider) { + validate(serviceProvider); + final String id = UUID.randomUUID().toString(); + try { + jdbcTemplate.update(CREATE_SERVICE_PROVIDER_SQL, new PreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps) throws SQLException { + int pos = 1; + ps.setString(pos++, id); + ps.setInt(pos++, serviceProvider.getVersion()); + ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); + ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); + ps.setString(pos++, serviceProvider.getName()); + ps.setString(pos++, serviceProvider.getEntityId()); + ps.setString(pos++, JsonUtils.writeValueAsString(serviceProvider.getConfig())); + ps.setString(pos++, serviceProvider.getIdentityZoneId()); + ps.setBoolean(pos++, serviceProvider.isActive()); + } + }); + } catch (DuplicateKeyException e) { + throw new SamlSpAlreadyExistsException(e.getMostSpecificCause().getMessage()); + } + return retrieve(id); + } + + @Override + public SamlServiceProvider update(final SamlServiceProvider serviceProvider) { + validate(serviceProvider); + final String zoneId = IdentityZoneHolder.get().getId(); + jdbcTemplate.update(UPDATE_SERVICE_PROVIDER_SQL, new PreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps) throws SQLException { + int pos = 1; + ps.setInt(pos++, serviceProvider.getVersion() + 1); + ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); + ps.setString(pos++, serviceProvider.getName()); + ps.setString(pos++, JsonUtils.writeValueAsString(serviceProvider.getConfig())); + ps.setBoolean(pos++, serviceProvider.isActive()); + ps.setString(pos++, serviceProvider.getId().trim()); + ps.setString(pos++, zoneId); + } + }); + return retrieve(serviceProvider.getId()); + } + + protected void validate(SamlServiceProvider provider) { + if (provider == null) { + throw new NullPointerException("SAML Service Provider can not be null."); + } + if (!StringUtils.hasText(provider.getIdentityZoneId())) { + throw new DataIntegrityViolationException("Identity zone ID must be set."); + } + + SamlServiceProviderDefinition saml = ObjectUtils.castInstance(provider.getConfig(), + SamlServiceProviderDefinition.class); + saml.setSpEntityId(provider.getEntityId()); + saml.setZoneId(provider.getIdentityZoneId()); + provider.setConfig(saml); + } + + private static final class SamlServiceProviderRowMapper implements RowMapper { + public SamlServiceProviderRowMapper() { + // Default constructor. + } + + @Override + public SamlServiceProvider mapRow(ResultSet rs, int rowNum) throws SQLException { + SamlServiceProvider samlServiceProvider = new SamlServiceProvider(); + int pos = 1; + samlServiceProvider.setId(rs.getString(pos++).trim()); + samlServiceProvider.setVersion(rs.getInt(pos++)); + samlServiceProvider.setCreated(rs.getTimestamp(pos++)); + samlServiceProvider.setLastModified(rs.getTimestamp(pos++)); + samlServiceProvider.setName(rs.getString(pos++)); + samlServiceProvider.setEntityId(rs.getString(pos++)); + String config = rs.getString(pos++); + SamlServiceProviderDefinition definition = JsonUtils.readValue(config, SamlServiceProviderDefinition.class); + samlServiceProvider.setConfig(definition); + samlServiceProvider.setIdentityZoneId(rs.getString(pos++)); + samlServiceProvider.setActive(rs.getBoolean(pos++)); + return samlServiceProvider; + } + } + + @Override + public Log getLogger() { + + return LOGGER; + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProvider.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProvider.java new file mode 100644 index 00000000000..9b4bd4e55d8 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProvider.java @@ -0,0 +1,307 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.io.IOException; +import java.util.Date; + +import javax.validation.constraints.NotNull; + +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@JsonSerialize(using = SamlServiceProvider.SamlServiceProviderSerializer.class) +@JsonDeserialize(using = SamlServiceProvider.SamlServiceProviderDeserializer.class) +public class SamlServiceProvider { + + public static final String FIELD_ID = "id"; + public static final String FIELD_ENTITY_ID = "entityId"; + public static final String FIELD_NAME = "name"; + public static final String FIELD_VERSION = "version"; + public static final String FIELD_CREATED = "created"; + public static final String FIELD_LAST_MODIFIED = "lastModified"; + public static final String FIELD_ACTIVE = "active"; + public static final String FIELD_IDENTITY_ZONE_ID = "identityZoneId"; + public static final String FIELD_CONFIG = "config"; + + // see deserializer at the bottom + private String id; + @NotNull + private String entityId; + @NotNull + private String name; + private SamlServiceProviderDefinition config; + private int version = 0; + private Date created = new Date(); + private Date lastModified = new Date(); + private boolean active = true; + private String identityZoneId; + + public Date getCreated() { + return created; + } + + public SamlServiceProvider setCreated(Date created) { + this.created = created; + return this; + } + + public Date getLastModified() { + return lastModified; + } + + public SamlServiceProvider setLastModified(Date lastModified) { + this.lastModified = lastModified; + return this; + } + + public SamlServiceProvider setVersion(int version) { + this.version = version; + return this; + } + + public int getVersion() { + return version; + } + + public String getName() { + return name; + } + + public SamlServiceProvider setName(String name) { + this.name = name; + return this; + } + + public String getId() { + return id; + } + + public SamlServiceProvider setId(String id) { + this.id = id; + return this; + } + + public SamlServiceProviderDefinition getConfig() { + return config; + } + + public SamlServiceProvider setConfig(SamlServiceProviderDefinition config) { + + if (StringUtils.hasText(getEntityId())) { + config.setSpEntityId(getEntityId()); + } + if (StringUtils.hasText(getIdentityZoneId())) { + config.setZoneId(getIdentityZoneId()); + } + this.config = config; + return this; + } + + public String getEntityId() { + return entityId; + } + + public SamlServiceProvider setEntityId(String entityId) { + this.entityId = entityId; + if (config != null) { + config.setSpEntityId(entityId); + } + return this; + } + + public boolean isActive() { + return active; + } + + public SamlServiceProvider setActive(boolean active) { + this.active = active; + return this; + } + + public String getIdentityZoneId() { + return identityZoneId; + } + + public SamlServiceProvider setIdentityZoneId(String identityZoneId) { + this.identityZoneId = identityZoneId; + if (config != null) { + config.setZoneId(identityZoneId); + } + return this; + } + + public boolean configIsValid() { + // There may be need for this method in the fugure but for now it does nothing. + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((config == null) ? 0 : config.hashCode()); + result = prime * result + ((created == null) ? 0 : created.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((lastModified == null) ? 0 : lastModified.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((entityId == null) ? 0 : entityId.hashCode()); + result = prime * result + version; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SamlServiceProvider other = (SamlServiceProvider) obj; + if (config == null) { + if (other.config != null) + return false; + } else if (!config.equals(other.config)) + return false; + if (created == null) { + if (other.created != null) + return false; + } else if (!created.equals(other.created)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (lastModified == null) { + if (other.lastModified != null) + return false; + } else if (!lastModified.equals(other.lastModified)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (entityId == null) { + if (other.entityId != null) + return false; + } else if (!entityId.equals(other.entityId)) + return false; + if (version != other.version) + return false; + return true; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("SamlServiceProvider{"); + sb.append("id='").append(id).append('\''); + sb.append(", entityId='").append(entityId).append('\''); + sb.append(", name='").append(name).append('\''); + sb.append(", active=").append(active); + sb.append('}'); + return sb.toString(); + } + + public static class SamlServiceProviderSerializer extends JsonSerializer { + @Override + public void serialize(SamlServiceProvider value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { + gen.writeStartObject(); + gen.writeStringField(FIELD_CONFIG, JsonUtils.writeValueAsString(value.getConfig())); + gen.writeStringField(FIELD_ID, value.getId()); + gen.writeStringField(FIELD_ENTITY_ID, value.getEntityId()); + gen.writeStringField(FIELD_NAME, value.getName()); + gen.writeNumberField(FIELD_VERSION, value.getVersion()); + writeDateField(FIELD_CREATED, value.getCreated(), gen); + writeDateField(FIELD_LAST_MODIFIED, value.getLastModified(), gen); + gen.writeBooleanField(FIELD_ACTIVE, value.isActive()); + gen.writeStringField(FIELD_IDENTITY_ZONE_ID, value.getIdentityZoneId()); + gen.writeEndObject(); + } + + public void writeDateField(String fieldName, Date value, JsonGenerator gen) throws IOException { + if (value != null) { + gen.writeNumberField(fieldName, value.getTime()); + } else { + gen.writeNullField(fieldName); + } + } + } + + public static class SamlServiceProviderDeserializer extends JsonDeserializer { + @Override + public SamlServiceProvider deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + SamlServiceProvider result = new SamlServiceProvider(); + // determine the type of IdentityProvider + JsonNode node = JsonUtils.readTree(jp); + // deserialize based on type + String config = getNodeAsString(node, FIELD_CONFIG, null); + SamlServiceProviderDefinition definition = null; + if (StringUtils.hasText(config)) { + definition = JsonUtils.readValue(config, SamlServiceProviderDefinition.class); + } + result.setConfig(definition); + + result.setId(getNodeAsString(node, FIELD_ID, null)); + result.setEntityId(getNodeAsString(node, FIELD_ENTITY_ID, null)); + result.setName(getNodeAsString(node, FIELD_NAME, null)); + result.setVersion(getNodeAsInt(node, FIELD_VERSION, 0)); + result.setCreated(getNodeAsDate(node, FIELD_CREATED)); + result.setLastModified(getNodeAsDate(node, FIELD_LAST_MODIFIED)); + result.setActive(getNodeAsBoolean(node, FIELD_ACTIVE, true)); + result.setIdentityZoneId(getNodeAsString(node, FIELD_IDENTITY_ZONE_ID, null)); + return result; + } + + protected String getNodeAsString(JsonNode node, String fieldName, String defaultValue) { + JsonNode typeNode = node.get(fieldName); + return typeNode == null ? defaultValue : typeNode.asText(defaultValue); + } + + protected int getNodeAsInt(JsonNode node, String fieldName, int defaultValue) { + JsonNode typeNode = node.get(fieldName); + return typeNode == null ? defaultValue : typeNode.asInt(defaultValue); + } + + protected boolean getNodeAsBoolean(JsonNode node, String fieldName, boolean defaultValue) { + JsonNode typeNode = node.get(fieldName); + return typeNode == null ? defaultValue : typeNode.asBoolean(defaultValue); + } + + protected Date getNodeAsDate(JsonNode node, String fieldName) { + JsonNode typeNode = node.get(fieldName); + long date = typeNode == null ? -1 : typeNode.asLong(-1); + if (date == -1) { + return null; + } else { + return new Date(date); + } + } + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderChangedListener.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderChangedListener.java new file mode 100644 index 00000000000..0680e4101f2 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderChangedListener.java @@ -0,0 +1,79 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.util.ObjectUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.event.ServiceProviderModifiedEvent; +import org.opensaml.saml2.metadata.provider.MetadataProvider; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.springframework.context.ApplicationListener; +import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; + +/** + * Listens to SAML service provider modified events from the RESTful service controller and updates the internal state and data persistence as necessary. + */ +public class SamlServiceProviderChangedListener implements ApplicationListener { + + private static final Log logger = LogFactory.getLog(SamlServiceProviderChangedListener.class); + private ZoneAwareIdpMetadataManager metadataManager = null; + private final SamlServiceProviderConfigurator configurator; + private final IdentityZoneProvisioning zoneProvisioning; + + public SamlServiceProviderChangedListener(SamlServiceProviderConfigurator configurator, + IdentityZoneProvisioning zoneProvisioning) { + this.configurator = configurator; + this.zoneProvisioning = zoneProvisioning; + } + + @Override + public void onApplicationEvent(ServiceProviderModifiedEvent event) { + if (metadataManager == null) { + return; + } + SamlServiceProvider changedSamlServiceProvider = (SamlServiceProvider) event.getSource(); + IdentityZone zone = zoneProvisioning.retrieve(changedSamlServiceProvider.getIdentityZoneId()); + ZoneAwareIdpMetadataManager.ExtensionMetadataManager manager = metadataManager.getManager(zone); + SamlServiceProviderDefinition definition = ObjectUtils.castInstance(changedSamlServiceProvider.getConfig(), + SamlServiceProviderDefinition.class); + try { + if (changedSamlServiceProvider.isActive()) { + ExtendedMetadataDelegate[] delegates = configurator.addSamlServiceProviderDefinition(definition); + if (delegates[1] != null) { + manager.removeMetadataProvider(delegates[1]); + } + manager.addMetadataProvider(delegates[0]); + } else { + ExtendedMetadataDelegate delegate = configurator.removeSamlServiceProviderDefinition(definition); + if (delegate != null) { + manager.removeMetadataProvider(delegate); + } + } + for (MetadataProvider provider : manager.getProviders()) { + provider.getMetadata(); + } + manager.refreshMetadata(); + metadataManager.getManager(zone).refreshMetadata(); + } catch (MetadataProviderException e) { + logger.error("Unable to add new SAML service provider:" + definition, e); + } + } + + public void setMetadataManager(ZoneAwareIdpMetadataManager metadataManager) { + this.metadataManager = metadataManager; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java new file mode 100644 index 00000000000..4fa67ed43c2 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.SimpleHttpConnectionManager; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.http.client.utils.URIBuilder; +import org.cloudfoundry.identity.uaa.provider.saml.ConfigMetadataProvider; +import org.cloudfoundry.identity.uaa.provider.saml.FixedHttpMetaDataProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.parse.BasicParserPool; +import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; +import org.springframework.util.StringUtils; + +/** + * Holds internal state of available SAML Service Providers. + */ +public class SamlServiceProviderConfigurator { + private Map serviceProviders = new HashMap<>(); + private HttpClientParams clientParams; + private BasicParserPool parserPool; + + private Timer dummyTimer = new Timer() { + + @Override + public void cancel() { + super.cancel(); + } + + @Override + public int purge() { + return 0; + } + + @Override + public void schedule(TimerTask task, long delay) { + // Do nothing. + } + + @Override + public void schedule(TimerTask task, long delay, long period) { + // Do nothing. + } + + @Override + public void schedule(TimerTask task, Date firstTime, long period) { + // Do nothing. + } + + @Override + public void schedule(TimerTask task, Date time) { + // Do nothing. + } + + @Override + public void scheduleAtFixedRate(TimerTask task, long delay, long period) { + // Do nothing. + } + + @Override + public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { + // Do nothing. + } + }; + + public SamlServiceProviderConfigurator() { + dummyTimer.cancel(); + } + + public List getSamlServiceProviderDefinitions() { + return Collections.unmodifiableList(new ArrayList<>(serviceProviders.keySet())); + } + + public List getSamlServiceProviderDefinitionsForZone(IdentityZone zone) { + List result = new LinkedList<>(); + for (SamlServiceProviderDefinition def : getSamlServiceProviderDefinitions()) { + if (zone.getId().equals(def.getZoneId())) { + result.add(def); + } + } + return result; + } + + public List getSamlServiceProviderDefinitions(List allowedSps, + IdentityZone zone) { + List spsInTheZone = getSamlServiceProviderDefinitionsForZone(zone); + if (allowedSps != null) { + List result = new LinkedList<>(); + for (SamlServiceProviderDefinition def : spsInTheZone) { + if (allowedSps.contains(def.getSpEntityId())) { + result.add(def); + } + } + return result; + } + return spsInTheZone; + } + + protected String getUniqueAlias(SamlServiceProviderDefinition def) { + return def.getUniqueAlias(); + } + + /** + * adds or replaces a SAML service provider + * + * @param providerDefinition + * - the provider to be added + * @return an array consisting of {provider-added, provider-deleted} where provider-deleted may be null + * @throws MetadataProviderException + * if the system fails to fetch meta data for this provider + */ + public synchronized ExtendedMetadataDelegate[] addSamlServiceProviderDefinition( + SamlServiceProviderDefinition providerDefinition) throws MetadataProviderException { + ExtendedMetadataDelegate added, deleted = null; + if (providerDefinition == null) { + throw new NullPointerException(); + } + if (!StringUtils.hasText(providerDefinition.getSpEntityId())) { + throw new NullPointerException("You must set the SAML SP Entity."); + } + if (!StringUtils.hasText(providerDefinition.getZoneId())) { + throw new NullPointerException("You must set the SAML SP Identity Zone Id."); + } + for (SamlServiceProviderDefinition def : getSamlServiceProviderDefinitions()) { + if (getUniqueAlias(providerDefinition).equals(getUniqueAlias(def))) { + deleted = serviceProviders.remove(def); + break; + } + } + SamlServiceProviderDefinition clone = providerDefinition.clone(); + added = getExtendedMetadataDelegate(clone); + String entityIdToBeAdded = ((ConfigMetadataProvider) added.getDelegate()).getEntityID(); + boolean entityIDexists = false; + for (Map.Entry entry : serviceProviders.entrySet()) { + SamlServiceProviderDefinition definition = entry.getKey(); + if (clone.getZoneId().equals(definition.getZoneId())) { + ConfigMetadataProvider provider = (ConfigMetadataProvider) entry.getValue().getDelegate(); + if (entityIdToBeAdded.equals(provider.getEntityID())) { + entityIDexists = true; + break; + } + } + } + if (entityIDexists) { + throw new MetadataProviderException("Duplicate entity id:" + entityIdToBeAdded); + } + + serviceProviders.put(clone, added); + return new ExtendedMetadataDelegate[] { added, deleted }; + } + + public synchronized ExtendedMetadataDelegate removeSamlServiceProviderDefinition( + SamlServiceProviderDefinition providerDefinition) { + return serviceProviders.remove(providerDefinition); + } + + public List getSamlServiceProviders() { + return getSamlServiceProviders(null); + } + + public List getSamlServiceProviders(IdentityZone zone) { + List result = new LinkedList<>(); + for (SamlServiceProviderDefinition def : getSamlServiceProviderDefinitions()) { + if (zone == null || zone.getId().equals(def.getZoneId())) { + ExtendedMetadataDelegate metadata = serviceProviders.get(def); + if (metadata != null) { + result.add(metadata); + } + } + } + return result; + } + + public ExtendedMetadataDelegate getExtendedMetadataDelegateFromCache(SamlServiceProviderDefinition def) + throws MetadataProviderException { + return serviceProviders.get(def); + } + + public ExtendedMetadataDelegate getExtendedMetadataDelegate(SamlServiceProviderDefinition def) + throws MetadataProviderException { + ExtendedMetadataDelegate metadata; + switch (def.getType()) { + case DATA: { + metadata = configureXMLMetadata(def); + break; + } + case URL: { + metadata = configureURLMetadata(def); + break; + } + default: { + throw new MetadataProviderException( + "Invalid metadata type for alias[" + def.getSpEntityId() + "]:" + def.getMetaDataLocation()); + } + } + return metadata; + } + + protected ExtendedMetadataDelegate configureXMLMetadata(SamlServiceProviderDefinition def) { + ConfigMetadataProvider configMetadataProvider = new ConfigMetadataProvider(def.getZoneId(), def.getSpEntityId(), + def.getMetaDataLocation()); + configMetadataProvider.setParserPool(getParserPool()); + ExtendedMetadata extendedMetadata = new ExtendedMetadata(); + extendedMetadata.setLocal(false); + extendedMetadata.setAlias(def.getSpEntityId()); + ExtendedMetadataDelegate delegate = new ExtendedMetadataDelegate(configMetadataProvider, extendedMetadata); + delegate.setMetadataTrustCheck(def.isMetadataTrustCheck()); + + return delegate; + } + + @SuppressWarnings("unchecked") + protected ExtendedMetadataDelegate configureURLMetadata(SamlServiceProviderDefinition def) + throws MetadataProviderException { + Class socketFactory = null; + try { + def = def.clone(); + socketFactory = (Class) Class.forName(def.getSocketFactoryClassName()); + ExtendedMetadata extendedMetadata = new ExtendedMetadata(); + extendedMetadata.setAlias(def.getSpEntityId()); + SimpleHttpConnectionManager connectionManager = new SimpleHttpConnectionManager(true); + connectionManager.getParams().setDefaults(getClientParams()); + HttpClient client = new HttpClient(connectionManager); + FixedHttpMetaDataProvider fixedHttpMetaDataProvider = new FixedHttpMetaDataProvider(dummyTimer, client, + adjustURIForPort(def.getMetaDataLocation())); + fixedHttpMetaDataProvider.setSocketFactory(socketFactory.newInstance()); + byte[] metadata = fixedHttpMetaDataProvider.fetchMetadata(); + def.setMetaDataLocation(new String(metadata, StandardCharsets.UTF_8)); + return configureXMLMetadata(def); + } catch (URISyntaxException e) { + throw new MetadataProviderException("Invalid socket factory(invalid URI):" + def.getMetaDataLocation(), e); + } catch (ClassNotFoundException e) { + throw new MetadataProviderException("Invalid socket factory:" + def.getSocketFactoryClassName(), e); + } catch (InstantiationException e) { + throw new MetadataProviderException("Invalid socket factory:" + def.getSocketFactoryClassName(), e); + } catch (IllegalAccessException e) { + throw new MetadataProviderException("Invalid socket factory:" + def.getSocketFactoryClassName(), e); + } + } + + protected String adjustURIForPort(String uri) throws URISyntaxException { + URI metadataURI = new URI(uri); + if (metadataURI.getPort() < 0) { + switch (metadataURI.getScheme()) { + case "https": + return new URIBuilder(uri).setPort(443).build().toString(); + case "http": + return new URIBuilder(uri).setPort(80).build().toString(); + default: + return uri; + } + } + return uri; + } + + public HttpClientParams getClientParams() { + return clientParams; + } + + public void setClientParams(HttpClientParams clientParams) { + this.clientParams = clientParams; + } + + public BasicParserPool getParserPool() { + return parserPool; + } + + public void setParserPool(BasicParserPool parserPool) { + this.parserPool = parserPool; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinition.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinition.java new file mode 100644 index 00000000000..13fb302aced --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinition.java @@ -0,0 +1,294 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.io.IOException; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class SamlServiceProviderDefinition { + + public static final String DEFAULT_HTTP_SOCKET_FACTORY = "org.apache.commons.httpclient.protocol.DefaultProtocolSocketFactory"; + public static final String DEFAULT_HTTPS_SOCKET_FACTORY = "org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory"; + + public enum MetadataLocation { + URL, + DATA, + UNKNOWN + } + + private String metaDataLocation; + private String spEntityId; + private String zoneId; + private String nameID; + private int singleSignOnServiceIndex; + private boolean metadataTrustCheck; + private String socketFactoryClassName; + + public SamlServiceProviderDefinition clone() { + return new SamlServiceProviderDefinition(metaDataLocation, + spEntityId, + nameID, + singleSignOnServiceIndex, + metadataTrustCheck, + zoneId); + } + + public SamlServiceProviderDefinition() {} + + public SamlServiceProviderDefinition(String metaDataLocation, + String spEntityAlias, + String nameID, + int singleSignOnServiceIndex, + boolean metadataTrustCheck, + String zoneId) { + this.metaDataLocation = metaDataLocation; + this.spEntityId = spEntityAlias; + this.nameID = nameID; + this.singleSignOnServiceIndex = singleSignOnServiceIndex; + this.metadataTrustCheck = metadataTrustCheck; + this.zoneId = zoneId; + } + + @JsonIgnore + public MetadataLocation getType() { + String trimmedLocation = metaDataLocation.trim(); + if (trimmedLocation.startsWith("0) { + return socketFactoryClassName; + } + if (getMetaDataLocation()==null || getMetaDataLocation().trim().length()==0) { + throw new IllegalStateException("Invalid meta data URL[" + getMetaDataLocation() + "] cannot determine socket factory."); + } + if (getMetaDataLocation().startsWith("https")) { + return DEFAULT_HTTPS_SOCKET_FACTORY; + } else { + return DEFAULT_HTTP_SOCKET_FACTORY; + } + } + + public void setSocketFactoryClassName(String socketFactoryClassName) { + this.socketFactoryClassName = socketFactoryClassName; + if (socketFactoryClassName!=null && socketFactoryClassName.trim().length()>0) { + try { + Class.forName( + socketFactoryClassName, + true, + Thread.currentThread().getContextClassLoader() + ); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } catch (ClassCastException e) { + throw new IllegalArgumentException(e); + } + } + } + + public String getZoneId() { + return zoneId; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SamlServiceProviderDefinition that = (SamlServiceProviderDefinition) o; + + return Objects.equals(getUniqueAlias(), that.getUniqueAlias()); + } + + @Override + public int hashCode() { + String alias = getUniqueAlias(); + return alias==null ? 0 : alias.hashCode(); + } + + @JsonIgnore + protected String getUniqueAlias() { + return getSpEntityId()+"###"+getZoneId(); + } + + @Override + public String toString() { + return "SamlServiceProviderDefinition{" + + "spEntityAlias='" + spEntityId + '\'' + + ", metaDataLocation='" + metaDataLocation + '\'' + + ", nameID='" + nameID + '\'' + + ", singleSignOnServiceIndex=" + singleSignOnServiceIndex + + ", metadataTrustCheck=" + metadataTrustCheck + + ", socketFactoryClassName='" + socketFactoryClassName + '\'' + + ", zoneId='" + zoneId + '\'' + + '}'; + } + + public static class Builder { + + private String metaDataLocation; + private String spEntityId; + private String zoneId; + private String nameID; + private int singleSignOnServiceIndex; + private boolean metadataTrustCheck; + private String socketFactoryClassName; + + private Builder(){} + + public static Builder get() { + return new Builder(); + } + + public SamlServiceProviderDefinition build() { + SamlServiceProviderDefinition def = new SamlServiceProviderDefinition(); + + def.setMetaDataLocation(metaDataLocation); + def.setSpEntityId(spEntityId); + def.setZoneId(zoneId); + def.setNameID(nameID); + def.setSingleSignOnServiceIndex(singleSignOnServiceIndex); + def.setMetadataTrustCheck(metadataTrustCheck); + def.setSocketFactoryClassName(socketFactoryClassName); + return def; + } + + public Builder setMetaDataLocation(String metaDataLocation) { + this.metaDataLocation = metaDataLocation; + return this; + } + + public Builder setSpEntityId(String spEntityId) { + this.spEntityId = spEntityId; + return this; + } + + public Builder setZoneId(String zoneId) { + this.zoneId = zoneId; + return this; + } + + public Builder setNameID(String nameID) { + this.nameID = nameID; + return this; + } + + public Builder setSingleSignOnServiceIndex(int singleSignOnServiceIndex) { + this.singleSignOnServiceIndex = singleSignOnServiceIndex; + return this; + } + + public Builder setMetadataTrustCheck(boolean metadataTrustCheck) { + this.metadataTrustCheck = metadataTrustCheck; + return this; + } + + public Builder setSocketFactoryClassName(String socketFactoryClassName) { + this.socketFactoryClassName = socketFactoryClassName; + return this; + } + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDeletable.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDeletable.java new file mode 100644 index 00000000000..8638f595548 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDeletable.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.apache.commons.logging.Log; +import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.springframework.context.ApplicationListener; + +/** + * Handles SAML service provider deleted events. + */ +public interface SamlServiceProviderDeletable extends ApplicationListener> { + default void onApplicationEvent(EntityDeletedEvent event) { + if (event==null || event.getDeleted()==null) { + return; + } else if (event.getDeleted() instanceof SamlServiceProvider) { + String entityId = ((SamlServiceProvider)event.getDeleted()).getEntityId(); + String zoneId = ((SamlServiceProvider)event.getDeleted()).getIdentityZoneId(); + deleteByEntityId(entityId, zoneId); + } else { + getLogger().debug("Unsupported deleted event for deletion of object:"+event.getDeleted()); + } + } + + default boolean isUaaZone(String zoneId) { + return IdentityZone.getUaa().getId().equals(zoneId); + } + + int deleteByEntityId(String entityId, String zoneId); + + int deleteByIdentityZone(String zoneId); + + Log getLogger(); +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderProvisioning.java new file mode 100644 index 00000000000..afaaddd5e6c --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderProvisioning.java @@ -0,0 +1,20 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.util.List; + +public interface SamlServiceProviderProvisioning { + + SamlServiceProvider create(SamlServiceProvider identityProvider); + + void delete(String id); + + SamlServiceProvider update(SamlServiceProvider identityProvider); + + SamlServiceProvider retrieve(String id); + + List retrieveActive(String zoneId); + + List retrieveAll(boolean activeOnly, String zoneId); + + SamlServiceProvider retrieveByEntityId(String entityId, String zoneId); +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlSpAlreadyExistsException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlSpAlreadyExistsException.java new file mode 100644 index 00000000000..f8d3802d138 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlSpAlreadyExistsException.java @@ -0,0 +1,15 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.cloudfoundry.identity.uaa.error.UaaException; + +public class SamlSpAlreadyExistsException extends UaaException { + + /** + * Serialization id + */ + private static final long serialVersionUID = -6544686748746941568L; + + public SamlSpAlreadyExistsException(String msg) { + super("sp_exists", msg, 409); + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGenerator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGenerator.java new file mode 100644 index 00000000000..8a47ee410c5 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGenerator.java @@ -0,0 +1,75 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.springframework.security.saml.util.SAMLUtil; + +public class ZoneAwareIdpMetadataGenerator extends IdpMetadataGenerator { + + @Override + public boolean isAssertionsSigned() { + if (!IdentityZoneHolder.isUaa()) { + return getZoneDefinition().getSamlConfig().isAssertionSigned(); + } + return super.isAssertionsSigned(); + } + + @Override + public int getAssertionTimeToLiveSeconds() { + if (!IdentityZoneHolder.isUaa()) { + return getZoneDefinition().getSamlConfig().getAssertionTimeToLiveSeconds(); + } + return super.getAssertionTimeToLiveSeconds(); + } + + @Override + public IdpExtendedMetadata generateExtendedMetadata() { + IdpExtendedMetadata metadata = super.generateExtendedMetadata(); + metadata.setAlias(UaaUrlUtils.getSubdomain() + metadata.getAlias()); + return metadata; + } + + @Override + public String getEntityId() { + String entityId = super.getEntityId(); + if (UaaUrlUtils.isUrl(entityId)) { + return UaaUrlUtils.addSubdomainToUrl(entityId); + } else { + return UaaUrlUtils.getSubdomain() + entityId; + } + } + + @Override + public String getEntityBaseURL() { + return UaaUrlUtils.addSubdomainToUrl(super.getEntityBaseURL()); + } + + @Override + protected String getEntityAlias() { + return UaaUrlUtils.getSubdomain() + super.getEntityAlias(); + } + + @Override + public boolean isWantAuthnRequestSigned() { + if (!IdentityZoneHolder.isUaa()) { + return getZoneDefinition().getSamlConfig().isWantAuthnRequestSigned(); + } + return super.isWantAuthnRequestSigned(); + } + + protected IdentityZoneConfiguration getZoneDefinition() { + IdentityZone zone = IdentityZoneHolder.get(); + IdentityZoneConfiguration definition = zone.getConfig(); + return definition != null ? definition : new IdentityZoneConfiguration(); + } + + @Override + public EntityDescriptor generateMetadata() { + EntityDescriptor result = super.generateMetadata(); + result.setID(SAMLUtil.getNCNameString(result.getEntityID())); + return result; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataManager.java new file mode 100644 index 00000000000..91a5e5ad61b --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataManager.java @@ -0,0 +1,738 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; +import javax.xml.namespace.QName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.provider.saml.ComparableProvider; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.opensaml.saml2.metadata.EntitiesDescriptor; +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.RoleDescriptor; +import org.opensaml.saml2.metadata.provider.MetadataFilter; +import org.opensaml.saml2.metadata.provider.MetadataProvider; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.saml2.metadata.provider.ObservableMetadataProvider; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.security.x509.PKIXValidationInformationResolver; +import org.opensaml.xml.signature.SignatureTrustEngine; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.saml.key.KeyManager; +import org.springframework.security.saml.metadata.CachingMetadataManager; +import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; +import org.springframework.security.saml.metadata.ExtendedMetadataProvider; +import org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer; + +public class ZoneAwareIdpMetadataManager extends IdpMetadataManager implements ExtendedMetadataProvider, InitializingBean, DisposableBean, BeanNameAware { + + private static final Log logger = LogFactory.getLog(ZoneAwareIdpMetadataManager.class); + private SamlServiceProviderProvisioning providerDao; + private IdentityZoneProvisioning zoneDao; + private SamlServiceProviderConfigurator configurator; + private KeyManager keyManager; + private Map metadataManagers; + private long refreshInterval = 30000l; + private long lastRefresh = 0; + private Timer timer; + private String beanName = ZoneAwareIdpMetadataManager.class.getName()+"-"+System.identityHashCode(this); + + public ZoneAwareIdpMetadataManager(SamlServiceProviderProvisioning providerDao, + IdentityZoneProvisioning zoneDao, + SamlServiceProviderConfigurator configurator, + KeyManager keyManager) throws MetadataProviderException { + super(Collections.emptyList()); + this.providerDao = providerDao; + this.zoneDao = zoneDao; + this.configurator = configurator; + this.keyManager = keyManager; + super.setKeyManager(keyManager); + //disable internal timer + super.setRefreshCheckInterval(0); + if (metadataManagers==null) { + metadataManagers = new ConcurrentHashMap<>(); + } + } + + private class RefreshTask extends TimerTask { + @Override + public void run() { + try { + refreshAllProviders(false); + }catch (Exception x) { + log.error("Unable to run SAML provider refresh task:", x); + } + } + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @PostConstruct + public void checkAllProviders() throws MetadataProviderException { + for (Map.Entry entry : metadataManagers.entrySet()) { + entry.getValue().setKeyManager(keyManager); + } + refreshAllProviders(); + timer = new Timer("ZoneAwareMetadataManager.Refresh["+beanName+"]", true); + timer.schedule(new RefreshTask(),refreshInterval , refreshInterval); + } + + protected void refreshAllProviders() throws MetadataProviderException { + refreshAllProviders(true); + } + + protected String getThreadNameAndId() { + return Thread.currentThread().getName()+"-"+System.identityHashCode(Thread.currentThread()); + } + + protected void refreshAllProviders(boolean ignoreTimestamp) throws MetadataProviderException { + logger.debug("Running SAML SP refresh[" + getThreadNameAndId() + "] - ignoreTimestamp=" + ignoreTimestamp); + for (IdentityZone zone : zoneDao.retrieveAll()) { + ExtensionMetadataManager manager = getManager(zone); + boolean hasChanges = false; + @SuppressWarnings({ "unchecked", "rawtypes" }) + List zoneDefinitions = new LinkedList( + configurator.getSamlServiceProviderDefinitionsForZone(zone)); + for (SamlServiceProvider provider : providerDao.retrieveAll(false, zone.getId())) { + zoneDefinitions.remove(provider.getConfig()); + if (ignoreTimestamp || lastRefresh < provider.getLastModified().getTime()) { + try { + SamlServiceProviderDefinition definition = (SamlServiceProviderDefinition) provider.getConfig(); + try { + if (provider.isActive()) { + log.info("Adding SAML SP zone[" + zone.getId() + "] entityId[" + + definition.getSpEntityId() + "]"); + ExtendedMetadataDelegate[] delegates = configurator + .addSamlServiceProviderDefinition(definition); + if (delegates[1] != null) { + manager.removeMetadataProvider(delegates[1]); + } + manager.addMetadataProvider(delegates[0]); + } else { + removeSamlServiceProvider(zone, manager, definition); + } + hasChanges = true; + } catch (MetadataProviderException e) { + logger.error("Unable to refresh SAML Service Provider:" + definition, e); + } + } catch (JsonUtils.JsonUtilException x) { + logger.error("Unable to load SAML Service Provider:" + provider, x); + } + } + } + for (SamlServiceProviderDefinition definition : zoneDefinitions) { + removeSamlServiceProvider(zone, manager, definition); + hasChanges = true; + } + if (hasChanges) { + refreshZoneManager(manager); + } + } + lastRefresh = System.currentTimeMillis(); + } + + protected void removeSamlServiceProvider(IdentityZone zone, ExtensionMetadataManager manager, + SamlServiceProviderDefinition definition) { + log.info("Removing SAML SP zone[" + zone.getId() + "] entityId[" + definition.getSpEntityId() + "]"); + ExtendedMetadataDelegate delegate = configurator.removeSamlServiceProviderDefinition(definition); + if (delegate != null) { + manager.removeMetadataProvider(delegate); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public ExtensionMetadataManager getManager(IdentityZone zone) { + if (metadataManagers==null) { //called during super constructor + metadataManagers = new ConcurrentHashMap<>(); + } + ExtensionMetadataManager manager = metadataManagers.get(zone); + if (manager==null) { + try { + manager = new ExtensionMetadataManager(Collections.emptyList()); + } catch (MetadataProviderException e) { + throw new IllegalStateException(e); + } + manager.setKeyManager(keyManager); + ((ConcurrentHashMap)metadataManagers).putIfAbsent(zone, manager); + } + return metadataManagers.get(zone); + } + public ExtensionMetadataManager getManager() { + return getManager(IdentityZoneHolder.get()); + } + + @Override + public void setProviders(List newProviders) throws MetadataProviderException { + getManager().setProviders(newProviders); + } + + @Override + public void refreshMetadata() { + getManager().refreshMetadata(); + } + + @Override + public void addMetadataProvider(MetadataProvider newProvider) throws MetadataProviderException { + getManager().addMetadataProvider(newProvider); + } + + @Override + public void removeMetadataProvider(MetadataProvider provider) { + getManager().removeMetadataProvider(provider); + } + + @Override + public List getProviders() { + return getManager().getProviders(); + } + + @Override + public List getAvailableProviders() { + return getManager().getAvailableProviders(); + } + + @Override + protected void initializeProvider(ExtendedMetadataDelegate provider) throws MetadataProviderException { + getManager().initializeProvider(provider); + } + + @Override + protected void initializeProviderData(ExtendedMetadataDelegate provider) throws MetadataProviderException { + getManager().initializeProviderData(provider); + } + + @Override + protected void initializeProviderFilters(ExtendedMetadataDelegate provider) throws MetadataProviderException { + getManager().initializeProviderFilters(provider); + } + + @Override + protected SignatureTrustEngine getTrustEngine(MetadataProvider provider) { + return getManager().getTrustEngine(provider); + } + + @Override + protected PKIXValidationInformationResolver getPKIXResolver(MetadataProvider provider, Set trustedKeys, Set trustedNames) { + return getManager().getPKIXResolver(provider, trustedKeys, trustedNames); + } + + @Override + protected List parseProvider(MetadataProvider provider) throws MetadataProviderException { + return getManager().parseProvider(provider); + } + + @Override + public Set getIDPEntityNames() { + return getManager().getIDPEntityNames(); + } + + @Override + public Set getSPEntityNames() { + return getManager().getSPEntityNames(); + } + + @Override + public boolean isIDPValid(String idpID) { + return getManager().isIDPValid(idpID); + } + + @Override + public boolean isSPValid(String spID) { + return getManager().isSPValid(spID); + } + + @Override + public String getHostedIdpName() { + return getManager().getHostedIdpName(); + } + + @Override + public void setHostedIdpName(String hostedIdpName) { + getManager().setHostedIdpName(hostedIdpName); + } + + @Override + public String getHostedSPName() { + return getManager().getHostedSPName(); + } + + @Override + public void setHostedSPName(String hostedSPName) { + getManager().setHostedSPName(hostedSPName); + } + + @Override + public String getDefaultIDP() throws MetadataProviderException { + return getManager().getDefaultIDP(); + } + + @Override + public void setDefaultIDP(String defaultIDP) { + getManager().setDefaultIDP(defaultIDP); + } + + @Override + public EntityDescriptor getEntityDescriptor(byte[] hash) throws MetadataProviderException { + return getManager().getEntityDescriptor(hash); + } + + @Override + public String getEntityIdForAlias(String entityAlias) throws MetadataProviderException { + return getManager().getEntityIdForAlias(entityAlias); + } + + @Override + public ExtendedMetadata getDefaultExtendedMetadata() { + return getManager().getDefaultExtendedMetadata(); + } + + @Override + public void setDefaultExtendedMetadata(ExtendedMetadata defaultExtendedMetadata) { + getManager().setDefaultExtendedMetadata(defaultExtendedMetadata); + } + + @Override + public boolean isRefreshRequired() { + return getManager().isRefreshRequired(); + } + + @Override + public void setRefreshRequired(boolean refreshRequired) { + getManager().setRefreshRequired(refreshRequired); + } + + @Override + public void setRefreshCheckInterval(long refreshCheckInterval) { + this.refreshInterval = refreshCheckInterval; + } + + @Override + public void setKeyManager(KeyManager keyManager) { + getManager().setKeyManager(keyManager); + } + + @Override + public void setTLSConfigurer(TLSProtocolConfigurer configurer) { + getManager().setTLSConfigurer(configurer); + } + + @Override + protected void doAddMetadataProvider(MetadataProvider provider, List providerList) { + getManager().doAddMetadataProvider(provider, providerList); + } + + @Override + public void setRequireValidMetadata(boolean requireValidMetadata) { + getManager().setRequireValidMetadata(requireValidMetadata); + } + + @Override + public MetadataFilter getMetadataFilter() { + return getManager().getMetadataFilter(); + } + + @Override + public void setMetadataFilter(MetadataFilter newFilter) throws MetadataProviderException { + getManager().setMetadataFilter(newFilter); + } + + @Override + public XMLObject getMetadata() throws MetadataProviderException { + return getManager().getMetadata(); + } + + @Override + public EntitiesDescriptor getEntitiesDescriptor(String name) throws MetadataProviderException { + return getManager().getEntitiesDescriptor(name); + } + + @Override + public EntityDescriptor getEntityDescriptor(String entityID) throws MetadataProviderException { + return getManager().getEntityDescriptor(entityID); + } + + @Override + public List getRole(String entityID, QName roleName) throws MetadataProviderException { + return getManager().getRole(entityID, roleName); + } + + @Override + public RoleDescriptor getRole(String entityID, QName roleName, String supportedProtocol) throws MetadataProviderException { + return getManager().getRole(entityID, roleName, supportedProtocol); + } + + @Override + public List getObservers() { + return getManager().getObservers(); + } + + @Override + protected void emitChangeEvent() { + getManager().emitChangeEvent(); + } + + @Override + public boolean requireValidMetadata() { + return getManager().requireValidMetadata(); + } + + @Override + public void destroy() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + } + for (Map.Entry manager : metadataManagers.entrySet()) { + manager.getValue().destroy(); + } + metadataManagers.clear(); + super.destroy(); + } + + @Override + public ExtendedMetadata getExtendedMetadata(String entityID) throws MetadataProviderException { + return super.getExtendedMetadata(entityID); + } + + protected Set refreshZoneManager(ExtensionMetadataManager manager) { + Set result = new HashSet<>(); + try { + + log.trace("Executing metadata refresh task"); + + // Invoking getMetadata performs a refresh in case it's needed + // Potentially expensive operation, but other threads can still load existing cached data + for (MetadataProvider provider : manager.getProviders()) { + provider.getMetadata(); + } + + // Refresh the metadataManager if needed + if (manager.isRefreshRequired()) { + manager.refreshMetadata(); + } + + + for (MetadataProvider provider : manager.getProviders()) { + if (provider instanceof ComparableProvider) { + result.add((ComparableProvider)provider); + } else if (provider instanceof ExtendedMetadataDelegate && + ((ExtendedMetadataDelegate)provider).getDelegate() instanceof ComparableProvider) { + result.add((ComparableProvider)((ExtendedMetadataDelegate)provider).getDelegate()); + } + } + + } catch (Throwable e) { + log.warn("Metadata refreshing has failed", e); + } + return result; + } + + //just so that we can override protected methods + public static class ExtensionMetadataManager extends CachingMetadataManager { + private String hostedIdpName; + + public ExtensionMetadataManager(List providers) throws MetadataProviderException { + super(providers); + //disable internal timers (they only get created when afterPropertiesSet) + setRefreshCheckInterval(0); + } + + @Override + public EntityDescriptor getEntityDescriptor(String entityID) throws MetadataProviderException { + return super.getEntityDescriptor(entityID); + } + + @Override + public EntityDescriptor getEntityDescriptor(byte[] hash) throws MetadataProviderException { + return super.getEntityDescriptor(hash); + } + + @Override + public String getEntityIdForAlias(String entityAlias) throws MetadataProviderException { + return super.getEntityIdForAlias(entityAlias); + } + + @Override + public ExtendedMetadata getExtendedMetadata(String entityID) throws MetadataProviderException { + return super.getExtendedMetadata(entityID); + } + + @Override + public void refreshMetadata() { + super.refreshMetadata(); + } + + @Override + public void addMetadataProvider(MetadataProvider newProvider) throws MetadataProviderException { + ComparableProvider cp = null; + if (newProvider instanceof ExtendedMetadataDelegate && ((ExtendedMetadataDelegate)newProvider).getDelegate() instanceof ComparableProvider) { + cp = (ComparableProvider) ((ExtendedMetadataDelegate)newProvider).getDelegate(); + } else { + logger.warn("Adding Unknown SAML Provider type:"+(newProvider!=null?newProvider.getClass():null)+":"+newProvider); + } + + for (MetadataProvider provider : getAvailableProviders()) { + if (newProvider.equals(provider)) { + removeMetadataProvider(provider); + if (cp!=null) { + logger.debug("Found duplicate SAML provider, removing before readding zone["+cp.getZoneId()+"] alias["+cp.getAlias()+"]"); + } + } + } + super.addMetadataProvider(newProvider); + if (cp!=null) { + logger.debug("Added Metadata for SAML provider zone[" + cp.getZoneId() + "] alias[" + cp.getAlias() + "]"); + } + + } + + @Override + public void destroy() { + super.destroy(); + } + + @Override + public List getAvailableProviders() { + return super.getAvailableProviders(); + } + + @Override + public ExtendedMetadata getDefaultExtendedMetadata() { + return super.getDefaultExtendedMetadata(); + } + + @Override + public String getDefaultIDP() throws MetadataProviderException { + return super.getDefaultIDP(); + } + + public String getHostedIdpName() { + return hostedIdpName; + } + + @Override + public String getHostedSPName() { + return super.getHostedSPName(); + } + + @Override + public Set getIDPEntityNames() { + return super.getIDPEntityNames(); + } + + @Override + public PKIXValidationInformationResolver getPKIXResolver(MetadataProvider provider, Set trustedKeys, Set trustedNames) { + return super.getPKIXResolver(provider, trustedKeys, trustedNames); + } + + @Override + public List getProviders() { + return super.getProviders(); + } + + @Override + public Set getSPEntityNames() { + return super.getSPEntityNames(); + } + + @Override + public SignatureTrustEngine getTrustEngine(MetadataProvider provider) { + return super.getTrustEngine(provider); + } + + @Override + public void initializeProvider(ExtendedMetadataDelegate provider) throws MetadataProviderException { + super.initializeProvider(provider); + } + + @Override + public void initializeProviderData(ExtendedMetadataDelegate provider) throws MetadataProviderException { + super.initializeProviderData(provider); + } + + @Override + public void initializeProviderFilters(ExtendedMetadataDelegate provider) throws MetadataProviderException { + super.initializeProviderFilters(provider); + } + + @Override + public boolean isIDPValid(String idpID) { + return super.isIDPValid(idpID); + } + + @Override + public boolean isRefreshRequired() { + return super.isRefreshRequired(); + } + + @Override + public boolean isSPValid(String spID) { + return super.isSPValid(spID); + } + + @Override + public List parseProvider(MetadataProvider provider) throws MetadataProviderException { + return super.parseProvider(provider); + } + + @Override + public void removeMetadataProvider(MetadataProvider provider) { + + ComparableProvider cp = null; + if (provider instanceof ExtendedMetadataDelegate && ((ExtendedMetadataDelegate)provider).getDelegate() instanceof ComparableProvider) { + cp = (ComparableProvider) ((ExtendedMetadataDelegate)provider).getDelegate(); + } else { + logger.warn("Removing Unknown SAML Provider type:"+(provider!=null?provider.getClass():null)+":"+provider); + } + super.removeMetadataProvider(provider); + if (cp!=null) { + logger.debug("Removed Metadata for SAML provider zone[" + cp.getZoneId() + "] alias[" + cp.getAlias() + "]"); + } + } + + @Override + public void setDefaultExtendedMetadata(ExtendedMetadata defaultExtendedMetadata) { + super.setDefaultExtendedMetadata(defaultExtendedMetadata); + } + + @Override + public void setDefaultIDP(String defaultIDP) { + super.setDefaultIDP(defaultIDP); + } + + public void setHostedIdpName(String hostedIdpName) { + this.hostedIdpName = hostedIdpName; + } + + @Override + public void setHostedSPName(String hostedSPName) { + super.setHostedSPName(hostedSPName); + } + + @Override + public void setKeyManager(KeyManager keyManager) { + super.setKeyManager(keyManager); + } + + @Override + public void setProviders(List newProviders) throws MetadataProviderException { + super.setProviders(newProviders); + } + + @Override + public void setRefreshCheckInterval(long refreshCheckInterval) { + super.setRefreshCheckInterval(refreshCheckInterval); + } + + @Override + public void setRefreshRequired(boolean refreshRequired) { + super.setRefreshRequired(refreshRequired); + } + + @Override + public void setTLSConfigurer(TLSProtocolConfigurer configurer) { + super.setTLSConfigurer(configurer); + } + + @Override + public void doAddMetadataProvider(MetadataProvider provider, List providerList) { + super.doAddMetadataProvider(provider, providerList); + } + + @Override + public void emitChangeEvent() { + super.emitChangeEvent(); + } + + @Override + public EntitiesDescriptor getEntitiesDescriptor(String name) throws MetadataProviderException { + return super.getEntitiesDescriptor(name); + } + + @Override + public XMLObject getMetadata() throws MetadataProviderException { + return super.getMetadata(); + } + + @Override + public MetadataFilter getMetadataFilter() { + return super.getMetadataFilter(); + } + + @Override + public List getObservers() { + return super.getObservers(); + } + + @Override + public List getRole(String entityID, QName roleName) throws MetadataProviderException { + return super.getRole(entityID, roleName); + } + + @Override + public RoleDescriptor getRole(String entityID, QName roleName, String supportedProtocol) throws MetadataProviderException { + return super.getRole(entityID, roleName, supportedProtocol); + } + + @Override + public void setMetadataFilter(MetadataFilter newFilter) throws MetadataProviderException { + super.setMetadataFilter(newFilter); + } + + @Override + public void setRequireValidMetadata(boolean requireValidMetadata) { + super.setRequireValidMetadata(requireValidMetadata); + } + + @Override + public boolean requireValidMetadata() { + return super.requireValidMetadata(); + } + } + + public static class MetadataProviderObserver implements ObservableMetadataProvider.Observer { + private ExtensionMetadataManager manager; + + public MetadataProviderObserver(ExtensionMetadataManager manager) { + this.manager = manager; + } + + public void onEvent(MetadataProvider provider) { + manager.setRefreshRequired(true); + } + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderEventPublisher.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderEventPublisher.java new file mode 100644 index 00000000000..e3e52c5b07c --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderEventPublisher.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.zone.event; + +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProvider; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; + +public class ServiceProviderEventPublisher implements ApplicationEventPublisherAware { + private ApplicationEventPublisher publisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + public void spCreated(SamlServiceProvider serviceProvider) { + publish(ServiceProviderModifiedEvent.serviceProviderCreated(serviceProvider)); + } + + public void spModified(SamlServiceProvider serviceProvider) { + publish(ServiceProviderModifiedEvent.serviceProviderModified(serviceProvider)); + } + + public void publish(ApplicationEvent event) { + if (publisher!=null) { + publisher.publishEvent(event); + } + } +} \ No newline at end of file diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderModifiedEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderModifiedEvent.java new file mode 100644 index 00000000000..296ce9e4752 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/event/ServiceProviderModifiedEvent.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.zone.event; + + +import org.cloudfoundry.identity.uaa.audit.AuditEvent; +import org.cloudfoundry.identity.uaa.audit.AuditEventType; +import org.cloudfoundry.identity.uaa.audit.event.AbstractUaaEvent; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProvider; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.springframework.security.core.Authentication; + +public class ServiceProviderModifiedEvent extends AbstractUaaEvent { + + /** + * Generated serialization id. + */ + private static final long serialVersionUID = -204120790766086570L; + + private AuditEventType eventType; + + public ServiceProviderModifiedEvent(SamlServiceProvider serviceProvider, Authentication authentication, AuditEventType type) { + super(serviceProvider, authentication); + eventType = type; + } + + @Override + public AuditEvent getAuditEvent() { + return createAuditRecord(getSource().toString(), eventType, getOrigin(getAuthentication()), JsonUtils.writeValueAsString(source)); + } + + public static ServiceProviderModifiedEvent serviceProviderCreated(SamlServiceProvider serviceProvider) { + return new ServiceProviderModifiedEvent(serviceProvider, getContextAuthentication(), AuditEventType.ServiceProviderCreatedEvent); + } + + public static ServiceProviderModifiedEvent serviceProviderModified(SamlServiceProvider serviceProvider) { + return new ServiceProviderModifiedEvent(serviceProvider, getContextAuthentication(), AuditEventType.ServiceProviderModifiedEvent); + } + +} diff --git a/server/src/main/resources/login-ui.xml b/server/src/main/resources/login-ui.xml index 8be2b46c5a5..f656809172e 100644 --- a/server/src/main/resources/login-ui.xml +++ b/server/src/main/resources/login-ui.xml @@ -268,7 +268,9 @@ + + + + + + + + + @@ -303,7 +312,6 @@ - diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_6__SAML_SP_Management.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_6__SAML_SP_Management.sql new file mode 100644 index 00000000000..58e15fdd8b0 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_6__SAML_SP_Management.sql @@ -0,0 +1,13 @@ +CREATE TABLE service_provider ( + id CHAR(36) NOT NULL PRIMARY KEY, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + lastmodified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + version BIGINT DEFAULT 0 NOT NULL, + identity_zone_id varchar(36) NOT NULL, + name varchar(255) NOT NULL, + entity_id varchar(36) NOT NULL, + config LONGVARCHAR, + active BOOLEAN DEFAULT TRUE NOT NULL +); + +CREATE UNIQUE INDEX entity_in_zone ON service_provider (identity_zone_id,entity_id); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_6__SAML_SP_Management.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_6__SAML_SP_Management.sql new file mode 100644 index 00000000000..5ca5ce697ab --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_6__SAML_SP_Management.sql @@ -0,0 +1,13 @@ +CREATE TABLE `service_provider` ( + `id` VARCHAR(36) NOT NULL, + `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + `lastmodified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + `version` BIGINT DEFAULT 0 NOT NULL, + `identity_zone_id` VARCHAR(36) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `entity_id` VARCHAR(36) NOT NULL, + `config` LONGTEXT, + `active` BOOLEAN DEFAULT TRUE NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `entity_in_zone` (`identity_zone_id`, `entity_id`) +); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_6__SAML_SP_Management.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_6__SAML_SP_Management.sql new file mode 100644 index 00000000000..9e225eeeff0 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_6__SAML_SP_Management.sql @@ -0,0 +1,13 @@ +CREATE TABLE service_provider ( + id VARCHAR(36) NOT NULL PRIMARY KEY, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + lastmodified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + version BIGINT DEFAULT 0, + identity_zone_id VARCHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + entity_id VARCHAR(36) NOT NULL, + config TEXT, + active BOOLEAN DEFAULT TRUE NOT NULL +); + +CREATE UNIQUE INDEX entity_in_zone ON service_provider (identity_zone_id,entity_id); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/AuditEventTypeTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/AuditEventTypeTests.java index 092a1cfad05..fef6e1dabbb 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/AuditEventTypeTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/AuditEventTypeTests.java @@ -14,6 +14,6 @@ public void testAuditEventType() { assertEquals(type, AuditEventType.fromCode(count)); count++; } - assertEquals(33,count); + assertEquals(35,count); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandlerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandlerTest.java new file mode 100644 index 00000000000..22acbf2028c --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpSamlAuthenticationSuccessHandlerTest.java @@ -0,0 +1,171 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.opensaml.common.SAMLException; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.signature.SignatureException; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.metadata.MetadataManager; + +public class IdpSamlAuthenticationSuccessHandlerTest { + + private final SamlTestUtils samlTestUtils = new SamlTestUtils(); + + @Before + public void setup() throws ConfigurationException { + samlTestUtils.initalize(); + } + + @Test + public void testOnAuthenticationSuccess() throws IOException, ServletException, MetadataProviderException, + MessageEncodingException, SAMLException, SecurityException, MarshallingException, SignatureException { + IdpSamlAuthenticationSuccessHandler successHandler = new IdpSamlAuthenticationSuccessHandler(); + + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + Authentication authentication = samlTestUtils.mockIdpSamlAuthentication(context); + + IdpExtendedMetadata idpExtendedMetaData = new IdpExtendedMetadata(); + idpExtendedMetaData.setAssertionsSigned(true); + + MetadataManager metadataManager = mock(MetadataManager.class); + when(metadataManager.getExtendedMetadata(context.getLocalEntityId())).thenReturn(idpExtendedMetaData); + when(metadataManager.getEntityDescriptor(context.getPeerEntityId())) + .thenReturn(context.getPeerEntityMetadata()); + when(metadataManager.getRole(context.getPeerEntityId(), context.getPeerEntityRole(), SAMLConstants.SAML20P_NS)) + .thenReturn(context.getPeerEntityRoleMetadata()); + successHandler.setMetadataManager(metadataManager); + + IdpWebSsoProfile profile = mock(IdpWebSsoProfile.class); + doNothing().when(profile).sendResponse(any(), any(), any()); + successHandler.setIdpWebSsoProfile(profile); + + HttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + successHandler.onAuthenticationSuccess(request, response, authentication); + } + + @Test(expected = ServletException.class) + public void testOnAuthenticationSuccessFailureIfIdpExtendedMetadataMissing() + throws IOException, ServletException, MetadataProviderException { + IdpSamlAuthenticationSuccessHandler successHandler = new IdpSamlAuthenticationSuccessHandler(); + + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + Authentication authentication = samlTestUtils.mockIdpSamlAuthentication(context); + + MetadataManager metadataManager = mock(MetadataManager.class); + when(metadataManager.getExtendedMetadata(context.getLocalEntityId())) + .thenThrow(new MetadataProviderException()); + successHandler.setMetadataManager(metadataManager); + + HttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + successHandler.onAuthenticationSuccess(request, response, authentication); + } + + @Test(expected = ServletException.class) + public void testOnAuthenticationSuccessFailureIfIdpPeerEntityIdNull() + throws IOException, ServletException, MetadataProviderException, MessageEncodingException, SAMLException, + SecurityException, MarshallingException, SignatureException { + IdpSamlAuthenticationSuccessHandler successHandler = new IdpSamlAuthenticationSuccessHandler(); + + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + Authentication authentication = samlTestUtils.mockIdpSamlAuthentication(context); + + IdpExtendedMetadata idpExtendedMetaData = new IdpExtendedMetadata(); + idpExtendedMetaData.setAssertionsSigned(true); + + MetadataManager metadataManager = mock(MetadataManager.class); + when(metadataManager.getExtendedMetadata(context.getLocalEntityId())).thenReturn(idpExtendedMetaData); + when(metadataManager.getEntityDescriptor(context.getPeerEntityId())) + .thenReturn(context.getPeerEntityMetadata()); + when(metadataManager.getRole(context.getPeerEntityId(), context.getPeerEntityRole(), SAMLConstants.SAML20P_NS)) + .thenReturn(context.getPeerEntityRoleMetadata()); + successHandler.setMetadataManager(metadataManager); + + IdpWebSsoProfile profile = mock(IdpWebSsoProfile.class); + doNothing().when(profile).sendResponse(any(), any(), any()); + successHandler.setIdpWebSsoProfile(profile); + + context.setPeerEntityId(null); + HttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + successHandler.onAuthenticationSuccess(request, response, authentication); + } + + @Test(expected = ServletException.class) + public void testOnAuthenticationSuccessFailureIfIdpPeerEntityMetadataNull() + throws IOException, ServletException, MetadataProviderException, MessageEncodingException, SAMLException, + SecurityException, MarshallingException, SignatureException { + IdpSamlAuthenticationSuccessHandler successHandler = new IdpSamlAuthenticationSuccessHandler(); + + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + Authentication authentication = samlTestUtils.mockIdpSamlAuthentication(context); + + IdpExtendedMetadata idpExtendedMetaData = new IdpExtendedMetadata(); + idpExtendedMetaData.setAssertionsSigned(true); + + MetadataManager metadataManager = mock(MetadataManager.class); + when(metadataManager.getExtendedMetadata(context.getLocalEntityId())).thenReturn(idpExtendedMetaData); + when(metadataManager.getEntityDescriptor(context.getPeerEntityId())).thenReturn(null); + when(metadataManager.getRole(context.getPeerEntityId(), context.getPeerEntityRole(), SAMLConstants.SAML20P_NS)) + .thenReturn(context.getPeerEntityRoleMetadata()); + successHandler.setMetadataManager(metadataManager); + + IdpWebSsoProfile profile = mock(IdpWebSsoProfile.class); + doNothing().when(profile).sendResponse(any(), any(), any()); + successHandler.setIdpWebSsoProfile(profile); + + HttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + successHandler.onAuthenticationSuccess(request, response, authentication); + } + + @Test(expected = ServletException.class) + public void testOnAuthenticationSuccessFailureIfIdpPeerRoleDescriptorNull() + throws IOException, ServletException, MetadataProviderException, MessageEncodingException, SAMLException, + SecurityException, MarshallingException, SignatureException { + IdpSamlAuthenticationSuccessHandler successHandler = new IdpSamlAuthenticationSuccessHandler(); + + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + Authentication authentication = samlTestUtils.mockIdpSamlAuthentication(context); + + IdpExtendedMetadata idpExtendedMetaData = new IdpExtendedMetadata(); + idpExtendedMetaData.setAssertionsSigned(true); + + MetadataManager metadataManager = mock(MetadataManager.class); + when(metadataManager.getExtendedMetadata(context.getLocalEntityId())).thenReturn(idpExtendedMetaData); + when(metadataManager.getEntityDescriptor(context.getPeerEntityId())) + .thenReturn(context.getPeerEntityMetadata()); + when(metadataManager.getRole(context.getPeerEntityId(), context.getPeerEntityRole(), SAMLConstants.SAML20P_NS)) + .thenReturn(null); + successHandler.setMetadataManager(metadataManager); + + IdpWebSsoProfile profile = mock(IdpWebSsoProfile.class); + doNothing().when(profile).sendResponse(any(), any(), any()); + successHandler.setIdpWebSsoProfile(profile); + + HttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + successHandler.onAuthenticationSuccess(request, response, authentication); + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImplTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImplTest.java new file mode 100644 index 00000000000..29edbb418c0 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/IdpWebSsoProfileImplTest.java @@ -0,0 +1,81 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Before; +import org.junit.Test; +import org.opensaml.common.SAMLException; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.Subject; +import org.opensaml.saml2.core.SubjectConfirmation; +import org.opensaml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.signature.SignatureException; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml.context.SAMLMessageContext; + +public class IdpWebSsoProfileImplTest { + + private final SamlTestUtils samlTestUtils = new SamlTestUtils(); + + @Before + public void setup() throws ConfigurationException { + samlTestUtils.initalize(); + } + + @Test + public void testBuildResponse() throws MessageEncodingException, SAMLException, MetadataProviderException, + SecurityException, MarshallingException, SignatureException { + IdpWebSsoProfileImpl profile = new IdpWebSsoProfileImpl(); + + Authentication authentication = samlTestUtils.mockAuthentication(); + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + + IdpWebSSOProfileOptions options = new IdpWebSSOProfileOptions(); + options.setAssertionsSigned(false); + profile.buildResponse(authentication, context, options); + + AuthnRequest request = (AuthnRequest) context.getInboundSAMLMessage(); + Response response = (Response) context.getOutboundSAMLMessage(); + Assertion assertion = response.getAssertions().get(0); + Subject subject = assertion.getSubject(); + assertEquals("marissa", subject.getNameID().getValue()); + + SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0); + SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData(); + assertEquals(request.getID(), subjectConfirmationData.getInResponseTo()); + } + + @Test + public void testBuildResponseWithSignedAssertion() throws MessageEncodingException, SAMLException, + MetadataProviderException, SecurityException, MarshallingException, SignatureException { + IdpWebSsoProfileImpl profile = new IdpWebSsoProfileImpl(); + + Authentication authentication = samlTestUtils.mockAuthentication(); + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext(); + + IdpWebSSOProfileOptions options = new IdpWebSSOProfileOptions(); + options.setAssertionsSigned(true); + profile.buildResponse(authentication, context, options); + + AuthnRequest request = (AuthnRequest) context.getInboundSAMLMessage(); + Response response = (Response) context.getOutboundSAMLMessage(); + Assertion assertion = response.getAssertions().get(0); + Subject subject = assertion.getSubject(); + assertEquals("marissa", subject.getNameID().getValue()); + + SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0); + SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData(); + assertEquals(request.getID(), subjectConfirmationData.getInResponseTo()); + + assertNotNull(assertion.getSignature()); + } + +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioningTest.java new file mode 100644 index 00000000000..526d0f1adc2 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/JdbcSamlServiceProviderProvisioningTest.java @@ -0,0 +1,222 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +import java.sql.Timestamp; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; +import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; + +public class JdbcSamlServiceProviderProvisioningTest extends JdbcTestBase { + + private JdbcSamlServiceProviderProvisioning db; + private RandomValueStringGenerator generator = new RandomValueStringGenerator(); + private Authentication authentication = mock(Authentication.class); + + + @Before + public void createDatasource() throws Exception { + db = new JdbcSamlServiceProviderProvisioning(jdbcTemplate); + } + + @After + public void cleanUp() { + IdentityZoneHolder.clear(); + } + + @Test + public void testCreateAndUpdateSamlServiceProviderInDefaultZone() throws Exception { + IdentityZoneHolder.set(IdentityZone.getUaa()); + String zoneId = IdentityZone.getUaa().getId(); + + SamlServiceProvider sp = createSamlServiceProvider(zoneId); + + SamlServiceProvider createdSp = db.create(sp); + Map rawCreatedSp = jdbcTemplate.queryForMap("select * from service_provider where id = ?", + createdSp.getId()); + + assertEquals(sp.getName(), createdSp.getName()); + assertEquals(sp.getConfig(), createdSp.getConfig()); + + assertEquals(sp.getName(), rawCreatedSp.get("name")); + assertEquals(sp.getConfig(), + JsonUtils.readValue((String) rawCreatedSp.get("config"), SamlServiceProviderDefinition.class)); + assertEquals(zoneId, rawCreatedSp.get("identity_zone_id").toString().trim()); + + sp.setId(createdSp.getId()); + sp.setLastModified(new Timestamp(System.currentTimeMillis())); + sp.setName("updated name"); + sp.setCreated(createdSp.getCreated()); + SamlServiceProviderDefinition updatedConfig = new SamlServiceProviderDefinition(); + updatedConfig.setMetaDataLocation(SamlTestUtils.UNSIGNED_SAML_SP_METADATA); + sp.setConfig(updatedConfig); + sp.setIdentityZoneId(zoneId); + createdSp = db.update(sp); + + assertEquals(sp.getName(), createdSp.getName()); + assertEquals(sp.getConfig(), createdSp.getConfig()); + assertEquals(sp.getLastModified().getTime() / 1000, createdSp.getLastModified().getTime() / 1000); + assertEquals(Integer.valueOf(rawCreatedSp.get("version").toString()) + 1, createdSp.getVersion()); + assertEquals(zoneId, createdSp.getIdentityZoneId()); + } + + private SamlServiceProvider createSamlServiceProvider(String zoneId) { + SamlServiceProvider sp = new SamlServiceProvider(); + sp.setActive(true); + SamlServiceProviderDefinition config = new SamlServiceProviderDefinition(); + config.setMetaDataLocation(SamlTestUtils.SAML_SP_METADATA); + sp.setConfig(config); + sp.setEntityId(SamlTestUtils.SP_ENTITY_ID); + sp.setIdentityZoneId(zoneId); + sp.setLastModified(new Date()); + sp.setName("Unit Test SAML SP"); + return sp; + } + + @Test + public void testCreateSamlServiceProviderInOtherZone() throws Exception { + IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); + + SamlServiceProvider sp = createSamlServiceProvider(zone.getId()); + + SamlServiceProvider createdSp = db.create(sp); + Map rawCreatedSp = jdbcTemplate.queryForMap("select * from service_provider where id = ?", + createdSp.getId()); + + assertEquals(sp.getName(), createdSp.getName()); + assertEquals(sp.getConfig(), createdSp.getConfig()); + + assertEquals(sp.getName(), rawCreatedSp.get("name")); + assertEquals(sp.getConfig(), + JsonUtils.readValue((String) rawCreatedSp.get("config"), SamlServiceProviderDefinition.class)); + assertEquals(zone.getId(), rawCreatedSp.get("identity_zone_id")); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void testGetSamlServiceProviderForWrongZone() throws Exception { + IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); + + SamlServiceProvider sp = createSamlServiceProvider(zone.getId()); + db.create(sp); + + // The current zone is not where we are creating the zone. + IdentityZoneHolder.set(IdentityZone.getUaa()); + db.retrieve(sp.getId()); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void testUpdateSamlServiceProviderInWrongZone() throws Exception { + IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); + + SamlServiceProvider sp = createSamlServiceProvider(zone.getId()); + + SamlServiceProvider createdSp = db.create(sp); + Map rawCreatedSp = jdbcTemplate.queryForMap("select * from service_provider where id = ?", + createdSp.getId()); + + assertEquals(sp.getName(), createdSp.getName()); + assertEquals(sp.getConfig(), createdSp.getConfig()); + + assertEquals(sp.getName(), rawCreatedSp.get("name")); + assertEquals(sp.getConfig(), + JsonUtils.readValue((String) rawCreatedSp.get("config"), SamlServiceProviderDefinition.class)); + assertEquals(zone.getId(), rawCreatedSp.get("identity_zone_id").toString().trim()); + + sp.setId(createdSp.getId()); + sp.setLastModified(new Timestamp(System.currentTimeMillis())); + sp.setName("updated name"); + sp.setCreated(createdSp.getCreated()); + SamlServiceProviderDefinition updatedConfig = new SamlServiceProviderDefinition(); + updatedConfig.setMetaDataLocation(SamlTestUtils.UNSIGNED_SAML_SP_METADATA); + sp.setConfig(updatedConfig); + sp.setIdentityZoneId(zone.getId()); + // Switch to a different zone before updating. + IdentityZoneHolder.set(IdentityZone.getUaa()); + db.update(sp); + } + + @Test(expected = SamlSpAlreadyExistsException.class) + public void testCreateSamlServiceProviderWithSameEntityIdInDefaultZone() throws Exception { + IdentityZoneHolder.set(IdentityZone.getUaa()); + String zoneId = IdentityZone.getUaa().getId(); + SamlServiceProvider sp = createSamlServiceProvider(zoneId); + db.create(sp); + db.create(sp); + } + + @Test(expected = SamlSpAlreadyExistsException.class) + public void testCreateSamlServiceProviderWithSameEntityIdInOtherZone() throws Exception { + IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); + SamlServiceProvider sp = createSamlServiceProvider(zone.getId()); + db.create(sp); + db.create(sp); + } + + @Test + public void testCreateSamlServiceProviderWithSameEntityIdInDifferentZones() throws Exception { + IdentityZoneHolder.set(IdentityZone.getUaa()); + String zoneId = IdentityZone.getUaa().getId(); + SamlServiceProvider sp = createSamlServiceProvider(zoneId); + db.create(sp); + + IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); + zoneId = zone.getId(); + sp.setIdentityZoneId(zoneId); + db.create(sp); + } + + @Test + public void testDeleteSamlServiceProvidersInUaaZone() { + IdentityZoneHolder.set(IdentityZone.getUaa()); + String zoneId = IdentityZone.getUaa().getId(); + + SamlServiceProvider sp = createSamlServiceProvider(zoneId); + SamlServiceProvider createdSp = db.create(sp); + + assertNotNull(createdSp); + assertThat(jdbcTemplate.queryForObject("select count(*) from service_provider where identity_zone_id=?", + new Object[] { IdentityZoneHolder.get().getId() }, Integer.class), is(1)); + db.onApplicationEvent(new EntityDeletedEvent<>(createdSp, authentication)); + assertThat(jdbcTemplate.queryForObject("select count(*) from service_provider where identity_zone_id=?", + new Object[] { IdentityZoneHolder.get().getId() }, Integer.class), is(0)); + } + + @Test + public void testDeleteSamlServiceProvidersInOtherZone() { + String zoneId = generator.generate(); + IdentityZone zone = MultitenancyFixture.identityZone(zoneId, zoneId); + IdentityZoneHolder.set(zone); + + SamlServiceProvider sp = createSamlServiceProvider(zoneId); + SamlServiceProvider createdSp = db.create(sp); + + assertNotNull(createdSp); + assertThat(jdbcTemplate.queryForObject("select count(*) from service_provider where identity_zone_id=?", + new Object[] { IdentityZoneHolder.get().getId() }, Integer.class), is(1)); + db.onApplicationEvent(new EntityDeletedEvent<>(createdSp, authentication)); + assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=?", + new Object[] { IdentityZoneHolder.get().getId() }, Integer.class), is(0)); + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfiguratorTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfiguratorTest.java new file mode 100644 index 00000000000..77a27a09dd7 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfiguratorTest.java @@ -0,0 +1,100 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.fail; + +import java.util.Timer; + +import org.cloudfoundry.identity.uaa.provider.saml.ComparableProvider; +import org.junit.Before; +import org.junit.Test; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.parse.BasicParserPool; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; + +public class SamlServiceProviderConfiguratorTest { + + private static final String SINGLE_ADD_ENTITY_ID = "cloudfoundry-saml-login"; + private final SamlTestUtils samlTestUtils = new SamlTestUtils(); + + private SamlServiceProviderConfigurator conf = null; + private SamlServiceProviderDefinition singleAdd = null; + private SamlServiceProviderDefinition singleAddWithoutHeader = null; + + @Before + public void setup() throws Exception { + samlTestUtils.initalize(); + conf = new SamlServiceProviderConfigurator(); + conf.setParserPool(new BasicParserPool()); + singleAdd = SamlServiceProviderDefinition.Builder.get() + .setMetaDataLocation(String.format(SamlTestUtils.UNSIGNED_SAML_SP_METADATA_WITHOUT_ID, + new RandomValueStringGenerator().generate())) + .setSpEntityId(SINGLE_ADD_ENTITY_ID).setNameID("sample-nameID").setSingleSignOnServiceIndex(1) + .setMetadataTrustCheck(true).setZoneId("uaa").build(); + singleAddWithoutHeader = SamlServiceProviderDefinition.Builder.get() + .setMetaDataLocation(String.format(SamlTestUtils.UNSIGNED_SAML_SP_METADATA_WITHOUT_HEADER, + new RandomValueStringGenerator().generate())) + .setSpEntityId(SINGLE_ADD_ENTITY_ID).setNameID("sample-nameID").setSingleSignOnServiceIndex(1) + .setMetadataTrustCheck(true).setZoneId("uaa").build(); + } + + @Test + public void testCloneSamlServiceProviderDefinition() throws Exception { + SamlServiceProviderDefinition clone = singleAdd.clone(); + assertEquals(singleAdd, clone); + assertNotSame(singleAdd, clone); + } + + @Test + public void testAddAndUpdateAndRemoveSamlServiceProviderDefinition() throws Exception { + conf.addSamlServiceProviderDefinition(singleAdd); + assertEquals(1, conf.getSamlServiceProviders().size()); + conf.addSamlServiceProviderDefinition(singleAddWithoutHeader); + assertEquals(1, conf.getSamlServiceProviders().size()); + conf.removeSamlServiceProviderDefinition(singleAdd); + assertEquals(0, conf.getSamlServiceProviders().size()); + } + + @Test(expected = MetadataProviderException.class) + public void testAddSamlServiceProviderDefinitionWithConflictingEntityId() throws Exception { + conf.addSamlServiceProviderDefinition(singleAdd); + SamlServiceProviderDefinition duplicate = SamlServiceProviderDefinition.Builder.get() + .setMetaDataLocation(String.format(SamlTestUtils.UNSIGNED_SAML_SP_METADATA_WITHOUT_ID, + new RandomValueStringGenerator().generate())) + .setSpEntityId(SINGLE_ADD_ENTITY_ID + "_2").setNameID("sample-nameID").setSingleSignOnServiceIndex(1) + .setMetadataTrustCheck(true).setZoneId("uaa").build(); + conf.addSamlServiceProviderDefinition(duplicate); + } + + @Test(expected = NullPointerException.class) + public void testAddNullSamlServiceProvider() throws Exception { + conf.addSamlServiceProviderDefinition(null); + } + + @Test(expected = NullPointerException.class) + public void testAddSamlServiceProviderWithNullEntityId() throws Exception { + singleAdd.setSpEntityId(null); + conf.addSamlServiceProviderDefinition(singleAdd); + } + + @Test + public void testGetEntityID() throws Exception { + Timer t = new Timer(); + conf.addSamlServiceProviderDefinition(singleAdd); + for (SamlServiceProviderDefinition def : conf.getSamlServiceProviderDefinitions()) { + switch (def.getSpEntityId()) { + case "cloudfoundry-saml-login": { + ComparableProvider provider = (ComparableProvider) conf.getExtendedMetadataDelegateFromCache(def) + .getDelegate(); + assertEquals("cloudfoundry-saml-login", provider.getEntityID()); + break; + } + default: + fail(String.format("Unknown provider %s", def.getSpEntityId())); + } + + } + t.cancel(); + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinitionTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinitionTest.java new file mode 100644 index 00000000000..bd2e209beb3 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderDefinitionTest.java @@ -0,0 +1,90 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderDefinition.MetadataLocation.DATA; +import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderDefinition.MetadataLocation.UNKNOWN; +import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderDefinition.MetadataLocation.URL; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.commons.httpclient.contrib.ssl.StrictSSLProtocolSocketFactory; +import org.junit.Before; +import org.junit.Test; + +public class SamlServiceProviderDefinitionTest { + + SamlServiceProviderDefinition definition; + + @Before + public void createDefinition() { + definition = SamlServiceProviderDefinition.Builder.get() + .setMetaDataLocation("location") + .setSpEntityId("alias") + .setNameID("nameID") + .setMetadataTrustCheck(true) + .setZoneId("zoneId") + .build(); + } + + @Test + public void testXmlWithDoctypeFails() { + definition.setMetaDataLocation(SamlTestUtils.SAML_SP_METADATA.replace("", "\n")); + assertEquals(UNKNOWN, definition.getType()); + } + + @Test + public void testGetFileTypeFailsAndIsNoLongerSupported() throws Exception { + definition.setMetaDataLocation(System.getProperty("user.home")); + assertEquals(UNKNOWN, definition.getType()); + } + + @Test + public void testGetUrlTypeMustBeValidUrl() throws Exception { + definition.setMetaDataLocation("http"); + assertEquals(UNKNOWN, definition.getType()); + } + + @Test + public void testGetUrlWhenValid() throws Exception { + definition.setMetaDataLocation("http://login.identity.cf-app.com/saml/idp/metadata"); + assertEquals(URL, definition.getType()); + } + + @Test + public void testGetDataTypeIsValid() throws Exception { + definition.setMetaDataLocation(" builder = (SAMLObjectBuilder) builderFactory + .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); + AuthnRequest request = builder.buildObject(); + request.setVersion(SAMLVersion.VERSION_20); + request.setID(generateID()); + request.setIssuer(getIssuer(SP_ENTITY_ID)); + request.setVersion(SAMLVersion.VERSION_20); + request.setIssueInstant(new DateTime()); + return request; + } + + public String generateID() { + Random r = new Random(); + return 'a' + Long.toString(Math.abs(r.nextLong()), 20) + Long.toString(Math.abs(r.nextLong()), 20); + } + + public Issuer getIssuer(String localEntityId) { + @SuppressWarnings("unchecked") + SAMLObjectBuilder issuerBuilder = (SAMLObjectBuilder) builderFactory + .getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(localEntityId); + return issuer; + } + + public Authentication mockIdpSamlAuthentication(SAMLMessageContext context) { + + Authentication samlAuthenticationToken = new SAMLAuthenticationToken(context); + Authentication loginAuthenticationToken = mockAuthentication(); + IdpSamlCredentialsHolder credentials = new IdpSamlCredentialsHolder(samlAuthenticationToken, + loginAuthenticationToken); + IdpSamlAuthentication authentication = new IdpSamlAuthentication(credentials); + return authentication; + } + + public Authentication mockAuthentication() { + Authentication authentication = mock(Authentication.class); + when(authentication.getName()).thenReturn("marissa"); + + UaaPrincipal principal = new UaaPrincipal(UUID.randomUUID().toString(), "marissa", "marissa@testing.org", + "http://localhost:8080/uaa/oauth/token", "marissa", "uaa"); + when(authentication.getPrincipal()).thenReturn(principal); + + Collection authorities = new ArrayList<>(); + authorities.add(UaaAuthority.UAA_USER); + doReturn(authorities).when(authentication).getAuthorities(); + + return authentication; + } + + public static final String SAML_SP_METADATA = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "mPb/c/Gb/PN61JNRptMgHbK9L08=" + + "" + + "" + + "Ra6mE3hjN68Jwk6D3DktVrOu0BXJCSPTMr0YTgQyII8fv7j93BhuGMoZHw48tww6N9zkUDEuy+uRp9vd4gepxs8+XiL+kvoclMAStmzJ62/2fGuI3hCvht2lBXIuFBpZab3iuqxBhwceLnsvvsM5y4nfYDXuBS1XGRzrygLbldM=" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" + + "" + + "" + + "" + + ""; + + public static final String UNSIGNED_SAML_SP_METADATA = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" + + "" + + "" + + "" + + ""; + + public static final String UNSIGNED_SAML_SP_METADATA_WITHOUT_ID = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEOMAwGA1UECBMF" + + "YXJ1YmExDjAMBgNVBAoTBWFydWJhMQ4wDAYDVQQHEwVhcnViYTEOMAwGA1UECxMFYXJ1YmExDjAM" + + "BgNVBAMTBWFydWJhMR0wGwYJKoZIhvcNAQkBFg5hcnViYUBhcnViYS5hcjAeFw0xNTExMjAyMjI2" + + "MjdaFw0xNjExMTkyMjI2MjdaMHwxCzAJBgNVBAYTAmF3MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UE" + + "ChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQLEwVhcnViYTEOMAwGA1UEAxMFYXJ1YmEx" + + "HTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB" + + "gQDHtC5gUXxBKpEqZTLkNvFwNGnNIkggNOwOQVNbpO0WVHIivig5L39WqS9u0hnA+O7MCA/KlrAR" + + "4bXaeVVhwfUPYBKIpaaTWFQR5cTR1UFZJL/OF9vAfpOwznoD66DDCnQVpbCjtDYWX+x6imxn8HCY" + + "xhMol6ZnTbSsFW6VZjFMjQIDAQABo4HaMIHXMB0GA1UdDgQWBBTx0lDzjH/iOBnOSQaSEWQLx1sy" + + "GDCBpwYDVR0jBIGfMIGcgBTx0lDzjH/iOBnOSQaSEWQLx1syGKGBgKR+MHwxCzAJBgNVBAYTAmF3" + + "MQ4wDAYDVQQIEwVhcnViYTEOMAwGA1UEChMFYXJ1YmExDjAMBgNVBAcTBWFydWJhMQ4wDAYDVQQL" + + "EwVhcnViYTEOMAwGA1UEAxMFYXJ1YmExHTAbBgkqhkiG9w0BCQEWDmFydWJhQGFydWJhLmFyggEA" + + "MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYvBJ0HOZbbHClXmGUjGs+GS+xC1FO/am" + + "2suCSYqNB9dyMXfOWiJ1+TLJk+o/YZt8vuxCKdcZYgl4l/L6PxJ982SRhc83ZW2dkAZI4M0/Ud3o" + + "ePe84k8jm3A7EvH5wi5hvCkKRpuRBwn3Ei+jCRouxTbzKPsuCVB+1sNyxMTXzf0=" + + "" + + "" + + "" + + "" + + "" + + "" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + + "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" + + "" + + "" + + "" + + ""; + + public static final String UNSIGNED_SAML_SP_METADATA_WITHOUT_HEADER = UNSIGNED_SAML_SP_METADATA_WITHOUT_ID.replace("", ""); + +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGeneratorTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGeneratorTest.java new file mode 100644 index 00000000000..0dd5f6cfa29 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/ZoneAwareIdpMetadataGeneratorTest.java @@ -0,0 +1,53 @@ +package org.cloudfoundry.identity.uaa.provider.saml.idp; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ZoneAwareIdpMetadataGeneratorTest { + + public static final String ZONE_ID = "zone-id"; + private ZoneAwareIdpMetadataGenerator generator; + private IdentityZone otherZone; + private IdentityZoneConfiguration otherZoneDefinition; + + @Before + public void setup() { + otherZone = new IdentityZone(); + otherZone.setId(ZONE_ID); + otherZone.setName(ZONE_ID); + otherZone.setSubdomain(ZONE_ID); + otherZone.setConfig(new IdentityZoneConfiguration()); + otherZoneDefinition = otherZone.getConfig(); + otherZoneDefinition.getSamlConfig().setRequestSigned(true); + otherZoneDefinition.getSamlConfig().setWantAssertionSigned(true); + + otherZone.setConfig(otherZoneDefinition); + + generator = new ZoneAwareIdpMetadataGenerator(); + } + + @After + public void clear() { + IdentityZoneHolder.clear(); + } + + @Test + public void testWantRequestSigned() { + generator.setWantAuthnRequestSigned(false); + assertFalse(generator.isWantAuthnRequestSigned()); + + generator.setWantAuthnRequestSigned(true); + assertTrue(generator.isWantAuthnRequestSigned()); + + IdentityZoneHolder.set(otherZone); + + assertFalse(generator.isWantAuthnRequestSigned()); + } +} diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 281cae232d1..8a7ed7949dc 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -332,6 +332,7 @@ + diff --git a/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml index 641d1dda0b6..7eb81b4e115 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml @@ -143,4 +143,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/uaa/src/main/webapp/WEB-INF/spring/saml-idp.xml b/uaa/src/main/webapp/WEB-INF/spring/saml-idp.xml new file mode 100644 index 00000000000..c3f3c4c3d85 --- /dev/null +++ b/uaa/src/main/webapp/WEB-INF/spring/saml-idp.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml b/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml index f5c901d5df1..55d269cfc84 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml @@ -32,6 +32,7 @@ + @@ -109,6 +110,7 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + @@ -128,6 +130,7 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + @@ -199,6 +206,7 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + @@ -225,6 +233,7 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + @@ -251,23 +260,35 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + + + + - + + + + + + - + + + + @@ -292,6 +313,7 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + @@ -332,6 +354,7 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ + @@ -345,5 +368,4 @@ qy45ptdwJLqLJCeNoR0JUcDNIRhOCuOPND7pcMtX6hI/ - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginWithLocalIdpIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginWithLocalIdpIT.java new file mode 100644 index 00000000000..fac08b8c093 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginWithLocalIdpIT.java @@ -0,0 +1,561 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.integration.feature; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeTrue; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.cloudfoundry.identity.uaa.ServerRunning; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils; +import org.cloudfoundry.identity.uaa.login.test.LoginServerClassRunner; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProvider; +import org.cloudfoundry.identity.uaa.provider.saml.idp.SamlServiceProviderDefinition; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; +import org.cloudfoundry.identity.uaa.zone.SamlConfig; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.opensaml.saml2.metadata.IDPSSODescriptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.test.TestAccounts; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.type.TypeReference; + +@RunWith(LoginServerClassRunner.class) +@ContextConfiguration(classes = DefaultIntegrationTestConfig.class) +public class SamlLoginWithLocalIdpIT { + + @Autowired + @Rule + public IntegrationTestRule integrationTestRule; + + @Autowired + RestOperations restOperations; + + @Autowired + WebDriver webDriver; + + @Value("${integration.test.base_url}") + String baseUrl; + + @Autowired + TestAccounts testAccounts; + + @Autowired + TestClient testClient; + + ServerRunning serverRunning = ServerRunning.isRunning(); + + @Before + public void clearWebDriverOfCookies() throws Exception { + webDriver.get(baseUrl + "/logout.do"); + webDriver.manage().deleteAllCookies(); + webDriver.get(baseUrl.replace("localhost", "testzone1.localhost") + "/logout.do"); + webDriver.manage().deleteAllCookies(); + webDriver.get(baseUrl.replace("localhost", "testzone2.localhost") + "/logout.do"); + webDriver.manage().deleteAllCookies(); + webDriver.get("http://simplesamlphp.cfapps.io/module.php/core/authenticate.php?as=example-userpass&logout"); + webDriver.get("http://simplesamlphp2.cfapps.io/module.php/core/authenticate.php?as=example-userpass&logout"); + } + + /** + * Test that can UAA generate it's own SAML identity provider metadata. + */ + @Test + public void testDownloadSamlIdpMetadata() { + String entityId = "unit-test-idp"; + SamlIdentityProviderDefinition idpDefinition = createLocalSamlIdpDefinition(entityId, "uaa"); + Assert.assertTrue(idpDefinition.getMetaDataLocation().contains(IDPSSODescriptor.DEFAULT_ELEMENT_LOCAL_NAME)); + Assert.assertTrue(idpDefinition.getMetaDataLocation().contains("entityID=\"" + entityId + "\"")); + } + + /** + * Test that we can create an identity provider in UAA using UAA's own SAML identity provider metadata. + */ + @Test + public void testCreateSamlIdp() throws Exception { + SamlIdentityProviderDefinition idpDef = createLocalSamlIdpDefinition("unit-test-idp", OriginKeys.UAA); + IntegrationTestUtils.createIdentityProvider("Local SAML IdP", "unit-test-idp", true, this.baseUrl, + this.serverRunning, idpDef); + } + + public static SamlIdentityProviderDefinition createLocalSamlIdpDefinition(String alias, String zoneId) { + + String url; + if (StringUtils.isNotEmpty(zoneId) && !zoneId.equals("uaa")) { + url = "http://" + zoneId + ".localhost:8080/uaa/saml/idp/metadata/alias/" + zoneId + "." + alias + "/idp"; + } else { + url = "http://localhost:8080/uaa/saml/idp/metadata/alias/" + alias + "/idp"; + } + + RestTemplate client = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", "application/samlmetadata+xml"); + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + HttpEntity getHeaders = new HttpEntity(headers); + ResponseEntity metadataResponse = client.exchange(url, HttpMethod.GET, getHeaders, String.class); + + String idpMetaData = metadataResponse.getBody(); + SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition(); + def.setZoneId(zoneId); + def.setMetaDataLocation(idpMetaData); + def.setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); + def.setAssertionConsumerIndex(0); + def.setMetadataTrustCheck(false); + def.setShowSamlLink(true); + if (StringUtils.isNotEmpty(zoneId) && !zoneId.equals(OriginKeys.UAA)) { + def.setIdpEntityAlias(zoneId + "." + alias); + def.setLinkText("Login with Local SAML IdP(" + zoneId + "." + alias + ")"); + } else { + def.setIdpEntityAlias(alias); + def.setLinkText("Login with Local SAML IdP(" + alias + ")"); + } + return def; + } + + @Test + public void testCreateSamlSp() throws Exception { + SamlServiceProviderDefinition spDef = createLocalSamlSpDefinition("cloudfoundry-saml-login", "uaa"); + createSamlServiceProvider("Local SAML SP", "unit-test-sp", baseUrl, serverRunning, spDef); + } + + public static SamlServiceProviderDefinition createLocalSamlSpDefinition(String alias, String zoneId) { + + String url; + if (StringUtils.isNotEmpty(zoneId) && !zoneId.equals("uaa")) { + url = "http://" + zoneId + ".localhost:8080/uaa/saml/metadata/alias/" + zoneId + "." + alias; + } else { + url = "http://localhost:8080/uaa/saml/metadata/alias/" + alias; + } + + RestTemplate client = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", "application/samlmetadata+xml"); + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + HttpEntity getHeaders = new HttpEntity(headers); + ResponseEntity metadataResponse = client.exchange(url, HttpMethod.GET, getHeaders, String.class); + + String spMetaData = metadataResponse.getBody(); + SamlServiceProviderDefinition def = new SamlServiceProviderDefinition(); + def.setZoneId(zoneId); + if (StringUtils.isNotEmpty(zoneId) && !zoneId.equals("uaa")) { + def.setSpEntityId(zoneId + "." + alias); + } else { + def.setSpEntityId(alias); + } + def.setMetaDataLocation(spMetaData); + def.setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); + def.setSingleSignOnServiceIndex(0); + def.setMetadataTrustCheck(false); + return def; + } + + public static SamlServiceProviderDefinition createZone1SamlSpDefinition(String alias) { + return createLocalSamlSpDefinition(alias, "testzone1"); + } + + public static SamlServiceProviderDefinition createZone2SamlSpDefinition(String alias) { + return createLocalSamlSpDefinition(alias, "testzone2"); + } + + public static SamlServiceProvider createSamlServiceProvider(String name, String entityId, String baseUrl, + ServerRunning serverRunning, SamlServiceProviderDefinition samlServiceProviderDefinition) throws Exception { + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate(IntegrationTestUtils + .getClientCredentialsResource(baseUrl, new String[0], "identity", "identitysecret")); + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret")); + String email = new RandomValueStringGenerator().generate() + "@samltesting.org"; + ScimUser user = IntegrationTestUtils.createUser(adminClient, baseUrl, email, "firstname", "lastname", email, + true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, user.getId(), OriginKeys.UAA); + + String zoneAdminToken = IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), "identity", "identitysecret", email, "secr3T"); + + SamlServiceProvider provider = new SamlServiceProvider(); + provider.setConfig(samlServiceProviderDefinition); + provider.setIdentityZoneId(OriginKeys.UAA); + provider.setActive(true); + provider.setEntityId(samlServiceProviderDefinition.getSpEntityId()); + provider.setName(name); + provider = createOrUpdateSamlServiceProvider(zoneAdminToken, baseUrl, provider); + assertNotNull(provider.getId()); + return provider; + } + + public static SamlServiceProvider createOrUpdateSamlServiceProvider(String accessToken, String url, + SamlServiceProvider provider) { + RestTemplate client = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + headers.add("Authorization", "bearer " + accessToken); + headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); + headers.add(IdentityZoneSwitchingFilter.HEADER, provider.getIdentityZoneId()); + List existing = getSamlServiceProviders(accessToken, url, provider.getIdentityZoneId()); + if (existing != null) { + for (SamlServiceProvider p : existing) { + if (p.getEntityId().equals(provider.getEntityId()) + && p.getIdentityZoneId().equals(provider.getIdentityZoneId())) { + provider.setId(p.getId()); + HttpEntity putHeaders = new HttpEntity(provider, headers); + ResponseEntity providerPut = client.exchange(url + "/service-providers/{id}", + HttpMethod.PUT, putHeaders, String.class, provider.getId()); + if (providerPut.getStatusCode() == HttpStatus.OK) { + return JsonUtils.readValue(providerPut.getBody(), SamlServiceProvider.class); + } + } + } + } + + HttpEntity postHeaders = new HttpEntity(provider, headers); + ResponseEntity providerPost = client.exchange(url + "/service-providers/{id}", HttpMethod.POST, + postHeaders, String.class, provider.getId()); + if (providerPost.getStatusCode() == HttpStatus.CREATED) { + return JsonUtils.readValue(providerPost.getBody(), SamlServiceProvider.class); + } + throw new IllegalStateException( + "Invalid result code returned, unable to create identity provider:" + providerPost.getStatusCode()); + } + + public static List getSamlServiceProviders(String zoneAdminToken, String url, String zoneId) { + RestTemplate client = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + headers.add("Authorization", "bearer " + zoneAdminToken); + headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + HttpEntity getHeaders = new HttpEntity(headers); + ResponseEntity providerGet = client.exchange(url + "/service-providers", HttpMethod.GET, getHeaders, + String.class); + if (providerGet != null && providerGet.getStatusCode() == HttpStatus.OK) { + return JsonUtils.readValue(providerGet.getBody(), new TypeReference>() { + // Do nothing. + }); + } + return null; + } + + @Test + public void testLocalSamlIdpLogin() throws Exception { + ScimUser user = IntegrationTestUtils.createRandomUser(this.baseUrl); + testLocalSamlIdpLogin("/login", "Where to?", user.getPrimaryEmail(), "secr3T"); + } + + private void testLocalSamlIdpLogin(String firstUrl, String lookfor, String username, String password) + throws Exception { + SamlIdentityProviderDefinition idpDef = createLocalSamlIdpDefinition("unit-test-idp", "uaa"); + @SuppressWarnings("unchecked") + IdentityProvider provider = IntegrationTestUtils.createIdentityProvider( + "Local SAML IdP", "unit-test-idp", true, this.baseUrl, this.serverRunning, idpDef); + + SamlServiceProviderDefinition spDef = createLocalSamlSpDefinition("cloudfoundry-saml-login", "uaa"); + createSamlServiceProvider("Local SAML SP", "cloudfoundry-saml-login", baseUrl, serverRunning, spDef); + + // tells us that we are on travis + assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + + webDriver.get(baseUrl + firstUrl); + Assert.assertEquals("Cloud Foundry", webDriver.getTitle()); + webDriver.findElement(By.xpath("//a[text()='" + provider.getConfig().getLinkText() + "']")).click(); + webDriver.findElement(By.xpath("//h1[contains(text(), 'Welcome!')]")); + webDriver.findElement(By.name("username")).clear(); + webDriver.findElement(By.name("username")).sendKeys(username); + webDriver.findElement(By.name("password")).sendKeys(password); + webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); + assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString(lookfor)); + + provider.setActive(false); + IntegrationTestUtils.updateIdentityProvider(this.baseUrl, this.serverRunning, provider); + } + + @SuppressWarnings("unchecked") + @Test + public void testLocalSamlIdpLoginInTestZone1Works() throws Exception { + assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String zoneId = "testzone1"; + + RestTemplate identityClient = IntegrationTestUtils + .getClientCredentialsTemplate(IntegrationTestUtils.getClientCredentialsResource(baseUrl, + new String[] { "zones.write", "zones.read", "scim.zones" }, "identity", "identitysecret")); + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret")); + IdentityZone zone = IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); + String email = new RandomValueStringGenerator().generate() + "@samltesting.org"; + ScimUser user = IntegrationTestUtils.createUser(adminClient, baseUrl, email, "firstname", "lastname", email, + true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, user.getId(), zoneId); + + String zoneAdminToken = IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), "identity", "identitysecret", email, "secr3T"); + + String testZone1Url = baseUrl.replace("localhost", zoneId + ".localhost"); + String zoneAdminClientId = new RandomValueStringGenerator().generate() + "-" + zoneId + "-admin"; + BaseClientDetails clientDetails = new BaseClientDetails(zoneAdminClientId, null, "uaa.none", + "client_credentials", "uaa.admin,scim.read,scim.write,uaa.resource", testZone1Url); + clientDetails.setClientSecret("secret"); + IntegrationTestUtils.createClientAsZoneAdmin(zoneAdminToken, baseUrl, zoneId, clientDetails); + + RestTemplate zoneAdminClient = IntegrationTestUtils.getClientCredentialsTemplate(IntegrationTestUtils + .getClientCredentialsResource(testZone1Url, new String[0], zoneAdminClientId, "secret")); + String zoneUserEmail = new RandomValueStringGenerator().generate() + "@samltesting.org"; + IntegrationTestUtils.createUser(zoneAdminClient, testZone1Url, zoneUserEmail, "Dana", "Scully", zoneUserEmail, + true); + + SamlIdentityProviderDefinition samlIdentityProviderDefinition = createZone1IdpDefinition("unit-test-idp"); + IdentityProvider provider = new IdentityProvider<>(); + provider.setIdentityZoneId(zoneId); + provider.setType(OriginKeys.SAML); + provider.setActive(true); + provider.setConfig(samlIdentityProviderDefinition); + provider.setOriginKey(samlIdentityProviderDefinition.getIdpEntityAlias()); + provider.setName("Local SAML IdP for testzone1"); + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken, baseUrl, provider); + assertNotNull(provider.getId()); + + + SamlServiceProviderDefinition samlServiceProviderDefinition = createZone1SamlSpDefinition("cloudfoundry-saml-login"); + SamlServiceProvider sp = new SamlServiceProvider(); + sp.setIdentityZoneId(zoneId); + sp.setActive(true); + sp.setConfig(samlServiceProviderDefinition); + sp.setEntityId(samlServiceProviderDefinition.getSpEntityId()); + sp.setName("Local SAML SP for testzone1"); + sp = createOrUpdateSamlServiceProvider(zoneAdminToken, baseUrl, sp); + + webDriver.get(baseUrl + "/logout.do"); + webDriver.get(testZone1Url + "/logout.do"); + webDriver.get(testZone1Url + "/login"); + Assert.assertEquals(zone.getName(), webDriver.getTitle()); + + List elements = webDriver + .findElements(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); + assertNotNull(elements); + assertEquals(1, elements.size()); + + WebElement element = elements.get(0); + assertNotNull(element); + element.click(); + webDriver.findElement(By.xpath("//h1[contains(text(), 'Welcome to The Twiglet Zone[" + zoneId + "]!')]")); + webDriver.findElement(By.name("username")).clear(); + webDriver.findElement(By.name("username")).sendKeys(zoneUserEmail); + webDriver.findElement(By.name("password")).sendKeys("secr3T"); + webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); + assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString("Where to?")); + + webDriver.get(baseUrl + "/logout.do"); + webDriver.get(testZone1Url + "/logout.do"); + + // disable the provider + provider.setActive(false); + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken, baseUrl, provider); + assertNotNull(provider.getId()); + webDriver.get(testZone1Url + "/login"); + Assert.assertEquals(zone.getName(), webDriver.getTitle()); + elements = webDriver + .findElements(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); + assertNotNull(elements); + assertEquals(0, elements.size()); + + // enable the provider + provider.setActive(true); + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken, baseUrl, provider); + assertNotNull(provider.getId()); + webDriver.get(testZone1Url + "/login"); + Assert.assertEquals(zone.getName(), webDriver.getTitle()); + elements = webDriver + .findElements(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); + assertNotNull(elements); + assertEquals(1, elements.size()); + } + + /** + * In this test testzone1 acts as the SAML IdP and testzone2 acts as the SAML SP. + */ + @SuppressWarnings("unchecked") + @Test + public void testCrossZoneSamlIntegration() throws Exception { + assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String idpZoneId = "testzone1"; + String spZoneId = "testzone2"; + + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret")); + RestTemplate identityClient = IntegrationTestUtils + .getClientCredentialsTemplate(IntegrationTestUtils.getClientCredentialsResource(baseUrl, + new String[] { "zones.write", "zones.read", "scim.zones" }, "identity", "identitysecret")); + + IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, idpZoneId, idpZoneId); + String idpZoneAdminEmail = new RandomValueStringGenerator().generate() + "@samltesting.org"; + ScimUser idpZoneAdminUser = IntegrationTestUtils.createUser(adminClient, baseUrl, idpZoneAdminEmail, "firstname", "lastname", idpZoneAdminEmail, + true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, idpZoneAdminUser.getId(), idpZoneId); + String idpZoneAdminToken = IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), "identity", "identitysecret", idpZoneAdminEmail, "secr3T"); + + String idpZoneUserEmail = new RandomValueStringGenerator().generate() + "@samltesting.org"; + String idpZoneUrl = baseUrl.replace("localhost", idpZoneId + ".localhost"); + createZoneUser(idpZoneId, idpZoneAdminToken, idpZoneUserEmail, idpZoneUrl); + + SamlConfig samlConfig = new SamlConfig(); + samlConfig.setWantAssertionSigned(true); + IdentityZoneConfiguration config = new IdentityZoneConfiguration(); + config.setSamlConfig(samlConfig); + IdentityZone spZone = IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, spZoneId, spZoneId, config ); + String spZoneAdminEmail = new RandomValueStringGenerator().generate() + "@samltesting.org"; + ScimUser spZoneAdminUser = IntegrationTestUtils.createUser(adminClient, baseUrl, spZoneAdminEmail, "firstname", "lastname", spZoneAdminEmail, + true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, spZoneAdminUser.getId(), spZoneId); + String spZoneAdminToken = IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), "identity", "identitysecret", spZoneAdminEmail, "secr3T"); + String spZoneUrl = baseUrl.replace("localhost", spZoneId + ".localhost"); + + SamlIdentityProviderDefinition samlIdentityProviderDefinition = createZone1IdpDefinition("unit-test-idp"); + IdentityProvider idp = new IdentityProvider<>(); + idp.setIdentityZoneId(spZoneId); + idp.setType(OriginKeys.SAML); + idp.setActive(true); + idp.setConfig(samlIdentityProviderDefinition); + idp.setOriginKey(samlIdentityProviderDefinition.getIdpEntityAlias()); + idp.setName("Local SAML IdP for testzone1"); + idp = IntegrationTestUtils.createOrUpdateProvider(spZoneAdminToken, baseUrl, idp); + assertNotNull(idp.getId()); + + SamlServiceProviderDefinition samlServiceProviderDefinition = createZone2SamlSpDefinition("cloudfoundry-saml-login"); + SamlServiceProvider sp = new SamlServiceProvider(); + sp.setIdentityZoneId(idpZoneId); + sp.setActive(true); + sp.setConfig(samlServiceProviderDefinition); + sp.setEntityId(samlServiceProviderDefinition.getSpEntityId()); + sp.setName("Local SAML SP for testzone2"); + sp = createOrUpdateSamlServiceProvider(idpZoneAdminToken, baseUrl, sp); + + webDriver.get(baseUrl + "/logout.do"); + webDriver.get(spZoneUrl + "/logout.do"); + webDriver.get(spZoneUrl + "/login"); + Assert.assertEquals(spZone.getName(), webDriver.getTitle()); + + List elements = webDriver + .findElements(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); + assertNotNull(elements); + assertEquals(1, elements.size()); + + WebElement element = elements.get(0); + assertNotNull(element); + element.click(); + webDriver.findElement(By.xpath("//h1[contains(text(), 'Welcome to The Twiglet Zone[" + idpZoneId + "]!')]")); + webDriver.findElement(By.name("username")).clear(); + webDriver.findElement(By.name("username")).sendKeys(idpZoneUserEmail); + webDriver.findElement(By.name("password")).sendKeys("secr3T"); + webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); + assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString("Where to?")); + + webDriver.get(baseUrl + "/logout.do"); + webDriver.get(spZoneUrl + "/logout.do"); + + // disable the provider + idp.setActive(false); + idp = IntegrationTestUtils.createOrUpdateProvider(spZoneAdminToken, baseUrl, idp); + assertNotNull(idp.getId()); + webDriver.get(spZoneUrl + "/login"); + Assert.assertEquals(spZone.getName(), webDriver.getTitle()); + elements = webDriver + .findElements(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); + assertNotNull(elements); + assertEquals(0, elements.size()); + + // enable the provider + idp.setActive(true); + idp = IntegrationTestUtils.createOrUpdateProvider(spZoneAdminToken, baseUrl, idp); + assertNotNull(idp.getId()); + webDriver.get(spZoneUrl + "/login"); + Assert.assertEquals(spZone.getName(), webDriver.getTitle()); + elements = webDriver + .findElements(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); + assertNotNull(elements); + assertEquals(1, elements.size()); + } + + private void createZoneUser(String idpZoneId, String zoneAdminToken, String zoneUserEmail, String zoneUrl) throws Exception { + String zoneAdminClientId = new RandomValueStringGenerator().generate() + "-" + idpZoneId + "-admin"; + BaseClientDetails clientDetails = new BaseClientDetails(zoneAdminClientId, null, "uaa.none", + "client_credentials", "uaa.admin,scim.read,scim.write,uaa.resource", zoneUrl); + clientDetails.setClientSecret("secret"); + IntegrationTestUtils.createClientAsZoneAdmin(zoneAdminToken, baseUrl, idpZoneId, clientDetails); + + RestTemplate zoneAdminClient = IntegrationTestUtils.getClientCredentialsTemplate(IntegrationTestUtils + .getClientCredentialsResource(zoneUrl, new String[0], zoneAdminClientId, "secret")); + IntegrationTestUtils.createUser(zoneAdminClient, zoneUrl, zoneUserEmail, "Dana", "Scully", zoneUserEmail, + true); + } + + protected boolean doesSupportZoneDNS() { + try { + return Arrays.equals(Inet4Address.getByName("testzone1.localhost").getAddress(), + new byte[] { 127, 0, 0, 1 }) + && Arrays.equals(Inet4Address.getByName("testzone2.localhost").getAddress(), + new byte[] { 127, 0, 0, 1 }) + && Arrays.equals(Inet4Address.getByName("testzone3.localhost").getAddress(), + new byte[] { 127, 0, 0, 1 }); + } catch (UnknownHostException e) { + return false; + } + } + + public SamlIdentityProviderDefinition createZone1IdpDefinition(String alias) { + return createLocalSamlIdpDefinition(alias, "testzone1"); + } + + public SamlIdentityProviderDefinition createZone2IdpDefinition(String alias) { + return createLocalSamlIdpDefinition(alias, "testzone2"); + } + + public SamlIdentityProviderDefinition createZone3IdpDefinition(String alias) { + return createLocalSamlIdpDefinition(alias, "testzone3"); + } + +} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java index c3805eb8ca0..acd186045c5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java @@ -23,6 +23,7 @@ import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.resources.SearchResults; +import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; @@ -442,6 +443,25 @@ public static IdentityZone createZoneOrUpdateSubdomain(RestTemplate client, return zone.getBody(); } + public static IdentityZone createZoneOrUpdateSubdomain(RestTemplate client, + String url, + String id, + String subdomain, + IdentityZoneConfiguration config) { + + ResponseEntity zoneGet = client.getForEntity(url + "/identity-zones/{id}", String.class, id); + if (zoneGet.getStatusCode()==HttpStatus.OK) { + IdentityZone existing = JsonUtils.readValue(zoneGet.getBody(), IdentityZone.class); + existing.setSubdomain(subdomain); + existing.setConfig(config); + client.put(url + "/identity-zones/{id}", existing, id); + return existing; + } + IdentityZone identityZone = fixtureIdentityZone(id, subdomain, config); + ResponseEntity zone = client.postForEntity(url + "/identity-zones", identityZone, IdentityZone.class); + return zone.getBody(); + } + public static void makeZoneAdmin(RestTemplate client, String url, String userId, @@ -595,8 +615,19 @@ public static List getProviders(String zoneAdminToken, */ public static IdentityProvider createIdentityProvider(String originKey, boolean addShadowUserOnLogin, String baseUrl, ServerRunning serverRunning) throws Exception { String zoneAdminToken = getZoneAdminToken(baseUrl, serverRunning); - SamlIdentityProviderDefinition samlIdentityProviderDefinition = createSimplePHPSamlIDP(originKey, OriginKeys.UAA); + return createIdentityProvider("simplesamlphp for uaa", originKey, addShadowUserOnLogin, baseUrl, serverRunning, samlIdentityProviderDefinition); + } + + /** + * @param originKey The unique identifier used to reference the identity provider in UAA. + * @param addShadowUserOnLogin Specifies whether UAA should automatically create shadow users upon successful SAML authentication. + * @return An object representation of an identity provider. + * @throws Exception on error + */ + public static IdentityProvider createIdentityProvider(String name, String originKey, boolean addShadowUserOnLogin, String baseUrl, ServerRunning serverRunning, SamlIdentityProviderDefinition samlIdentityProviderDefinition) throws Exception { + String zoneAdminToken = getZoneAdminToken(baseUrl, serverRunning); + samlIdentityProviderDefinition.setAddShadowUserOnLogin(addShadowUserOnLogin); IdentityProvider provider = new IdentityProvider(); provider.setIdentityZoneId(OriginKeys.UAA); @@ -604,7 +635,7 @@ public static IdentityProvider createIdentityProvider(String originKey, boolean provider.setActive(true); provider.setConfig(samlIdentityProviderDefinition); provider.setOriginKey(samlIdentityProviderDefinition.getIdpEntityAlias()); - provider.setName("simplesamlphp for uaa"); + provider.setName(name); provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken,baseUrl,provider); assertNotNull(provider.getId()); return provider; @@ -629,6 +660,40 @@ public static String getZoneAdminToken(String baseUrl, ServerRunning serverRunni "secr3T"); } + public static ScimUser createRandomUser(String baseUrl) throws Exception { + + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") + ); + String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; + return IntegrationTestUtils.createUser(adminClient, baseUrl, email, "firstname", "lastname", email, true); + } + + public static IdentityProvider updateIdentityProvider( + String baseUrl, ServerRunning serverRunning, IdentityProvider provider) throws Exception { + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "identity", "identitysecret") + ); + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") + ); + String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; + ScimUser user = IntegrationTestUtils.createUser(adminClient, baseUrl, email, "firstname", "lastname", email, true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, user.getId(), OriginKeys.UAA); + + String zoneAdminToken = + IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), + "identity", + "identitysecret", + email, + "secr3T"); + + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken, baseUrl, provider); + assertNotNull(provider.getId()); + return provider; + } + public static SamlIdentityProviderDefinition createSimplePHPSamlIDP(String alias, String zoneId) { if (!("simplesamlphp".equals(alias) || "simplesamlphp2".equals(alias))) { throw new IllegalArgumentException("Only valid origins are: simplesamlphp,simplesamlphp2"); @@ -720,11 +785,17 @@ public static IdentityProvider createOrUpdateProvider(String accessToken, } public static IdentityZone fixtureIdentityZone(String id, String subdomain) { + + return fixtureIdentityZone(id, subdomain, null); + } + + public static IdentityZone fixtureIdentityZone(String id, String subdomain, IdentityZoneConfiguration config) { IdentityZone identityZone = new IdentityZone(); identityZone.setId(id); identityZone.setSubdomain(subdomain); identityZone.setName("The Twiglet Zone[" + id + "]"); identityZone.setDescription("Like the Twilight Zone but tastier[" + id + "]."); + identityZone.setConfig(config); return identityZone; }