Cross-Domain Single Sign-On

A couple of blog posts ago I’ve been detailing how regular cookie based Single Sign-On (SSO) works. I believe now it is time to have a look at this again, but make it a bit more difficult: let’s see what happens if you want to achieve SSO across different domains.

So let’s say that you have an OpenAM instance on the login.example.com domain and the goal is to achieve SSO with a site on the cool.application.com domain. As you can see right away, the domains are different, which means that regular SSO won’t be enough here (if you don’t see why, have a read of my SSO post).

Cross-Domain Single Sign-On

We already know now that in the regular SSO flow the point is that the application is in the same domain as OpenAM, hence it has access to the session cookie, which in turn allows the application to identify the user. Since in the cross-domain scenario the session cookie isn’t accessible, our goal is going to be to somehow get the session cookie from the OpenAM domain to the application’s domain, more precisely:
We need to have a mechanism that is implemented by both OpenAM and the application, which allows us to transfer the session cookie from one place to the other.
Whilst this sounds great, it would be quite cumbersome for all the different applications to implement this cookie sharing mechanism, so what else can we do?

Policy Agents to the rescue

For CDSSO OpenAM has its own proprietary implementation to transfer the session ID across domains. It’s proprietary, as it does not follow any SSO standard (for example SAML), but it does attempt to mimic them to some degree.
OpenAM has the concept of Policy Agents (PA) which are (relatively) easily installable modules for web servers (or Java EE containers) that can add extra security to your applications. It does so by ensuring that the end-users are always properly authenticated and authorized to view your protected resources.
As the Policy Agents are OpenAM components, they implement this proprietary CDSSO mechanism in order to simplify SSO integrations.

Without further ado, let’s have a look at the CDSSO sequence diagram:
CDSSO sequence

A bit more detail about each step:

  1. The user attempts to access a resource that is protected by the Policy Agent.
  2. The PA is unable to find the user’s session cookie, so it redirects the user to…
  3. …the cdcservlet. (The Cross-Domain Controller Servlet is the component that will eventually share the session ID value with the PA.)
  4. When the user accesses the cdcservlet, OpenAM is able to detect whether the user has an active session (the cdcservlet is on OpenAM’s domain, hence a previously created session cookie should be visible there), and…
  5. when the token is invalid…
  6. we redirect to…
  7. …the sign in page,
  8. which will be displayed to the user…
  9. …and then the user submits her credentials.
  10. If the credentials were correct, we redirect the user back to…
  11. …the cdcservlet, which will…
  12. …ensure…
  13. …that the user’s session ID is actually valid, and then…
  14. …displays a self-submitting HTML form to the user, which contains a huge Base64 encoded XML that holds the user’s session ID.
  15. The user then auto-submits the form to the PA, where…
  16. …the PA checks the validity…
  17. …of the session ID extracted from the POST payload…
  18. …and then performs the necessary authorization checks…
  19. …to ensure that the user is allowed to access the protected resource…
  20. …and then the PA creates the session cookie for the application’s domain, and the user either sees the requested content or an HTTP 403 Forbidden page. For subsequent requests the PA will see the session cookie on the application domain, hence this whole authentication / authorization process will become much simpler.

An example CDSSO LARES (Liberty Alliance RESponse) response that gets submitted to PA (like in step 15 above) looks just like this:

<lib:AuthnResponse xmlns:lib="http://projectliberty.org/schemas/core/2002/12" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ResponseID="sb976ed48177fd6c052e2241229cca5dee6b62617"  InResponseTo="s498ed3a335122c67461c145b2349b68e5e08075d" MajorVersion="1" MinorVersion="0" IssueInstant="2014-05-22T20:29:46Z"><samlp:Status>
<samlp:StatusCode Value="samlp:Success">
</samlp:StatusCode>
</samlp:Status>
<saml:Assertion  xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:lib="http://projectliberty.org/schemas/core/2002/12"  id="s7e0dad257500d7b92aca165f258a88caadcc3e9801" MajorVersion="1" MinorVersion="0" AssertionID="s7e0dad257500d7b92aca165f258a88caadcc3e9801" Issuer="https://login.example.com:443/openam/cdcservlet" IssueInstant="2014-05-22T20:29:46Z" InResponseTo="s498ed3a335122c67461c145b2349b68e5e08075d" xsi:type="lib:AssertionType">
<saml:Conditions  NotBefore="2014-05-22T20:29:46Z" NotOnOrAfter="2014-05-22T20:30:46Z" >
<saml:AudienceRestrictionCondition>
<saml:Audience>https://cool.application.com:443/?Realm=%2F</saml:Audience>
</saml:AudienceRestrictionCondition>
</saml:Conditions>
<saml:AuthenticationStatement  AuthenticationMethod="vir" AuthenticationInstant="2014-05-22T20:29:46Z" ReauthenticateOnOrAfter="2014-05-22T20:30:46Z" xsi:type="lib:AuthenticationStatementType"><saml:Subject   xsi:type="lib:SubjectType"><saml:NameIdentifier NameQualifier="https://login.example.com:443/openam/cdcservlet">AQIC5wM2LY4SfcxADqjyMPRB8ohce%2B6kH4VGD408SnVyfUI%3D%40AAJTSQACMDEAAlNLABQtMzk5MDEwMTM3MzUzOTY5MTcyOQ%3D%3D%23</saml:NameIdentifier>
<saml:SubjectConfirmation>
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
</saml:SubjectConfirmation>
<lib:IDPProvidedNameIdentifier  NameQualifier="https://login.example.com:443/openam/cdcservlet" >AQIC5wM2LY4SfcxADqjyMPRB8ohce%2B6kH4VGD408SnVyfUI%3D%40AAJTSQACMDEAAlNLABQtMzk5MDEwMTM3MzUzOTY5MTcyOQ%3D%3D%23</lib:IDPProvidedNameIdentifier>
</saml:Subject><saml:SubjectLocality  IPAddress="127.0.0.1" DNSAddress="localhost" /><lib:AuthnContext><lib:AuthnContextClassRef>http://www.projectliberty.org/schemas/authctx/classes/Password</lib:AuthnContextClassRef><lib:AuthnContextStatementRef>http://www.projectliberty.org/schemas/authctx/classes/Password</lib:AuthnContextStatementRef></lib:AuthnContext></saml:AuthenticationStatement></saml:Assertion>
<lib:ProviderID>https://login.example.com:443/openam/cdcservlet</lib:ProviderID></lib:AuthnResponse>

If you watch carefully you can see the important bit right in the middle:

<saml:NameIdentifier NameQualifier="https://login.example.com:443/openam/cdcservlet">AQIC5wM2LY4SfcxADqjyMPRB8ohce%2B6kH4VGD408SnVyfUI%3D%40AAJTSQACMDEAAlNLABQtMzk5MDEwMTM3MzUzOTY5MTcyOQ%3D%3D%23</saml:NameIdentifier>

As you can see the value of the NameIdentifier element is the user’s session ID. Once the PA creates the session cookie on the application’s domain you’ve pretty much achieved single sign-on across domains, well done! ;)

P.S.: If you are looking for a guide on how to set up Policy Agents for CDSSO, check out the documentation.