* Added two unit tests to SecurityUtilsTest to verify the found email address is correct
* Removed unused imports in SecurityUtilsTest
* Added unit tests for ServicesConfigReader while investigating DRYD-1702
* cleaned up leftover printlns in SecurityUtilsTest
* cleaned up imports in SecurityUtilsTest
* reorganized methods in ServicesConfigReaderImplTest
* refactored findUser method to make it easier to test and prepare it for deprecation of ReponseToken
* refactored some useful common code out of SecurityUtilsTest into AbstractSecurityTestBase
* refactored some of the SAML-object-creating utility methods
* made 'parse' methods public so they can be tested
* added unit test to check that 'identifier' probe assertions correctly pull out attribute values
* source cleanup: organizing imports, formatting
* added .mvn to .gitignore to ignore local, per-developer maven properties
---------
Co-authored-by: Anthony Bucci <abucci@bucci.onl>
*.diff
logged_schemas
logs
+.mvn
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
public class CSpaceSaml2ResponseAuthenticationConverter implements Converter<ResponseToken, CSpaceSaml2Authentication> {
- private final Logger logger = LoggerFactory.getLogger(CSpaceSaml2ResponseAuthenticationConverter.class);
-
- private CSpaceUserDetailsService userDetailsService;
-
- public CSpaceSaml2ResponseAuthenticationConverter(CSpaceUserDetailsService userDetailsService) {
- this.userDetailsService = userDetailsService;
- }
-
- @Override
- public CSpaceSaml2Authentication convert(ResponseToken responseToken) {
- Saml2Authentication authentication = OpenSamlAuthenticationProvider
- .createDefaultResponseAuthenticationConverter()
- .convert(responseToken);
-
- String registrationId = responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
- ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
- SAMLRelyingPartyType relyingPartyRegistration = ConfigUtils.getSAMLRelyingPartyRegistration(serviceConfig, registrationId);
- CSpaceUser user = findUser(relyingPartyRegistration, responseToken);
-
- if (user != null) {
- return new CSpaceSaml2Authentication(user, authentication);
- }
-
- return null;
- }
-
- /**
- * Attempt to find a CSpace user for a SAML response.
- *
- * @param relyingPartyRegistration
- * @param responseToken
- * @return
- */
- private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, ResponseToken responseToken) {
- AssertionProbesType assertionSsoIdProbes = (
- relyingPartyRegistration != null
- ? relyingPartyRegistration.getAssertionSsoIdProbes()
- : null
- );
-
- AssertionProbesType assertionUsernameProbes = (
- relyingPartyRegistration != null
- ? relyingPartyRegistration.getAssertionUsernameProbes()
- : null
- );
-
- List<String> attemptedUsernames = new ArrayList<>();
- List<Assertion> assertions = responseToken.getResponse().getAssertions();
-
- SecurityUtils.logSamlAssertions(assertions);
-
- for (Assertion assertion : assertions) {
- CSpaceUser user = null;
- String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes);
-
- // First, look for a CSpace user whose SSO ID is the ID in the assertion.
-
- if (ssoId != null) {
- try {
- user = (CSpaceUser) userDetailsService.loadUserBySsoId(ssoId);
- }
- catch (UsernameNotFoundException e) {
- }
- }
-
- if (user != null) {
- return user;
- }
-
- // Next, look for a CSpace user whose username is the email address in the assertion.
-
- Set<String> candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(assertion, assertionUsernameProbes);
-
- for (String candidateUsername : candidateUsernames) {
- try {
- user = (CSpaceUser) userDetailsService.loadUserByUsername(candidateUsername);
-
- if (user != null) {
- String expectedSsoId = user.getSsoId();
-
- if (expectedSsoId == null) {
- // Store the ID from the IdP to use in future log ins. Note that this does not save
- // the SSO ID to the database. That happens in CSpaceAuthenticationSuccessEvent.
-
- user.setSsoId(ssoId);
-
- // TODO: If the email address in the assertion differs from the CSpace user's email,
- // update the CSpace user.
- } else if (!StringUtils.equals(expectedSsoId, ssoId)) {
- // If the user previously logged in via SSO, but they had a different ID from the
- // IdP, something's wrong. (Did an account on the IdP get assigned an email that
- // previously belonged to a different account on the IdP?)
-
- logger.warn("User with username {} has expected SSO ID {}, but received {} in SAML assertion",
- candidateUsername, expectedSsoId, ssoId);
-
- user = null;
- }
-
- if (user != null) {
- return user;
- }
- }
- }
- catch(UsernameNotFoundException e) {
- }
- }
-
- attemptedUsernames.addAll(candidateUsernames);
- }
-
- // No CSpace user was found for this SAML response.
- // TODO: Auto-create a CSpace user, using the display name, email address, and ID in the response.
-
- String errorMessage = attemptedUsernames.size() == 0
- ? "The SAML response did not contain a CollectionSpace username."
- : "No CollectionSpace account found for " + StringUtils.join(attemptedUsernames, " / ") + ".";
-
- throw(new UsernameNotFoundException(errorMessage));
- }
+ private final Logger logger = LoggerFactory.getLogger(CSpaceSaml2ResponseAuthenticationConverter.class);
+
+ private CSpaceUserDetailsService userDetailsService;
+
+ public CSpaceSaml2ResponseAuthenticationConverter(CSpaceUserDetailsService userDetailsService) {
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ public CSpaceSaml2Authentication convert(ResponseToken responseToken) {
+ Saml2Authentication authentication = OpenSamlAuthenticationProvider
+ .createDefaultResponseAuthenticationConverter().convert(responseToken);
+
+ String registrationId = responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
+ ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+ SAMLRelyingPartyType relyingPartyRegistration = ConfigUtils.getSAMLRelyingPartyRegistration(serviceConfig,
+ registrationId);
+ CSpaceUser user = findUser(relyingPartyRegistration, responseToken);
+
+ if (user != null) {
+ return new CSpaceSaml2Authentication(user, authentication);
+ }
+
+ return null;
+ }
+
+ /**
+ * Attempt to find a CSpace user for from a list of relying parties
+ * (from the services configuration) and a list of assertions (from the SAML response)
+ *
+ * @param relyingPartyRegistration object representing relying parties from the
+ * service config
+ * @param assertions list of SAML assertions to test
+ * @return the found CSpace user, if any
+ */
+ private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, List<Assertion> assertions) {
+ AssertionProbesType assertionSsoIdProbes = (relyingPartyRegistration != null
+ ? relyingPartyRegistration.getAssertionSsoIdProbes()
+ : null);
+
+ AssertionProbesType assertionUsernameProbes = (relyingPartyRegistration != null
+ ? relyingPartyRegistration.getAssertionUsernameProbes()
+ : null);
+
+ List<String> attemptedUsernames = new ArrayList<>();
+
+ SecurityUtils.logSamlAssertions(assertions);
+
+ for (Assertion assertion : assertions) {
+ CSpaceUser user = null;
+ String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes);
+
+ // First, look for a CSpace user whose SSO ID is the ID in the assertion.
+
+ if (ssoId != null) {
+ try {
+ user = (CSpaceUser) userDetailsService.loadUserBySsoId(ssoId);
+ } catch (UsernameNotFoundException e) {
+ }
+ }
+
+ if (user != null) {
+ return user;
+ }
+
+ // Next, look for a CSpace user whose username is the email address in the
+ // assertion.
+
+ Set<String> candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(assertion,
+ assertionUsernameProbes);
+
+ for (String candidateUsername : candidateUsernames) {
+ try {
+ user = (CSpaceUser) userDetailsService.loadUserByUsername(candidateUsername);
+
+ if (user != null) {
+ String expectedSsoId = user.getSsoId();
+
+ if (expectedSsoId == null) {
+ // Store the ID from the IdP to use in future log ins. Note that this does not
+ // save
+ // the SSO ID to the database. That happens in CSpaceAuthenticationSuccessEvent.
+
+ user.setSsoId(ssoId);
+
+ // TODO: If the email address in the assertion differs from the CSpace user's
+ // email,
+ // update the CSpace user.
+ } else if (!StringUtils.equals(expectedSsoId, ssoId)) {
+ // If the user previously logged in via SSO, but they had a different ID from
+ // the
+ // IdP, something's wrong. (Did an account on the IdP get assigned an email that
+ // previously belonged to a different account on the IdP?)
+
+ logger.warn(
+ "User with username {} has expected SSO ID {}, but received {} in SAML assertion",
+ candidateUsername, expectedSsoId, ssoId);
+
+ user = null;
+ }
+
+ if (user != null) {
+ return user;
+ }
+ }
+ } catch (UsernameNotFoundException e) {
+ }
+ }
+
+ attemptedUsernames.addAll(candidateUsernames);
+ }
+
+ // No CSpace user was found for this SAML response.
+ // TODO: Auto-create a CSpace user, using the display name, email address, and
+ // ID in the response.
+
+ String errorMessage = attemptedUsernames.size() == 0
+ ? "The SAML response did not contain a CollectionSpace username."
+ : "No CollectionSpace account found for " + StringUtils.join(attemptedUsernames, " / ") + ".";
+
+ throw (new UsernameNotFoundException(errorMessage));
+ }
+
+ /**
+ * Attempt to find a CSpace user for a SAML response.
+ *
+ * @deprecated
+ * @param relyingPartyRegistration
+ * @param responseToken
+ * @return a CSpace user
+ */
+ private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, ResponseToken responseToken) {
+ List<Assertion> assertions = responseToken.getResponse().getAssertions();
+ return findUser(relyingPartyRegistration,assertions);
+ }
}
--- /dev/null
+package org.collectionspace.services.common.test;
+
+import java.io.ByteArrayInputStream;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.namespace.QName;
+
+import org.collectionspace.services.common.config.ServicesConfigReaderImpl;
+import org.collectionspace.services.config.ServiceConfig;
+import org.joda.time.DateTime;
+import org.opensaml.core.config.ConfigurationService;
+import org.opensaml.core.config.InitializationException;
+import org.opensaml.core.config.InitializationService;
+import org.opensaml.core.xml.XMLObjectBuilder;
+import org.opensaml.core.xml.XMLObjectBuilderFactory;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.schema.XSAny;
+import org.opensaml.core.xml.schema.XSString;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.AttributeValue;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Subject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeSuite;
+
+public class AbstractSecurityTestBase {
+ private static final Logger logger = LoggerFactory.getLogger(SecurityUtilsTest.class);
+ protected static String BANNER = "-------------------------------------------------------";
+ protected static String FRIENDLY_ATTR_NAME = "mail";
+ protected static String ATTR_NAME = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
+ protected static String ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri";
+ protected static String EMAIL_ADDRESS = "example@example.org";
+ protected static final String USERNAME_ATTRIBUTE = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
+ protected static final String SSOID_ATTRIBUTE = "http://schemas.auth0.com/identifier";
+ protected static final String SSO_CONFIG_STRING = createDefaultTestConfig();
+
+ protected static String createDefaultTestConfig() {
+ return createTestConfig(USERNAME_ATTRIBUTE, SSOID_ATTRIBUTE);
+ }
+
+ protected static String createTestConfig(String usernameAttribute, String ssoAttribute) {
+ return new StringBuilder().append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<svc:service-config xmlns:svc='http://collectionspace.org/services/config'>")
+ .append("<security>").append("<sso>").append("<saml>").append("<single-logout />")
+ .append("<relying-party-registrations>").append("<relying-party id=\"auth0\">")
+ .append("<name>Auth0 - Scenario 11</name>")
+ .append("<icon location=\"https://cdn.auth0.com/manhattan/versions/1.4478.0/assets/badge.png\" />")
+ .append("<metadata location=\"https://dev-cf0ltyyfory6gtqm.us.auth0.com/samlp/metadata/ZXtZfEN0mj96GP8LCmEUWcpuDO0OtqKY\" />")
+ .append("<assertion-username-probes>").append("<attribute name=\"" + usernameAttribute + "\" />")
+ .append("</assertion-username-probes>").append("<assertion-sso-id-probes>")
+ .append("<attribute name=\"" + ssoAttribute + "\" />").append("</assertion-sso-id-probes>")
+ .append("</relying-party>").append("</relying-party-registrations>").append("</saml>")
+ .append("</sso>\n").append("</security>").append("</svc:service-config>").toString();
+ }
+
+ protected static final String MOCK_ROOT_DIR = "./";
+
+ protected ServiceConfig parseServiceConfigString() throws JAXBException {
+ return parseServiceConfigString(MOCK_ROOT_DIR, SSO_CONFIG_STRING);
+ }
+
+ protected ServiceConfig parseServiceConfigString(String mockRootDir, String seviceConfigString)
+ throws JAXBException {
+ ServicesConfigReaderImpl rdr = new ServicesConfigReaderImpl(mockRootDir);
+ ByteArrayInputStream in = new ByteArrayInputStream(seviceConfigString.getBytes());
+ try {
+ serviceConfig = (ServiceConfig) rdr.parse(in, ServiceConfig.class);
+ } catch (JAXBException e) {
+ logger.warn("Could not create test service config: " + e.getLocalizedMessage());
+ throw e;
+ }
+ return serviceConfig;
+ }
+
+ /* for mocking useful SAML objects */
+ protected <T extends SAMLObject> T createNewSAMLObject(Class<T> clazz)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
+ QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null);
+
+ @SuppressWarnings("unchecked") // NOTE: the T extends SAMLObject ought to guarantee this works
+ T theObject = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName);
+ return theObject;
+ }
+
+ protected XSString createNewXSString(String value) {
+ XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
+ @SuppressWarnings("unchecked")
+ XMLObjectBuilder<XSString> stringBuilder = (XMLObjectBuilder<XSString>) builderFactory
+ .getBuilder(XSString.TYPE_NAME);
+ XSString theString = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
+ theString.setValue(value);
+ return theString;
+ }
+
+ // NOTE: making the assumption that OpenSAML parses an untyped attribute value
+ // into XSAny with value in the text content
+ protected XSAny createNewXSAny(String value) {
+ XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
+ @SuppressWarnings("unchecked")
+ XMLObjectBuilder<XSAny> stringBuilder = (XMLObjectBuilder<XSAny>) builderFactory.getBuilder(XSAny.TYPE_NAME);
+ XSAny theAny = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSAny.TYPE_NAME);
+ theAny.setTextContent(value);
+ return theAny;
+ }
+
+ protected Assertion createTestAssertionNoAttributes()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ Assertion testAssertion = createNewSAMLObject(Assertion.class);
+ testAssertion.setVersion(SAMLVersion.VERSION_20);
+ testAssertion.setIssueInstant(new DateTime());
+
+ Subject testSubject = createNewSAMLObject(Subject.class);
+ NameID testNameId = createNewSAMLObject(NameID.class);
+ testNameId.setValue("test subject nameid");
+ testSubject.setNameID(testNameId);
+ testAssertion.setSubject(testSubject);
+
+ return testAssertion;
+ }
+
+ protected Attribute createTestAttribute(boolean hasTypedAttributeValues, String attributeName,
+ String attributeNameFormat)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ Attribute attr = createNewSAMLObject(Attribute.class);
+ attr.setFriendlyName(FRIENDLY_ATTR_NAME);
+ attr.setName(attributeName);
+ attr.setNameFormat(attributeNameFormat);
+ if (hasTypedAttributeValues) {
+ XSString attrValue = createNewXSString(EMAIL_ADDRESS);
+ attr.getAttributeValues().add(attrValue);
+ } else {
+ XSAny attrValue = createNewXSAny(EMAIL_ADDRESS);
+ attr.getAttributeValues().add(attrValue);
+ }
+
+ return attr;
+ }
+
+ protected Attribute createDefaultTestAttribute(boolean hasTypedAttributeValues)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ return createTestAttribute(hasTypedAttributeValues, ATTR_NAME, ATTR_NAME_FORMAT);
+ }
+
+ protected Assertion createTestAssertion(Attribute attribute)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ Assertion testAssertion = createTestAssertionNoAttributes();
+
+ AttributeStatement attrStmt = createNewSAMLObject(AttributeStatement.class);
+ attrStmt.getAttributes().add(attribute);
+ testAssertion.getAttributeStatements().add(attrStmt);
+
+ return testAssertion;
+ }
+
+ protected Assertion createTestAssertionTypedAttributeValues()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ return createTestAssertion(createDefaultTestAttribute(true));
+ }
+
+ protected Assertion createTestAssertionUntypedAttributeValues()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ return createTestAssertion(createDefaultTestAttribute(false));
+ }
+
+ /* test suite setup below */
+ protected Assertion testAssertionTypedAttributeValues = null;
+ protected Assertion testAssertionUntypedAttributeValues = null;
+ protected ServiceConfig serviceConfig = null;
+
+ @BeforeSuite
+ protected void setup() throws InitializationException, NoSuchFieldException, IllegalAccessException, JAXBException {
+ /* try to set up openSAML */
+ XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry();
+ ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
+ try {
+ InitializationService.initialize();
+ } catch (InitializationException e) {
+ logger.error("Could not initialize openSAML: " + e.getLocalizedMessage(), e);
+ throw e;
+ }
+ // try to create a test assertion with typed attribute values; fail the test if
+ // this doesn't work
+ try {
+ testAssertionTypedAttributeValues = createTestAssertionTypedAttributeValues();
+ } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
+ logger.error("Could not create test assertion with typed attribute values: " + e.getLocalizedMessage(), e);
+ throw e;
+ }
+ // try to create a test assertion with untyped attribute values; fail the test
+ // if this doesn't work
+ try {
+ testAssertionUntypedAttributeValues = createTestAssertionUntypedAttributeValues();
+ } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
+ logger.error("Could not create test assertion with untyped attribute values: " + e.getLocalizedMessage(),
+ e);
+ throw e;
+ }
+
+ /* try to set up mock config */
+ serviceConfig = parseServiceConfigString();
+ }
+}
package org.collectionspace.services.common.test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
-import org.testng.annotations.BeforeSuite;
-import org.testng.annotations.Test;
-import org.w3c.dom.Element;
-
-import java.util.ArrayList;
import java.util.Set;
-import javax.xml.namespace.QName;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
+import javax.xml.bind.JAXBException;
import org.collectionspace.services.common.security.SecurityUtils;
-import org.collectionspace.services.config.AssertionAttributeProbeType;
import org.collectionspace.services.config.AssertionProbesType;
-import org.joda.time.DateTime;
-import org.opensaml.core.config.ConfigurationService;
-import org.opensaml.core.config.InitializationException;
-import org.opensaml.core.config.InitializationService;
-import org.opensaml.core.xml.XMLObject;
-import org.opensaml.core.xml.XMLObjectBuilder;
-import org.opensaml.core.xml.XMLObjectBuilderFactory;
-import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
-import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
-import org.opensaml.core.xml.io.Marshaller;
-import org.opensaml.core.xml.io.MarshallingException;
-import org.opensaml.core.xml.schema.XSAny;
-import org.opensaml.core.xml.schema.XSString;
-import org.opensaml.saml.common.SAMLObject;
-import org.opensaml.saml.common.SAMLVersion;
-import org.opensaml.saml.saml2.core.AttributeStatement;
-import org.opensaml.saml.saml2.core.AttributeValue;
-import org.opensaml.saml.saml2.core.NameID;
-import org.opensaml.saml.saml2.core.Subject;
+import org.collectionspace.services.config.SAMLRelyingPartyRegistrationsType;
+import org.collectionspace.services.config.SAMLRelyingPartyType;
+import org.collectionspace.services.config.ServiceConfig;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
-public class SecurityUtilsTest {
+public class SecurityUtilsTest extends AbstractSecurityTestBase {
private static final Logger logger = LoggerFactory.getLogger(SecurityUtilsTest.class);
- private static String BANNER = "-------------------------------------------------------";
- private static String FRIENDLY_ATTR_NAME = "mail";
- private static String ATTR_NAME = "urn:oid:0.9.2342.19200300.100.1.3";
- private static String ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri";
- private static String EMAIL_ADDRESS = "example@example.org";
- private void testBanner(String msg) {
- logger.info("\r" + BANNER + "\r\n" + this.getClass().getName() + "\r\n" + msg + "\r\n" + BANNER);
- }
- /*
- private String xml2String(XMLObject xmlObject) {
- Element element = null;
- String xmlString = "<uninitialized>";
- try {
- Marshaller out = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(xmlObject);
- out.marshall(xmlObject);
- element = xmlObject.getDOM();
-
- } catch (MarshallingException e) {
- logger.error(e.getMessage(), e);
- e.printStackTrace();
- }
-
- try {
- Transformer transformer = TransformerFactory.newInstance().newTransformer();
- transformer.setOutputProperty(OutputKeys.INDENT, "yes");
- StreamResult result = new StreamResult(new java.io.StringWriter());
-
- transformer.transform(new DOMSource(element), result);
- xmlString = result.getWriter().toString();
- } catch (TransformerConfigurationException e) {
- logger.error("Transformer configuration exception: " + e.getLocalizedMessage());
- e.printStackTrace();
- } catch (TransformerException e) {
- logger.error("Exception in transformer: " + e.getLocalizedMessage());
- e.printStackTrace();
- }
-
- return xmlString;
+
+ private void testBanner(String msg) {
+ logger.info("\r" + BANNER + "\r\n" + this.getClass().getName() + "\r\n" + msg + "\r\n" + BANNER);
}
- */
- private <T extends SAMLObject> T createNewSAMLObject(Class<T> clazz) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
- XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
- QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null);
-
- @SuppressWarnings("unchecked") // NOTE: the T extends SAMLObject ought to guarantee this works
- T theObject = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName);
- return theObject;
- }
- private XSString createNewXSString(String value) {
- XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
- @SuppressWarnings("unchecked")
- XMLObjectBuilder<XSString> stringBuilder = (XMLObjectBuilder<XSString>) builderFactory.getBuilder(XSString.TYPE_NAME);
- XSString theString = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
- theString.setValue(value);
- return theString;
- }
- // NOTE: making the assumption that OpenSAML parses an untyped attribute value into XSAny with value in the text content
- private XSAny createNewXSAny(String value) {
- XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
- @SuppressWarnings("unchecked")
- XMLObjectBuilder<XSAny> stringBuilder = (XMLObjectBuilder<XSAny>) builderFactory.getBuilder(XSAny.TYPE_NAME);
- XSAny theAny = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,XSAny.TYPE_NAME);
- theAny.setTextContent(value);
- return theAny;
- }
- private Assertion createTestAssertionNoAttributes() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
- Assertion testAssertion = createNewSAMLObject(Assertion.class);
- testAssertion.setVersion(SAMLVersion.VERSION_20);
- testAssertion.setIssueInstant(new DateTime());
-
- Subject testSubject = createNewSAMLObject(Subject.class);
- NameID testNameId = createNewSAMLObject(NameID.class);
- testNameId.setValue("test subject nameid");
- testSubject.setNameID(testNameId);
- testAssertion.setSubject(testSubject);
-
- return testAssertion;
- }
- private Attribute createAttribute(boolean hasTypedAttributeValues) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
- Attribute attr = createNewSAMLObject(Attribute.class);
- attr.setFriendlyName(FRIENDLY_ATTR_NAME);
- attr.setName(ATTR_NAME);
- attr.setNameFormat(ATTR_NAME_FORMAT);
- if(hasTypedAttributeValues) {
- XSString attrValue = createNewXSString(EMAIL_ADDRESS);
- attr.getAttributeValues().add(attrValue);
- }
- else {
- XSAny attrValue = createNewXSAny(EMAIL_ADDRESS);
- attr.getAttributeValues().add(attrValue);
- }
-
- return attr;
- }
- private Assertion createTestAssertionTypedAttributeValues() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
- Assertion testAssertion = createTestAssertionNoAttributes();
-
- Attribute attr = createAttribute(true);
-
- AttributeStatement attrStmt = createNewSAMLObject(AttributeStatement.class);
- attrStmt.getAttributes().add(attr);
- testAssertion.getAttributeStatements().add(attrStmt);
-
- return testAssertion;
- }
- private Assertion createTestAssertionUntypedAttributeValues() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
- Assertion testAssertion = createTestAssertionNoAttributes();
-
- Attribute attr = createAttribute(false);
-
- AttributeStatement attrStmt = createNewSAMLObject(AttributeStatement.class);
- attrStmt.getAttributes().add(attr);
- testAssertion.getAttributeStatements().add(attrStmt);
-
- return testAssertion;
- }
-
- // the tests are below
- private Assertion testAssertionTypedAttributeValues = null;
- private Assertion testAssertionUntypedAttributeValues = null;
- @BeforeSuite
- private void setup() throws InitializationException,NoSuchFieldException,IllegalAccessException {
- // try to set up openSAML
- XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry();
- ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
+
+ @Test
+ public void assertionWithTypedAttributeValuesIsNotNull() {
+ testBanner("the mock assertion with typed attribute values is not null");
+ Assert.assertNotNull(testAssertionTypedAttributeValues);
+ }
+
+ @Test
+ public void assertionWithUntypedAttributeValuesIsNotNull() {
+ testBanner("the mock assertion with untyped attribute values is not null");
+ Assert.assertNotNull(testAssertionUntypedAttributeValues);
+ }
+
+ @Test(dependsOnMethods = { "assertionWithTypedAttributeValuesIsNotNull" })
+ public void candidateUsernamesTypedNotNullOrEmpty() {
+ testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are typed as string");
+ Set<String> candidateUsernames = SecurityUtils
+ .findSamlAssertionCandidateUsernames(testAssertionTypedAttributeValues, null);
+ Assert.assertNotNull(candidateUsernames);
+ if (null != candidateUsernames)
+ Assert.assertFalse(candidateUsernames.isEmpty());
+ }
+
+ @Test(dependsOnMethods = { "assertionWithUntypedAttributeValuesIsNotNull" })
+ public void candidateUsernamesUntypedNotNullOrEmpty() {
+ testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are not typed");
+ Set<String> candidateUsernames = SecurityUtils
+ .findSamlAssertionCandidateUsernames(testAssertionUntypedAttributeValues, null);
+ Assert.assertNotNull(candidateUsernames);
+ if (null != candidateUsernames)
+ Assert.assertFalse(candidateUsernames.isEmpty());
+ }
+
+ @Test(dependsOnMethods = { "assertionWithUntypedAttributeValuesIsNotNull" })
+ public void candidateUsernamesUntypedIsCorrect() {
+ testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are not typed");
+ Set<String> candidateUsernames = SecurityUtils
+ .findSamlAssertionCandidateUsernames(testAssertionUntypedAttributeValues, null);
+ Assert.assertNotNull(candidateUsernames);
+ if (null != candidateUsernames)
+ Assert.assertEquals(candidateUsernames.iterator().next(), EMAIL_ADDRESS);
+ }
+
+ @Test(dependsOnMethods = { "assertionWithTypedAttributeValuesIsNotNull" })
+ public void candidateUsernamesTypedIsCorrect() {
+ testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are typed as string");
+ Set<String> candidateUsernames = SecurityUtils
+ .findSamlAssertionCandidateUsernames(testAssertionTypedAttributeValues, null);
+ Assert.assertNotNull(candidateUsernames);
+ if (null != candidateUsernames)
+ Assert.assertEquals(candidateUsernames.iterator().next(), EMAIL_ADDRESS);
+ }
+
+ @Test
+ public void idenfitiferProbeFindsSsoId() throws JAXBException, IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, SecurityException {
+ testBanner("identifier probe finds sso id");
+
+ String nameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri";
+ String identifierName = "http://schemas.auth0.com/identifier";
+
+ // set up a minimal mock configuration string with the SSO ID probe we wish to
+ // test
+ String theConfigString = createTestConfig(USERNAME_ATTRIBUTE, identifierName);
+ ServiceConfig theServiceConfig = null;
try {
- InitializationService.initialize();
- } catch (InitializationException e) {
- logger.error("Could not initialize openSAML: " + e.getLocalizedMessage(), e);
+ theServiceConfig = parseServiceConfigString(MOCK_ROOT_DIR, theConfigString);
+ } catch (JAXBException e) {
+ logger.warn("Could not create mock service config: " + e.getLocalizedMessage());
throw e;
- }
- // try to create a test assertion with typed attribute values; fail the test if this doesn't work
+ }
+ SAMLRelyingPartyRegistrationsType relyingPartyRegistrations = theServiceConfig.getSecurity().getSso().getSaml()
+ .getRelyingPartyRegistrations();
+ SAMLRelyingPartyType relyingPartyRegistration = relyingPartyRegistrations.getRelyingParty().get(0);
+ AssertionProbesType assertionSsoIdProbes = (relyingPartyRegistration != null
+ ? relyingPartyRegistration.getAssertionSsoIdProbes()
+ : null);
+
+ // create an attribute with the same name identifier as the test probe
+ Attribute attribute = null;
try {
- testAssertionTypedAttributeValues = createTestAssertionTypedAttributeValues();
+ attribute = createTestAttribute(true, identifierName, nameFormat);
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
- logger.error("Could not create test assertion with typed attribute values: " + e.getLocalizedMessage(), e);
+ logger.warn("Could not create mock attribute: " + e.getLocalizedMessage());
throw e;
}
- // try to create a test assertion with untyped attribute values; fail the test if this doesn't work
+ // create a SAML assertion with the attribute
+ Assertion assertion = null;
try {
- testAssertionUntypedAttributeValues = createTestAssertionUntypedAttributeValues();
+ assertion = createTestAssertion(attribute);
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
- logger.error("Could not create test assertion with untyped attribute values: " + e.getLocalizedMessage(), e);
+ logger.warn("Could not create SAML assertion" + e.getLocalizedMessage());
throw e;
}
- }
-
- @Test
- public void assertionWithTypedAttributeValuesIsNotNull() {
- testBanner("the mock assertion with typed attribute values is not null");
- Assert.assertNotNull(testAssertionTypedAttributeValues);
- }
- @Test
- public void assertionWithUntypedAttributeValuesIsNotNull() {
- testBanner("the mock assertion with untyped attribute values is not null");
- Assert.assertNotNull(testAssertionUntypedAttributeValues);
- }
- @Test(dependsOnMethods = {"assertionWithTypedAttributeValuesIsNotNull"})
- public void candidateUsernamesTypedNotNullOrEmpty() {
- testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are typed as string");
- Set<String> candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(testAssertionTypedAttributeValues, null);
- Assert.assertNotNull(candidateUsernames);
- if(null != candidateUsernames)
- Assert.assertFalse(candidateUsernames.isEmpty());
- }
- @Test(dependsOnMethods = {"assertionWithUntypedAttributeValuesIsNotNull"})
- public void candidateUsernamesUntypedNotNullOrEmpty() {
- testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are not typed");
- Set<String> candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(testAssertionUntypedAttributeValues, null);
- Assert.assertNotNull(candidateUsernames);
- if(null != candidateUsernames)
- Assert.assertFalse(candidateUsernames.isEmpty());
- }
+
+ // check whether getSamlAssertionSsoId finds the SSO ID we put in the assertion
+ // using the test probe
+ String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes);
+ Assert.assertNotNull(ssoId);
+ Assert.assertFalse(ssoId.isEmpty());
+ Assert.assertEquals(ssoId, EMAIL_ADDRESS);
+ }
}
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.sun.xml.bind</groupId>
+ <artifactId>jaxb-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>xerces</groupId>
+ <artifactId>xercesImpl</artifactId>
+ <version>2.12.2</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.jvnet.jaxb2_commons</groupId>
<artifactId>jaxb2-basics</artifactId>
return getFileChildren(rootDir, true);
}
- protected Object parse(File configFile, Class<?> clazz)
+ public Object parse(File configFile, Class<?> clazz)
throws FileNotFoundException, JAXBException {
Object result = null;
* @throws JAXBException
* @throws Exception
*/
- protected Object parse(InputStream configFileStream, Class<?> clazz)
+ public Object parse(InputStream configFileStream, Class<?> clazz)
throws JAXBException {
Object result = null;
--- /dev/null
+package org.collectionspace.services.common.config;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.JAXBException;
+
+import org.collectionspace.services.config.AssertionAttributeProbeType;
+import org.collectionspace.services.config.SAMLRelyingPartyType;
+import org.collectionspace.services.config.SAMLType;
+import org.collectionspace.services.config.ServiceConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+public class ServicesConfigReaderImplTest {
+ private static final Logger logger = LoggerFactory.getLogger(ServicesConfigReaderImplTest.class);
+ private static String BANNER = "-------------------------------------------------------";
+ // NOTE: adapted from https://collectionspace.atlassian.net/browse/DRYD-1702?focusedCommentId=60649
+ private static final String USERNAME_ATTRIBUTE = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
+ private static final String SSOID_ATTRIBUTE = "http://schemas.auth0.com/identifier";
+ private static final String SSO_CONFIG_STRING = new StringBuilder()
+ .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<svc:service-config xmlns:svc='http://collectionspace.org/services/config'>")
+ .append("<security>")
+ .append("<sso>")
+ .append("<saml>")
+ .append("<single-logout />")
+ .append("<relying-party-registrations>")
+ .append("<relying-party id=\"auth0\">")
+ .append("<name>Auth0 - Scenario 11</name>")
+ .append("<icon location=\"https://cdn.auth0.com/manhattan/versions/1.4478.0/assets/badge.png\" />")
+ .append("<metadata location=\"https://dev-cf0ltyyfory6gtqm.us.auth0.com/samlp/metadata/ZXtZfEN0mj96GP8LCmEUWcpuDO0OtqKY\" />")
+ .append("<assertion-username-probes>")
+ .append("<attribute name=\"" + USERNAME_ATTRIBUTE + "\" />")
+ .append("</assertion-username-probes>")
+ .append("<assertion-sso-id-probes>")
+ .append("<attribute name=\"" + SSOID_ATTRIBUTE + "\" />")
+ .append("</assertion-sso-id-probes>")
+ .append("</relying-party>")
+ .append("</relying-party-registrations>")
+ .append("</saml>")
+ .append("</sso>\n")
+ .append("</security>")
+ .append("</svc:service-config>")
+ .toString();
+ private static final String MOCK_ROOT_DIR = "./";
+ private void testBanner(String msg) {
+ logger.info("\r" + BANNER + "\r\n" + this.getClass().getName() + "\r\n" + msg + "\r\n" + BANNER);
+ }
+ private List<AssertionAttributeProbeType> getUserNameProbesFromConfig() {
+ return getUserNameProbesFromConfig(serviceConfig);
+ }
+ private List<AssertionAttributeProbeType> getUserNameProbesFromConfig(ServiceConfig serviceConfig) {
+ SAMLType samlConfig = serviceConfig.getSecurity().getSso().getSaml();
+ List<SAMLRelyingPartyType> relyingParties = samlConfig.getRelyingPartyRegistrations().getRelyingParty();
+ SAMLRelyingPartyType relyingParty = relyingParties.get(0);
+
+ List<Object> usernameProbes = relyingParty.getAssertionUsernameProbes().getNameIdOrAttribute();
+ ArrayList<AssertionAttributeProbeType> up = new ArrayList<AssertionAttributeProbeType>();
+ for (Object obj : usernameProbes) {
+ AssertionAttributeProbeType a = (AssertionAttributeProbeType) obj;
+ up.add(a);
+ }
+
+ return up;
+ }
+ private List<AssertionAttributeProbeType> getSsoIdProbesFromConfig() {
+ return getSsoIdProbesFromConfig(serviceConfig);
+ }
+ private List<AssertionAttributeProbeType> getSsoIdProbesFromConfig(ServiceConfig serviceConfig) {
+ SAMLType samlConfig = serviceConfig.getSecurity().getSso().getSaml();
+ List<SAMLRelyingPartyType> relyingParties = samlConfig.getRelyingPartyRegistrations().getRelyingParty();
+ SAMLRelyingPartyType relyingParty = relyingParties.get(0);
+
+ List<Object> ssoIdProbes = relyingParty.getAssertionSsoIdProbes().getNameIdOrAttribute();
+ ArrayList<AssertionAttributeProbeType> up = new ArrayList<AssertionAttributeProbeType>();
+ for (Object obj : ssoIdProbes) {
+ AssertionAttributeProbeType a = (AssertionAttributeProbeType) obj;
+ up.add(a);
+ }
+
+ return up;
+ }
+ // the tests are below
+ private ServiceConfig serviceConfig = null;
+ @BeforeSuite
+ public void setup() throws JAXBException {
+ ServicesConfigReaderImpl rdr = new ServicesConfigReaderImpl(MOCK_ROOT_DIR);
+ ByteArrayInputStream in = new ByteArrayInputStream(SSO_CONFIG_STRING.getBytes());
+ try {
+ serviceConfig = (ServiceConfig) rdr.parse(in, ServiceConfig.class);
+ } catch (JAXBException e) {
+ logger.warn("Could not create test service config: " + e.getLocalizedMessage());
+ throw e;
+ }
+ }
+ @Test
+ public void usernameProbesNotNullOrEmpty() {
+ testBanner("the username probes list is not null or empty");
+
+ List<AssertionAttributeProbeType> usernameProbes = getUserNameProbesFromConfig();
+ Assert.assertNotNull(usernameProbes);
+ if(null != usernameProbes) {
+ Assert.assertFalse(usernameProbes.isEmpty());
+ }
+ }
+ @Test(dependsOnMethods = {"usernameProbesNotNullOrEmpty"})
+ public void usernameProbesCorrectlyParsedFromConfig() {
+ testBanner("the username probes list has expected contents");
+
+ List<AssertionAttributeProbeType> usernameProbes = getUserNameProbesFromConfig();
+ Assert.assertEquals(usernameProbes.size(), 1);
+ AssertionAttributeProbeType probe = usernameProbes.get(0);
+ Assert.assertEquals(probe.getName(), USERNAME_ATTRIBUTE);
+ }
+ @Test
+ public void ssoIdProbesNotNullOrEmpty() {
+ testBanner("the SSO ID probes list is not null or empty");
+
+ List<AssertionAttributeProbeType> ssoIdProbes = getSsoIdProbesFromConfig();
+ Assert.assertNotNull(ssoIdProbes);
+ if(null != ssoIdProbes) {
+ Assert.assertFalse(ssoIdProbes.isEmpty());
+ }
+ }
+ @Test(dependsOnMethods = {"ssoIdProbesNotNullOrEmpty"})
+ public void ssoIdProbesCorrectlyParsedFromConfig() {
+ testBanner("the SSO ID probes list has expected contents");
+
+ List<AssertionAttributeProbeType> ssoIdProbes = getSsoIdProbesFromConfig();
+ Assert.assertEquals(ssoIdProbes.size(), 1);
+ AssertionAttributeProbeType probe = ssoIdProbes.get(0);
+ Assert.assertEquals(probe.getName(), SSOID_ATTRIBUTE);
+ }
+}