Monday, August 14, 2006

SAML Relying Party Implementation

How to implement SAML based SSO Relying Party.

Glossary. SAML - Security Assertion Markup Language is an XML standard for exchanging authentication and authorization data between security domains, that is, between an identity provider and a service provider. SSO - Single sign-on (SSO) is a specialized form of software authentication that enables a user to authenticate once and gain access to the resources of multiple software systems.

Introduction. Recently I have had to implement one part of SSO. My application as a provider of the services acts like a Relying Party in the SAML terms. Relying party is the system, or administrative domain, that relies on information supplied to it by the asserting party. While Asserting party is the system, or administrative domain, that asserts information about a subject. For instance, the asserting party asserts that this user has been authenticated and has given associated attributes. In other words I am in B2B relation with my partners. We want to use SSO all across our services. And of course we want to use modern SAML as assertion contract language.

Finding solution. In order to implement SSO with SAML you have to choose specification to which you conform. There are two of them 1.1 and 2.0. While 2.0 is more feature rich and restrictive, old 1.1 is more wide spread and accepted across business. Specification defines not only the SAML language itself, but also the way it can be used during assertions interchange. There are two possible ways of assertion flow for SSO purpose in 1.1 spec:
  • Browser/Artifact Profile - this stand for the case when target service (the service end-user wants to access) asks another party to confirm end-user credentials. Actual assertion flow is more complicated (You can reference to actual specification part 4.1.1 for more details).
  • Browser/POST Profile - in this case end-user submits his passport along with the reference to the target service he wants to access.
First case with artifact interchange is not suite for us. I have to initiate outgoing connection to trusted business partner site in order to obtain actual SAML assertion. I have to create request based on artifact supplied me earlier. To involve this kind of dependency is not a good idea for my particular application. I do not want to have a third party system access from inside my firewall. Altough, it is plain HTTP outgoing connection. The second case with all-sufficient SAML assertion I like more. From the top level overview it looks simple as that: End-User (browser) submit me (Relying party) ready to use passport (assertion). I trust this passport based in special trust relationship between me (Relying party) and asserting party who actually issued this passport. The important note here is I do not mention how this trust relationship has been established. This space is for another article I expect to publish in near future. Implementation. It is a good point here to start from utilizing ready to use SAML library. There is open source library called opensaml which can handle most of the SAML related operations. This is quite fresh library from the implementation point of view. And some times I find very strange architectural decisions in it. However, and it is more important for us, it works! So let's take all OpenSAML abstraction into our design and proceed with diagramming. At the first step, according to specification, browser post "Form with Response& Assertion" to our Servlet. Note: It is a little bit unusual here, why we submit response? In general it is clearer to submit requests. At least submit some kind of TransmitAssertionUnit, SAMLAssertionMessage or any other well suited for this particular satiation object name of the underlying abstraction. At the second step we have to construct BrowserProfileRequest object. It encapsulates data submitted for us by Asserting party through user browser in HttpRequest. At the third step we have to create binding profile we have chosen. In our case it is SAMLBrowserProfile. Let's use factory to create an instance and then perform response (actually, as I mentioned early it is request or just a message containing our assertion, but for some reason it is called BrowserProfileResponse) processing. Once we have BrowserProfileResponse object we have all information necessary to perform all other operations. It can be trust relationship validation step (As I mentioned earlier. See the next post for details). It can be some kind of authentications steps in which we create internal object represents authenticated user. And finally, we can redirect user to requested resource. Let's step ahead to actual coding. I want to have my servlet code clear. Thus let's introduce SsoSamlHelper here. This helper takes care of all implementation details. Then actual servlet looks like template method.

package org.ots.sso;

import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.NoSuchProviderException;
import org.opensaml.SAMLAssertion;
import org.opensaml.SAMLBrowserProfile;
import org.opensaml.SAMLBrowserProfileFactory;
import org.opensaml.SAMLException;
import org.opensaml.SAMLSignedObject;
import org.opensaml.UnsupportedProfileException;
import org.opensaml.SAMLBrowserProfile.BrowserProfileRequest;
import org.opensaml.SAMLBrowserProfile.BrowserProfileResponse;

import org.ots.ws.security.KeyStoreHolder;

/**
 * Useful static method to manipulate with SAML objects
 *
 * @version $Id$
 * @author Roman Kuzmik
 */
public class SsoSamlHelper {
    private static final Log log = LogFactory.getLog(SsoSamlHelper.class);

    /**
     * Exctract from HHTP request all SSO SAML related information
     *
     * @param httpRequest
     * @return
     * @throws SsoSamlException
     */
    public static BrowserProfileRequest createRequest(HttpServletRequest httpRequest) throws SsoSamlException{
        log.debug("");

        try {
            SAMLBrowserProfile profile = SAMLBrowserProfileFactory.getInstance();
            BrowserProfileRequest samlRequest = profile.receive(httpRequest);
            return samlRequest;
        } catch (UnsupportedProfileException e) {
            throw new SsoSamlException(e);
        } catch (NoSuchProviderException e) {
            throw new SsoSamlException(e);
        }
    }

