My question is what is the best option/best practice to support such
scenario and to be able to extend my SP to support more SAML flows
with differences in the Assertion attributes and more configurations?
To branch certain configurations such as the Assertion attributes you will need to create separate Service Providers. Other configurations and services can be shared. Other configurations should be shared. For instance, I use single custom SAMLUserDetailsService implementation that pulls the unique EntityID out of the credential and uses that to map the SAML attributes differently for each IDP.
When we use Spring SAML should we use different Spring Security
context files for each of the SAML flavors?
Are there issues with thread safety when using multiple contexts in
parallel?
I do not recommend running multiple security contexts separately. In my experience, there is a lot of configuration involved in Spring SAML and chances are, you will have to duplicate a ton of code needlessly by doing it this way.
In Spring SAML, there is the concept of using different aliases for different Service Providers. I have setup a many Service Providers to many IDPs and been able to use one Spring Security context and implement custom services where I need to to handle the differences. I do not have a full list of your requirements and there may be some that simply cannot be done within a single spring security context, but I'd wait to make sure that's the case before taking that route.
What specifically are will need to be different between each IDP?
I am limited in what code I am allowed to post, but I've included what I can.
Entry Point URL - If you have multiple IDPs with an alias set in your configuration, the entry point url by default will be
"/saml/login/alias/" +productAlias+ "?idp=" + entityId;
If you are behind a load balancer, you can configure it to rewrite any URL you want to the URL for the customer.
Bindings and Assertions - These are configured in each of the Service Providers metadata.xml file and can be different for each customer. The real challenge is how to pull the attributes out of the authenticated SAML request and get it in a usable form.
I don't know if there is a better way to do this, but my requirement was to have any bindings mappable and configurable for any IDP I have configured. To accomplish this, I implemented a custom SAMLUserDetailsService
. From the SAMLCredential
passed in to the service, you can get use credential.getRemoteEntityID()
to pull up the mapping for the customer. From there you'll need to parse out the attributes from the credential.
Example of parsing the SAML attributes for Microsoft and other IDPs
public class AttributeMapperImpl implements AttributeMapper {
@Override
public Map<String, List<String>> parseSamlStatements(List<AttributeStatement> attributeList) {
Map<String, List<String>> map = new HashMap<>();
attributeList.stream().map((statement) -> parseSamlAttributes(statement.getAttributes())).forEach((list) -> {
map.putAll(list);
});
return map;
}
@Override
public Map<String, List<String>> parseSamlAttributes(List<Attribute> attributes) {
Map<String, List<String>> map = new HashMap<>();
attributes.stream().forEach((attribute) -> {
List<String> sList = parseXMLObject(attribute.getAttributeValues());
map.put(attribute.getName(), sList);
});
return map;
}
@Override
public List<String> parseXMLObject(List<XMLObject> objs) {
List<String> list = new ArrayList<>();
objs.stream().forEach((obj) -> {
if(obj instanceof org.opensaml.xml.schema.impl.XSStringImpl){
XSStringImpl xs = (XSStringImpl) obj;
list.add(xs.getValue());
}else if(obj instanceof org.opensaml.xml.schema.impl.XSAnyImpl){
XSAnyImpl xs = (XSAnyImpl) obj;
list.add(xs.getTextContent());
}
});
return list;
}
@Override
public String parseSamlStatementsToString(Map<String, List<String>> map) {
String values = "";
Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry pair = (Map.Entry) it.next();
values += pair.getKey() + "=" + pair.getValue() + " ";
it.remove(); // avoids a ConcurrentModificationException
}
return values;
}
}
- Action on Success/Failure - There are many possible ways of doing this. I chose to use a single endpoint in a controller that has access to the session that all requests go to on success. After successful authentication, I can pull out of the session what IDP the user is from and redirect them accordingly. Failure is a bit more difficult because it's entirely possible and likely that some failures will be so severe that you won't know which IDP the request came from (i.e. if the saml message is signed with the wrong cert).