Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SAML NameIDPolicy #439

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<String> supportedNameID = Arrays.asList(NameIDType.EMAIL, NameIDType.PERSISTENT,
NameIDType.UNSPECIFIED);
generator.setNameID(supportedNameID);

EntityDescriptor descriptor = generator.generateMetadata();
ExtendedMetadata extendedMetadata = generator.generateExtendedMetadata();

Expand Down
Expand Up @@ -63,6 +63,7 @@
import java.util.Arrays;
import java.util.List;


public class IdpWebSsoProfileImpl extends WebSSOProfileImpl implements IdpWebSsoProfile {

@Override
Expand All @@ -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();
Expand Down Expand Up @@ -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<Assertion> assertionBuilder = (SAMLObjectBuilder<Assertion>) builderFactory
.getBuilder(Assertion.DEFAULT_ELEMENT_NAME);
Expand All @@ -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;
Expand Down Expand Up @@ -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<Subject> subjectBuilder = (SAMLObjectBuilder<Subject>) builderFactory
.getBuilder(Subject.DEFAULT_ELEMENT_NAME);
Expand All @@ -201,10 +202,32 @@ private void buildAssertionSubject(Assertion assertion, AuthnRequest authnReques
@SuppressWarnings("unchecked")
SAMLObjectBuilder<NameID> nameIdBuilder = (SAMLObjectBuilder<NameID>) 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<SubjectConfirmation> subjectConfirmationBuilder = (SAMLObjectBuilder<SubjectConfirmation>) builderFactory
Expand Down
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -49,6 +56,8 @@ public class SamlServiceProviderConfigurator {
private final Map<IdentityZone, Map<String, SamlServiceProviderHolder>> zoneServiceProviders = new HashMap<>();
private HttpClientParams clientParams;
private BasicParserPool parserPool;
private Set<String> supportedNameIDs = new HashSet<>(Arrays.asList(NameIDType.EMAIL, NameIDType.PERSISTENT,
NameIDType.UNSPECIFIED));

private Timer dummyTimer = new Timer() {

Expand Down Expand Up @@ -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<String, SamlServiceProviderHolder> serviceProviders = getOrCreateSamlServiceProviderMapForZone(zone);

ExtendedMetadataDelegate deleted = null;
Expand Down Expand Up @@ -281,4 +303,8 @@ public BasicParserPool getParserPool() {
public void setParserPool(BasicParserPool parserPool) {
this.parserPool = parserPool;
}

public void setSupportedNameIDs(Set<String> supportedNameIDs) {
this.supportedNameIDs = supportedNameIDs;
}
}
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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();
Expand Down
@@ -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;

Expand All @@ -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;
Expand All @@ -25,13 +29,21 @@ public class SamlServiceProviderConfiguratorTest {

private SamlServiceProviderConfigurator conf = null;

@Rule
public ExpectedException expectedEx = ExpectedException.none();

@Before
public void setup() throws Exception {
samlTestUtils.initalize();
conf = new SamlServiceProviderConfigurator();
conf.setParserPool(new BasicParserPool());
}

@After
public void cleanupTestMethod() {
expectedEx = ExpectedException.none();
}

@Test
public void testAddAndUpdateAndRemoveSamlServiceProvider() throws Exception {
SamlServiceProvider sp = mockSamlServiceProviderForZone("uaa");
Expand All @@ -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,
"<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>");
conf.addSamlServiceProvider(sp);
}

@Test(expected = IllegalArgumentException.class)
public void testAddSamlServiceProviderToWrongZone() throws Exception {
SamlServiceProvider sp = mockSamlServiceProviderForZone("uaa");
Expand Down