Monday, March 25, 2013

Spring SAML

At first blush, this stuff is ugly. Like kicked in the face by a billy goat when you were three years old ugly. I don't really understand any of it. But work with it I shall.

First off, get the logging working.

log4j.logger.org.springframework.security=DEBUG
log4j.logger.org.opensaml=DEBUG

If that doesn't produce many logs and/or you have the following error in your catalina.out then fetch slf4j-log4j12-1.6.2.jar from slf4j and put in in your /lib/ alongside the existing slf4j-api-1.6.2.jar file.

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Now you should have a tonn of logs.

In your tomcat application's securityContext.xml you should have something like the following which indicates that yoursite.com/saml/start should execute the doFilter method of your SamlDiscovery class.

<bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
 <security:filter-chain-map path-type="ant">
  <security:filter-chain pattern="/saml/start/**" filters="samlIDPDiscovery"/>
<bean id="samlIDPDiscovery" class="yourStuff.SamlDiscovery"/>
public class SamlDiscovery extends GenericFilterBean {
 doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
  ... response.sendRedirect(responseURL);
  // 302 -> yoursite.com/saml/login/alias/defaultAlias?disco=true&entityID=whateverYouLookedUp

From there you might look at the incoming request and decide based on a properties file or whatever the metadata file and entityId you want to use for this request. The problem is that spring/saml will totally ignore that entityID.

In your tomcat application's securityContext.xml you should have something like the following which indicates that yoursite.com/saml/login/alias/defaultAlias should execute the doFilter method of your SamlEntryPoint class.

<bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
 <security:filter-chain-map path-type="ant">
  <security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
<bean id="samlEntryPoint" class="yourStuff.SamlEntryPoint">
import org.springframework.security.saml.SAMLEntryPoint;
public class SamlEntryPoint extends SAMLEntryPoint
 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  // your chance for mods to the request is now
  super.doFilter(httpRequest, response, chain);

So you have no visibility into the behaviour of /saml/login/alias/defaultAlias except for the DEBUG logs which may show something like the following.

Checking child metadata provider for entity descriptor with entity ID: https://yourFrontEndApache.com:443/saml/metadata/alias/defaultAlias
Searching for entity descriptor with an entity ID of https://yourFrontEndApache:443/saml/metadata/alias/defaultAlias
Metadata document does not contain an EntityDescriptor with the ID https://yourFrontEndApache:443/saml/metadata/alias/defaultAlias

Checking child metadata provider for entity descriptor with entity ID: https://yourFrontEndApache.com:443/saml/metadata/alias/defaultAlias
Searching for entity descriptor with an entity ID of https://yourFrontEndApache:443/saml/metadata/alias/defaultAlias
Metadata document does not contain an EntityDescriptor with the ID https://yourFrontEndApache:443/saml/metadata/alias/defaultAlias

No IDP specified, using default https://yourInternalServer1:8455/samlStuff

In my case, I've got a tomcat app with two metadata files. Each file contains a single entityId which points to a different single-sign-on provider. That means that depending on the front-end domain by which you access my back-end application, I want to hand you over to single-sign-on provider #1 or #2. So I want to specify to /saml/login/alias/defaultAlias the entityId (and thus metadata and sso provider). But it's actual behaviour is to ignore my querystring argument and look for an entityId whose name matches the url from which it was referenced.

The logs above look to me like it checks metadata1 for an entityId name matching the access url, then checks metadata2 for the same then gives up and uses the first entityId from the first metadata.

Why not just change the entityId in the metadata file to match the source url? Well, that's no simple matter. You can't just hand edit that file. The result is an entityID related exception during /saml/start/

Is the "entityID" query string parameter possibly case sensitive? Switching it to "entityId" doesn't help.

How's It Supposed To Work?

4.7 Single sign-on process

SP initialized SSO process can be started in two ways:
- User accesses a resource protected by Spring Security which initializes SAMLEntryPoint
- User is redirected to the SSO page at e.g. https://www.server.com/context/saml/login/alias/defaultAlias

After identification of IDP to use SAML Extension creates an AuthnRequest SAML message and sends it to the selected IDP.

Unfortuantely "Section 4.8, IDP selection" contains only the text "TODO". So we must inspect the source.

public class SAMLEntryPoint extends GenericFilterBean implements AuthenticationEntryPoint {
 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  commence(fi.getRequest(), fi.getResponse(), null);
 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
  } else {
   initializeSSO(context, e);

This part of the process doesn't seem to care about query string arguments "disco" and "entityID". Those are part of a discovery protocol.

public class SAMLEntryPoint extends GenericFilterBean implements AuthenticationEntryPoint {
 public static final String DISCOVERY_RESPONSE_PARAMETER = "disco";
 protected void initializeDiscovery(SAMLMessageContext context) throws ServletException, IOException, MetadataProviderException {
  discoveryURL = getServletContext().getContextPath() + SAMLDiscovery.FILTER_URL + "?" + SAMLDiscovery.RETURN_ID_PARAM + "=" + IDP_PARAMETER + "&" + SAMLDiscovery.ENTITY_ID_PARAM + "=" + context.getLocalEntityId();
  response.sendRedirect(discoveryURL);

public class SAMLDiscovery extends GenericFilterBean {
 public static final String FILTER_URL = "/saml/discovery";
 public static final String ENTITY_ID_PARAM = "entityID";

After much agony, we discovered that the parameter "entityID" is not a valid input to our use of SAMLEntryPoint. Instead using "idp" gives desired results. This was discovered from inpsection of SAMLContextProviderImpl.populatePeerEntityId()

{ "loggedin": false, "owner": false, "avatar": "", "render": "nothing", "trackingID": "UA-36983794-1", "description": "", "page": { "blogIds": [ 417 ] }, "domain": "holtstrom.com", "base": "\/michael", "url": "https:\/\/holtstrom.com\/michael\/", "frameworkFiles": "https:\/\/holtstrom.com\/michael\/_framework\/_files.4\/", "commonFiles": "https:\/\/holtstrom.com\/michael\/_common\/_files.3\/", "mediaFiles": "https:\/\/holtstrom.com\/michael\/media\/_files.3\/", "tmdbUrl": "http:\/\/www.themoviedb.org\/", "tmdbPoster": "http:\/\/image.tmdb.org\/t\/p\/w342" }