    /**
     * Perform SAML assertion reconstruction from the BrowserProfileRequest provided
     *
     * @param samlRequest
     * @return
     * @throws SsoSamlException
     */
    public static BrowserProfileResponse processRequest(BrowserProfileRequest samlRequest) throws SsoSamlException{
        log.debug("");

        try {
            SAMLBrowserProfile profile = SAMLBrowserProfileFactory.getInstance();

            StringBuffer issuerBuffer = new StringBuffer();

            BrowserProfileResponse samlResponse = profile.receive(
                    issuerBuffer,
                    samlRequest,
                    SsoSamlConstants.SAML_SSO_RECEPIENT,
                    /*ReplayCache*/ null,
                    /*SAMLBrowserProfile.ArtifactMapper*/ null,
                    SsoSamlConstants.SAML_MINOR_VERSION
            );
                    
            log.debug("done");
            return samlResponse;
        } catch (UnsupportedProfileException e) {
            throw new SsoSamlException(e);
        } catch (NoSuchProviderException e) {
            throw new SsoSamlException(e);
        } catch (SAMLException e) {
            throw new SsoSamlException(e);
        }  
    }

    /**
     * Give me trust entity key
     *
     * @param assertion
     * @return
     */
    public static String getIssuer(SAMLAssertion assertion){
        return assertion.getIssuer().trim();
    }

    /**
     * WS-S Signature validation
     *
     * @param samlObject
     * @param issuer
     * @param keyStoreHolder
     * @throws SsoSamlException
     */
    public static void verifySignature(SAMLSignedObject samlObject, String issuer, KeyStoreHolder keyStoreHolder) throws SsoSamlException{
        log.debug("");
        try {
            samlObject.verify(keyStoreHolder.getPublicKey(issuer));
        } catch (KeyStoreException e) {
            throw new SsoSamlException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new SsoSamlException(e);
        } catch (UnrecoverableKeyException e) {
            throw new SsoSamlException(e);
        } catch (SAMLException e) {
            throw new SsoSamlException(e);
        }
        log.debug("done");
    }
}
Javadoc provided here should be self explanations. Now we can construct our servlet in three lines of code as follows:
BrowserProfileRequest samlRequest = SsoSamlHelper.createRequest(httpRequest);
BrowserProfileResponse samlResponse = SsoSamlHelper.processRequest(samlRequest);
String issuer = SsoSamlHelper.getIssuer(samlResponse.assertion);
SsoSamlHelper.verifySignature(samlResponse.response, issuer, keyStoreHolder);
String nameIdentifier = samlResponse.authnStatement.getSubject().getNameIdentifier().getName();

//auth user with username==nameIdentifier

//redirect to samlRequest.TARGET
That's it! Servlet out looks as follows:
10:10:27(DEBUG)[ws.security.KeyStoreHolder.initKeyStore():74] 
10:10:27(DEBUG)[ws.security.KeyStoreHolder.initTrustStore():84] 
10:10:27(DEBUG)[sso.SsoSamlServlet.service():42] [START] 
10:10:27(DEBUG)[sso.SsoSamlServlet.service():45] read HTTP request into BrowserProfileRequest 
10:10:27(DEBUG)[sso.SsoSamlHelper.createRequest():40] 
10:10:27(DEBUG)[sso.SsoSamlServlet.service():47] samlRequest.SAMLResponse: base 64 code here 
10:10:27(DEBUG)[sso.SsoSamlServlet.service():48] samlRequest.TARGET:target URL here
10:10:27(DEBUG)[sso.SsoSamlServlet.service():50] process BrowserProfileRequest into BrowserProfileResponse 
10:10:27(DEBUG)[sso.SsoSamlHelper.processRequest():54] 
10:10:28(DEBUG)[sso.SsoSamlHelper.processRequest():70] done 
10:10:28(DEBUG)[sso.SsoSamlServlet.service():54] issuer: 'My partner #1' 
10:10:28(DEBUG)[sso.SsoSamlServlet.service():56] verify response signature 10:10:28(DEBUG)[sso.SsoSamlHelper.verifySignature():86] 
10:10:28(INFO) [security.signature.Reference.verify():742] Verification successful for URI "#cMnRtaOAdFkQmoCoQUdn" 
10:10:28(DEBUG)[sso.SsoSamlHelper.verifySignature():98] done 
10:10:28(DEBUG)[sso.SsoSamlServlet.service():59] perform authentication 10:10:28(DEBUG)[sso.SsoSamlServlet.service():61] nameIdentifier: hemaTest 
10:10:28(DEBUG)[service.handler.SecurityManager.getUserPasswordFromSamlAssertion():39] retrieve runtime properties 
10:10:28(DEBUG)[service.handler.SecurityManager.getUserPasswordFromSamlAssertion():43] companyName:My partner #1 
10:10:28(DEBUG)[service.handler.SecurityManager.getUserPasswordFromSamlAssertion():44] loginId:partnerUser_1 
10:10:28(DEBUG)[sso.SsoSamlServlet.service():69]


Test: SAML Asserting party Implementation. At this point we have to test our code. In order to do it we have to implement Asserting party. This includes "Inter-Site Transfer Service" implementation and all necessary parts for the browser functionality. This is definitely quite enough for the next article.

References. www.opersaml.com - SAML 1.1 and 2.0 implementation SAML 1.1 specifications

2 comments:

Anonymous said...

Hi Roman!
I would like to know, if it is possible to obtain (somehow) other classess used by You in this example? I'm new in SSO/SAML but I'm trying a learn-by-example approach. If you would be so kind and provide me this classess somehow (e.g. you could put them on Your site or post me by e-mail) I would be very gratefull.
I'm at your disposal under jchwastowska(no_spam)@gmail.com.

Thank you in advance.
Joanna

Anonymous said...

Yea ... I would also be interested in seeing the supporting classes