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.
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.TARGETThat'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