Cross-origin AJAX requests for Shib-protected resources

Multiple people have tried unsuccessfully to use this example and have noted it in JIRA or on-list, so just be aware it's probably off in some way.

Use case: if you want to enable AJAX (XmlHttpRequest) queries to a Shibboleth-protected resource on a different server.

Example:

Say there exists a web application running on webapp.example.edu, containing JavaScript code that makes XmlHttpRequest calls to another server, data.example.edu, which is protected by Shibboleth, using and identity provider on idp.example.edu.

Step-by-step guide

  1. Configure the IdP to add the required CORS headers to allow cross-origin requests.  In this case, only cross-origin requests within example.edu are allowed.  This shows a filter from the Jetty container added to the Shibboleth IdP web.xml file.  Ensure the implementation you use sets the Access-Control-Allow-Credentials header to "true" so that the IdP and SP can set and read their session cookies.

        <filter>
            <filter-name>cross-origin</filter-name>
            <filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
            <init-param>
             <param-name>allowedOrigins</param-name>
             <param-value>http(s)?://*.example.edu(:\d+)?</param-value>
           </init-param>
           <init-param>
             <param-name>allowedHeaders</param-name>
             <param-value>X-Requested-With,Content-Type,Accept,Origin</param-value>
           </init-param>
        </filter>
        <filter-mapping>
            <filter-name>cross-origin</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
  2. Configure data.example.edu to add required CORS headers.  Example for Apache httpd.conf:

    # enable CORS headers for example.edu sites only
    <IfModule mod_headers.c>
      <Location /webservice>
        SetEnvIf Origin "^http(s)?://.*\.example\.edu(:\d+)?$" AccessControlAllowOrigin=$0
        Header always set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
        Header always set Access-Control-Allow-Credentials true env=AccessControlAllowOrigin
      </Location>
    </IfModule>
  3. Configure data.example.edu to use the HTTP POST SAML binding for outbound SSO requests.  This is necessary because XmlHttpRequest will follow redirects (as used by the default HTTP Redirect binding), which will cause the Origin header to be removed on the way to the IdP and the request to fail.  Example for Shibboleth SP shibboleth2.xml:

    <SSO entityID="https://idp.example.edu/idp/shibboleth" outgoingBindings="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
      SAML2
    </SSO>
    
    
  4. Write JavaScript to handle the SAML authentication flow in the webapp on webapp.example.edu:
    1. Make the initial XmlHttpRequest to the web service on data.example.org.  Be sure to set the CORS headers to reflect the proper Origin and With-Credentials settings.  The response should either be the expected web service output (if there was already an SP session established on data.example.org, in which case you can stop), or an HTML form with a SAMLRequest parameter ready to be submitted to the IdP.
    2. Make another XmlHttpRequest to the IdP's SSO endpoint (which should be the HTML FORM action from the first step), passing the SAMLRequest parameter.  Again, be sure to set the Origin and With-Credentials settings correctly.  Assuming the user already has a session at the IdP, the response should be another HTML form with a SAMLResponse parameter.
    3. Make one more XmlHttpRequest to the AssertionConsumerService endpoint on data.example.org (which again should be the HTML form action from the previous step).  Set the Origin and With-Credentials settings as usual.  The XHR should follow the redirect returned by the ACS, which should go back to the original web service URL from the initial request, and return the expected data from the web service.
    4. Subsequent calls to the web service should get the expected data back immediately, provided the Shibboleth session is still valid, without needing round-trips to the IdP.


Some assumptions/tips:

  • This works best if webapp.example.edu requires Shib authentication as well.  This allows the user to get "logged in" the usual way, within their normal browser environment, to get an IdP session established.  Then you don't need to worry about the IdP returning login forms or the like to your XmlHttpRequest.
  • The IdP and SP must support the HTTP POST SAML binding for both requests and responses.  XmlHttpRequest will follow redirects, but doesn't propagate the CORS headers along the way, which results in failures.
  • The data.example.edu SP is assumed to be configured to use the same IdP as webapp.example.edu.  In theory, it is possible to use this in a federated context, but requires being able to request a particular IdP at the data.example.edu SP (for example, by having a discovery service that can be fed the entityID from the Shib-Identity-Provider variable/header from webapp.example.edu, or a parameter on the web service that can be dropped into a SessionInitiator URL).
  • The client JavaScript from webapp.example.edu needs to be aware of the SAML transaction, since it needs to perform the FORM POSTs.
  • You might want to think about how to handle a situation where there is no valid IdP session (perhaps it expired). One possible option is to have the data.example.edu SP set the isPassive flag on the SAML authentication request, so you get a SAML error instead of a login form as a response.  It's probably a Bad Idea to display the login form to the user and mediate a real login, as it makes the user's IdP look like it's on webapp.example.edu.