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 index 82d4ca85ede..3b82958fa75 100644 --- 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 @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.provider.saml.idp; +import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.provider.MetadataProvider; import org.opensaml.saml2.metadata.provider.MetadataProviderException; @@ -33,6 +34,8 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; /** * The filter expects calls on configured URL and presents user with SAML2 metadata representing this application @@ -128,6 +131,11 @@ protected void processMetadataInitialization(HttpServletRequest request) throws generator.setEntityId(getDefaultEntityID(baseURL, alias)); } + // Ensure supported nameID formats in uaa are listed in the metadata + Collection supportedNameID = Arrays.asList(NameIDType.EMAIL, NameIDType.PERSISTENT, + NameIDType.UNSPECIFIED); + generator.setNameID(supportedNameID); + EntityDescriptor descriptor = generator.generateMetadata(); ExtendedMetadata extendedMetadata = generator.generateExtendedMetadata(); 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 index 59c07c81705..d7950528f7a 100644 --- 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 @@ -63,6 +63,7 @@ import java.util.Arrays; import java.util.List; + public class IdpWebSsoProfileImpl extends WebSSOProfileImpl implements IdpWebSsoProfile { @Override @@ -78,7 +79,7 @@ public void sendResponse(Authentication authentication, SAMLMessageContext conte @SuppressWarnings("unchecked") protected void buildResponse(Authentication authentication, SAMLMessageContext context, IdpWebSSOProfileOptions options) - throws MetadataProviderException, SecurityException, MarshallingException, SignatureException { + throws MetadataProviderException, SecurityException, MarshallingException, SignatureException, SAMLException { IDPSSODescriptor idpDescriptor = (IDPSSODescriptor) context.getLocalEntityRoleMetadata(); SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getPeerEntityRoleMetadata(); AuthnRequest authnRequest = (AuthnRequest) context.getInboundSAMLMessage(); @@ -126,7 +127,7 @@ private void buildCommonAttributes(String localEntityId, Response response, Endp } private Assertion buildAssertion(Authentication authentication, AuthnRequest authnRequest, - IdpWebSSOProfileOptions options, String audienceURI, String issuerEntityId) { + IdpWebSSOProfileOptions options, String audienceURI, String issuerEntityId) throws SAMLException{ @SuppressWarnings("unchecked") SAMLObjectBuilder assertionBuilder = (SAMLObjectBuilder) builderFactory .getBuilder(Assertion.DEFAULT_ELEMENT_NAME); @@ -139,7 +140,7 @@ private Assertion buildAssertion(Authentication authentication, AuthnRequest aut buildAssertionAuthnStatement(assertion); buildAssertionConditions(assertion, options.getAssertionTimeToLiveSeconds(), audienceURI); buildAssertionSubject(assertion, authnRequest, options.getAssertionTimeToLiveSeconds(), - authentication.getName()); + (UaaPrincipal) authentication.getPrincipal()); buildAttributeStatement(assertion, authentication); return assertion; @@ -192,7 +193,7 @@ private void buildAssertionConditions(Assertion assertion, int assertionTtlSecon } private void buildAssertionSubject(Assertion assertion, AuthnRequest authnRequest, int assertionTtlSeconds, - String nameIdStr) { + UaaPrincipal uaaPrincipal) throws SAMLException { @SuppressWarnings("unchecked") SAMLObjectBuilder subjectBuilder = (SAMLObjectBuilder) builderFactory .getBuilder(Subject.DEFAULT_ELEMENT_NAME); @@ -201,10 +202,32 @@ private void buildAssertionSubject(Assertion assertion, AuthnRequest authnReques @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); + NameID nameID = nameIdBuilder.buildObject(); + + String nameIDFormat = NameIDType.UNSPECIFIED; + String nameIdStr = uaaPrincipal.getName(); + if(null != authnRequest.getSubject() && null != authnRequest.getSubject().getNameID() + && null != authnRequest.getSubject().getNameID().getFormat()){ + + nameIDFormat = authnRequest.getSubject().getNameID().getFormat(); + switch (nameIDFormat) { + case NameIDType.EMAIL: + nameIdStr = uaaPrincipal.getEmail(); + break; + case NameIDType.PERSISTENT: + nameIdStr = uaaPrincipal.getId(); + break; + case NameIDType.UNSPECIFIED: + nameIdStr = uaaPrincipal.getName(); + break; + default: + throw new SAMLException("The NameIDType '" + nameIDFormat + "' is not supported."); + } + } + + nameID.setValue(nameIdStr); + nameID.setFormat(nameIDFormat); + subject.setNameID(nameID); @SuppressWarnings("unchecked") SAMLObjectBuilder subjectConfirmationBuilder = (SAMLObjectBuilder) builderFactory 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 index d8b060caa8a..f21fbdb80d5 100644 --- 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 @@ -21,6 +21,10 @@ import org.cloudfoundry.identity.uaa.provider.saml.FixedHttpMetaDataProvider; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.metadata.NameIDFormat; +import org.opensaml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.xml.parse.BasicParserPool; import org.springframework.security.saml.metadata.ExtendedMetadata; @@ -40,6 +44,9 @@ import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; /** * Holds internal state of available SAML Service Providers. @@ -49,6 +56,8 @@ public class SamlServiceProviderConfigurator { private final Map> zoneServiceProviders = new HashMap<>(); private HttpClientParams clientParams; private BasicParserPool parserPool; + private Set supportedNameIDs = new HashSet<>(Arrays.asList(NameIDType.EMAIL, NameIDType.PERSISTENT, + NameIDType.UNSPECIFIED)); private Timer dummyTimer = new Timer() { @@ -165,6 +174,19 @@ synchronized ExtendedMetadataDelegate[] addSamlServiceProvider(SamlServiceProvid "Metadata entity id does not match SAML SP entity id: " + provider.getEntityId()); } + // Initializing here is necessary to access the SPSSODescriptor, otherwise an exception is thrown. + added.initialize(); + SPSSODescriptor spSsoDescriptor = added.getEntityDescriptor(metadataEntityId). + getSPSSODescriptor(SAMLConstants.SAML20P_NS); + if (null != spSsoDescriptor.getNameIDFormats() && !spSsoDescriptor.getNameIDFormats().isEmpty()) { + // The SP explicitly states the NameID formats it supports, we should check that we support at least one. + if (!spSsoDescriptor.getNameIDFormats().stream().anyMatch( + format -> this.supportedNameIDs.contains(format.getFormat()))) { + throw new MetadataProviderException( + "UAA does not support any of the NameIDFormats specified in the metadata for entity: " + + provider.getEntityId()); + } + } Map serviceProviders = getOrCreateSamlServiceProviderMapForZone(zone); ExtendedMetadataDelegate deleted = null; @@ -281,4 +303,8 @@ public BasicParserPool getParserPool() { public void setParserPool(BasicParserPool parserPool) { this.parserPool = parserPool; } + + public void setSupportedNameIDs(Set supportedNameIDs) { + this.supportedNameIDs = supportedNameIDs; + } } 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 index 7ae736d0895..f9268aea6b0 100644 --- 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 @@ -18,6 +18,7 @@ import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.core.SubjectConfirmation; import org.opensaml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.ws.message.encoder.MessageEncodingException; import org.opensaml.xml.ConfigurationException; @@ -37,6 +38,90 @@ public void setup() throws ConfigurationException { samlTestUtils.initalize(); } + @Test + public void testBuildResponseForSamlRequestWithPersistentNameID() throws MessageEncodingException, SAMLException, + MetadataProviderException, SecurityException, MarshallingException, SignatureException { + IdpWebSsoProfileImpl profile = new IdpWebSsoProfileImpl(); + + String authenticationId = UUID.randomUUID().toString(); + Authentication authentication = samlTestUtils.mockUaaAuthentication(authenticationId); + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext( + samlTestUtils.mockAuthnRequest(NameIDType.PERSISTENT)); + + 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(authenticationId, subject.getNameID().getValue()); + assertEquals(NameIDType.PERSISTENT, subject.getNameID().getFormat()); + + SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0); + SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData(); + assertEquals(request.getID(), subjectConfirmationData.getInResponseTo()); + + verifyAssertionAttributes(authenticationId, assertion); + } + + @Test + public void testBuildResponseForSamlRequestWithUnspecifiedNameID() throws MessageEncodingException, SAMLException, + MetadataProviderException, SecurityException, MarshallingException, SignatureException { + IdpWebSsoProfileImpl profile = new IdpWebSsoProfileImpl(); + + String authenticationId = UUID.randomUUID().toString(); + Authentication authentication = samlTestUtils.mockUaaAuthentication(authenticationId); + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext( + samlTestUtils.mockAuthnRequest(NameIDType.UNSPECIFIED)); + + 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()); + assertEquals(NameIDType.UNSPECIFIED, subject.getNameID().getFormat()); + + SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0); + SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData(); + assertEquals(request.getID(), subjectConfirmationData.getInResponseTo()); + + verifyAssertionAttributes(authenticationId, assertion); + } + + @Test + public void testBuildResponseForSamlRequestWithEmailAddressNameID() throws MessageEncodingException, SAMLException, + MetadataProviderException, SecurityException, MarshallingException, SignatureException { + IdpWebSsoProfileImpl profile = new IdpWebSsoProfileImpl(); + + String authenticationId = UUID.randomUUID().toString(); + Authentication authentication = samlTestUtils.mockUaaAuthentication(authenticationId); + SAMLMessageContext context = samlTestUtils.mockSamlMessageContext( + samlTestUtils.mockAuthnRequest(NameIDType.EMAIL)); + + 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@testing.org", subject.getNameID().getValue()); + assertEquals(NameIDType.EMAIL, subject.getNameID().getFormat()); + + SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0); + SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData(); + assertEquals(request.getID(), subjectConfirmationData.getInResponseTo()); + + verifyAssertionAttributes(authenticationId, assertion); + } + @Test public void testBuildResponse() throws MessageEncodingException, SAMLException, MetadataProviderException, SecurityException, MarshallingException, SignatureException { @@ -55,6 +140,7 @@ public void testBuildResponse() throws MessageEncodingException, SAMLException, Assertion assertion = response.getAssertions().get(0); Subject subject = assertion.getSubject(); assertEquals("marissa", subject.getNameID().getValue()); + assertEquals(NameIDType.UNSPECIFIED, subject.getNameID().getFormat()); SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0); SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData(); 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 index 4d3872c5292..dac8b298ea1 100644 --- 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 @@ -1,8 +1,9 @@ package org.cloudfoundry.identity.uaa.provider.saml.idp; -import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.MOCK_SP_ENTITY_ID; import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.mockSamlServiceProviderForZone; import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.mockSamlServiceProviderWithoutXmlHeaderInMetadata; +import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.mockSamlServiceProvider; +import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.MOCK_SP_ENTITY_ID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -13,8 +14,11 @@ import org.cloudfoundry.identity.uaa.provider.saml.ComparableProvider; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.xml.parse.BasicParserPool; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; @@ -25,6 +29,9 @@ public class SamlServiceProviderConfiguratorTest { private SamlServiceProviderConfigurator conf = null; + @Rule + public ExpectedException expectedEx = ExpectedException.none(); + @Before public void setup() throws Exception { samlTestUtils.initalize(); @@ -32,6 +39,11 @@ public void setup() throws Exception { conf.setParserPool(new BasicParserPool()); } + @After + public void cleanupTestMethod() { + expectedEx = ExpectedException.none(); + } + @Test public void testAddAndUpdateAndRemoveSamlServiceProvider() throws Exception { SamlServiceProvider sp = mockSamlServiceProviderForZone("uaa"); @@ -45,6 +57,28 @@ public void testAddAndUpdateAndRemoveSamlServiceProvider() throws Exception { assertEquals(0, conf.getSamlServiceProviders().size()); } + @Test + public void testAddSamlServiceProviderWithNoNameIDFormats() throws Exception { + SamlServiceProvider sp = mockSamlServiceProvider("uaa", ""); + + assertEquals(0, conf.getSamlServiceProviders().size()); + conf.addSamlServiceProvider(sp); + assertEquals(1, conf.getSamlServiceProviders().size()); + conf.removeSamlServiceProvider(sp.getEntityId()); + assertEquals(0, conf.getSamlServiceProviders().size()); + } + + @Test + public void testAddSamlServiceProviderWithUnsupportedNameIDFormats() throws Exception { + String entityId = "uaa"; + expectedEx.expect(MetadataProviderException.class); + expectedEx.expectMessage("UAA does not support any of the NameIDFormats specified in the metadata for entity: " + + entityId); + SamlServiceProvider sp = mockSamlServiceProvider(entityId, + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"); + conf.addSamlServiceProvider(sp); + } + @Test(expected = IllegalArgumentException.class) public void testAddSamlServiceProviderToWrongZone() throws Exception { SamlServiceProvider sp = mockSamlServiceProviderForZone("uaa"); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlTestUtils.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlTestUtils.java index ae336526f89..f7b854cee87 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlTestUtils.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlTestUtils.java @@ -25,6 +25,8 @@ import org.opensaml.common.SAMLVersion; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.Subject; +import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml2.metadata.SPSSODescriptor; @@ -91,6 +93,11 @@ public void initalize() throws ConfigurationException { @SuppressWarnings("unchecked") public SAMLMessageContext mockSamlMessageContext() { + return mockSamlMessageContext(mockAuthnRequest()); + } + + @SuppressWarnings("unchecked") + public SAMLMessageContext mockSamlMessageContext(AuthnRequest authnRequest) { SAMLMessageContext context = new SAMLMessageContext(); context.setLocalEntityId(IDP_ENTITY_ID); @@ -106,8 +113,6 @@ public SAMLMessageContext mockSamlMessageContext() { context.setPeerEntityMetadata(spMetadata); SPSSODescriptor spDescriptor = spMetadata.getSPSSODescriptor(SAML20P_NS); context.setPeerEntityRoleMetadata(spDescriptor); - - AuthnRequest authnRequest = mockAuthnRequest(); context.setInboundSAMLMessage(authnRequest); KeyManager keyManager = SamlKeyManagerFactory.getKeyManager(PROVIDER_PRIVATE_KEY, PROVIDER_PRIVATE_KEY_PASSWORD, PROVIDER_CERTIFICATE); @@ -145,6 +150,10 @@ public EntityDescriptor mockSpMetadata() { } public AuthnRequest mockAuthnRequest() { + return mockAuthnRequest(null); + } + + public AuthnRequest mockAuthnRequest(String nameIDFormat) { @SuppressWarnings("unchecked") SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); @@ -154,6 +163,15 @@ public AuthnRequest mockAuthnRequest() { request.setIssuer(getIssuer(SP_ENTITY_ID)); request.setVersion(SAMLVersion.VERSION_20); request.setIssueInstant(new DateTime()); + if (null != nameIDFormat) { + NameID nameID = ((SAMLObjectBuilder) builderFactory.getBuilder(NameID.DEFAULT_ELEMENT_NAME)) + .buildObject(); + nameID.setFormat(nameIDFormat); + Subject subject = ((SAMLObjectBuilder) builderFactory.getBuilder(Subject.DEFAULT_ELEMENT_NAME)) + .buildObject(); + subject.setNameID(nameID); + request.setSubject(subject); + } return request; } @@ -466,16 +484,19 @@ public UaaAuthentication mockUaaAuthentication(String id) { + "" + "" + "" - + "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" + + "%s" + "" + "" + "" + ""; + public static final String DEFAULT_NAME_ID_FORMATS = + "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("", ""); public static final String MOCK_SP_ENTITY_ID = "cloudfoundry-saml-login"; @@ -486,14 +507,19 @@ public static SamlServiceProvider mockSamlServiceProviderForZone(String zoneId) new RandomValueStringGenerator().generate())) .setNameID("sample-nameID").setSingleSignOnServiceIndex(1) .setMetadataTrustCheck(true).build(); + return new SamlServiceProvider().setEntityId(MOCK_SP_ENTITY_ID).setIdentityZoneId(zoneId) .setConfig(singleAddDef); } public static SamlServiceProvider mockSamlServiceProvider(String entityId) { + return mockSamlServiceProvider(entityId, DEFAULT_NAME_ID_FORMATS); + } + + public static SamlServiceProvider mockSamlServiceProvider(String entityId, String nameIdFormatsXML) { SamlServiceProviderDefinition singleAddDef = SamlServiceProviderDefinition.Builder.get() .setMetaDataLocation(String.format(SamlTestUtils.UNSIGNED_SAML_SP_METADATA_ID_AND_ENTITY_ID, - new RandomValueStringGenerator().generate(), entityId)) + new RandomValueStringGenerator().generate(), entityId, nameIdFormatsXML)) .setNameID("sample-nameID").setSingleSignOnServiceIndex(1) .setMetadataTrustCheck(true).build(); return new SamlServiceProvider().setEntityId(entityId).setIdentityZoneId("uaa") diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIDPRefreshMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIDPRefreshMockMvcTests.java index a5b11630181..388e5a6fd63 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIDPRefreshMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIDPRefreshMockMvcTests.java @@ -20,6 +20,7 @@ import org.cloudfoundry.identity.uaa.provider.IdentityProviderEndpoints; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.saml.idp.IdpMetadataGenerator; import org.cloudfoundry.identity.uaa.test.MockAuthentication; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; @@ -32,20 +33,29 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.opensaml.saml2.metadata.NameIDFormat; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.saml.key.KeyManager; import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; +import org.opensaml.saml2.core.NameIDType; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import static org.springframework.http.MediaType.TEXT_HTML; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -491,6 +501,33 @@ public void metadataInZoneContainsCorrectCertificate() throws Exception { .andExpect(content().string(containsString("MIIB1TCCAT4CCQCpQCfJYT8ZJTANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDFCRz"))); } + @Test + public void test_nameID_formats() throws Exception { + IdpMetadataGenerator generator = new IdpMetadataGenerator(); + generator.setKeyManager(mock(KeyManager.class)); + generator.setEntityId("Test-Entity"); + generator.setEntityBaseURL("/test/entity"); + generator.setNameID(Arrays.asList(NameIDType.EMAIL, NameIDType.PERSISTENT, + NameIDType.UNSPECIFIED)); + List formats = generator.generateMetadata().getIDPSSODescriptor("urn:oasis:names:tc:SAML:2.0:protocol").getNameIDFormats(); + List nameIDFormats = formats.stream().map(format -> format.getFormat()).collect(Collectors.toList()); + + //Uaa supported formats + assertTrue(nameIDFormats.contains(NameIDType.EMAIL)); + assertTrue(nameIDFormats.contains(NameIDType.PERSISTENT)); + assertTrue(nameIDFormats.contains(NameIDType.UNSPECIFIED)); + //Uaa unsupported formats + assertFalse(nameIDFormats.contains(NameIDType.TRANSIENT)); + assertFalse(nameIDFormats.contains(NameIDType.ENCRYPTED)); + assertFalse(nameIDFormats.contains(NameIDType.ENTITY)); + assertFalse(nameIDFormats.contains(NameIDType.KERBEROS)); + assertFalse(nameIDFormats.contains(NameIDType.NAME_QUALIFIER_ATTRIB_NAME)); + assertFalse(nameIDFormats.contains(NameIDType.SP_NAME_QUALIFIER_ATTRIB_NAME)); + assertFalse(nameIDFormats.contains(NameIDType.SPPROVIDED_ID_ATTRIB_NAME)); + assertFalse(nameIDFormats.contains(NameIDType.X509_SUBJECT)); + assertFalse(nameIDFormats.contains(NameIDType.WIN_DOMAIN_QUALIFIED)); + } + @Test public void test_zone_saml_properties() throws Exception { String zone1Name = new RandomValueStringGenerator().generate(); @@ -545,6 +582,57 @@ public void test_zone_saml_properties() throws Exception { } + @Test + public void metadataGeneratesCorrectNameIdFormats() throws Exception { + getMockMvc().perform( + get("/saml/idp/metadata")) + //positive tests... + .andExpect(status().isOk()) + .andExpect(content().string(containsString(NameIDType.EMAIL))) + .andExpect(content().string(containsString(NameIDType.UNSPECIFIED))) + .andExpect(content().string(containsString(NameIDType.PERSISTENT))) + //negative tests... + .andExpect(content().string(not(containsString(NameIDType.TRANSIENT)))) + .andExpect(content().string(not(containsString(NameIDType.ENCRYPTED)))) + .andExpect(content().string(not(containsString(NameIDType.ENTITY)))) + .andExpect(content().string(not(containsString(NameIDType.KERBEROS)))) + .andExpect(content().string(not(containsString(NameIDType.NAME_QUALIFIER_ATTRIB_NAME)))) + .andExpect(content().string(not(containsString(NameIDType.SP_NAME_QUALIFIER_ATTRIB_NAME)))) + .andExpect(content().string(not(containsString(NameIDType.SPPROVIDED_ID_ATTRIB_NAME)))) + .andExpect(content().string(not(containsString(NameIDType.WIN_DOMAIN_QUALIFIED)))) + .andExpect(content().string(not(containsString(NameIDType.X509_SUBJECT)))); + } + + @Test + public void metadataInZoneGeneratesSupportedNameIdFormats() throws Exception { + String zoneName = new RandomValueStringGenerator().generate(); + IdentityZone zone = new IdentityZone(); + zone.setName(zoneName); + zone.setSubdomain(zoneName); + zone.setId(zoneName); + zone = zoneProvisioning.create(zone); + + getMockMvc().perform( + get("/saml/idp/metadata") + .with(new SetServerNameRequestPostProcessor(zone.getSubdomain() + ".localhost"))) + //positive tests... + .andExpect(status().isOk()) + .andExpect(content().string(containsString(NameIDType.EMAIL))) + .andExpect(content().string(containsString(NameIDType.UNSPECIFIED))) + .andExpect(content().string(containsString(NameIDType.PERSISTENT))) + //negative tests... + .andExpect(content().string(not(containsString(NameIDType.TRANSIENT)))) + .andExpect(content().string(not(containsString(NameIDType.ENCRYPTED)))) + .andExpect(content().string(not(containsString(NameIDType.ENTITY)))) + .andExpect(content().string(not(containsString(NameIDType.KERBEROS)))) + .andExpect(content().string(not(containsString(NameIDType.NAME_QUALIFIER_ATTRIB_NAME)))) + .andExpect(content().string(not(containsString(NameIDType.SP_NAME_QUALIFIER_ATTRIB_NAME)))) + .andExpect(content().string(not(containsString(NameIDType.SPPROVIDED_ID_ATTRIB_NAME)))) + .andExpect(content().string(not(containsString(NameIDType.WIN_DOMAIN_QUALIFIED)))) + .andExpect(content().string(not(containsString(NameIDType.X509_SUBJECT)))); + } + + public IdentityProvider createSamlProvider(String metadata, String alias, String linkText) throws Exception { SamlIdentityProviderDefinition definition = createSimplePHPSamlIDP(IdentityZone.getUaa().getId(), metadata, alias, linkText); IdentityProvider provider = new IdentityProvider();