📜 ⬆️ ⬇️

Sharing SAML 2.0 SSO Integration Experience

1. Background

Despite the fact that the Single Sign On (SSO) function exists, it has been discussed and used for a long time, in practice its implementation is often accompanied by overcoming a variety of problems. The goal of this article will be to show how to implement the simplest native Service Provider 1 (SP) for SAML 2.0 identity provider (idP) and use it to integrate SSO into a Java Web application.

One of our recent projects was the preparation and clustering of a portal solution for a large university. As part of the project, we are faced with the task of implementing (and also clustering) a single authentication function for the following systems:

  1. Liferay version 6.1.20-ee-ga2.
  2. A simple java web application.
  3. Google apps.

From the customer’s side, the main requirements for building SSO were put forward:
  1. To build SSO, the SAML 2.0 protocol must be used.
  2. Integration with Jasig CAS is required to keep existing systems running.
  3. LDAP is used to verify user authentication.

As idP, we decided to use Shibboleth ( http://shibboleth.net/about/index.html ) as an open source system that fully implements SAML 1.0 && SAML 2.0 protocols.
')
Difficult moments that we encountered in solving this problem:

  1. Lack of expertise on working with the SAML 2.0 protocol and the Shibboleth product.
  2. The raw and not yet well-structured Shibboleth documentation from the manufacturer.
  3. Lack of quality examples on the implementation of a Service Provider to integrate SSO into its Java Web application.

Overcoming these barriers has become the motivation for publishing this article. We want to share the acquired knowledge, help developers solve such problems, and also facilitate familiarity with the SAML 2.0 protocol.

2. Who is the article for?

This article is aimed at the following audience:
  1. Developers integrating the SSO feature in their projects using SAML 2.0.
  2. Java developers who need a practical example of integrating SSO into their application using SAML 2.0.
  3. Java developers who want to try out Shibboleth components as SSO Identity Provider (idP).

To understand the article, it is recommended to have minimal knowledge of the SAML 2.0 protocol.

3. Main components of SSO operation

The diagram below shows the general operation of our centralized entrance.



The main components and points noted in the diagram:
  1. 2 applications participate in the SSO system:
    a. Java Web App - A Normal Java Web Application
    b. Google Apps is an application from Google cloud services. We will use it only to test the operation of SSO.
  2. SP Filter is an implementation of a Service Provider whose function will be to interact with Shibboleth idP means of sending and parsing SAML 2.0 messages
  3. Shibboleth idP is an application for authentication and authorization using SAML 1.0 and SAML 2.0.
  4. Tomcat AS - Java Application Server.
  5. The interaction between SP filter and Shibboleth idP occurs via a secure HTTPS protocol.

Note: In the Shibboleth idP diagram and the Java Web application are physically separated into different Tomcat servers. However, you can deploy the environment on a single network node using just one Tomcat instance.

4. Configure the environment for Shibboleth idP

Install and configure shibboleth idP:

1. Download the latest version of idP here shibboleth.net/downloads/identity-provider/latest 2 and unzip $ shDistr to an arbitrary place.
2. Check that the JAVA_HOME variable is set correctly 3 .
Run $ shDistr / install.sh (we will assume that you are using a UNIX-like operating system). four

The installer will request the following information to keep in mind:

Further we check that the installation process is successfully completed.

We introduce the notation:


3. Determine which attributes and from which sources idP will be extracted. In our case, we will transfer the user login. Add the attribute description to the $ shHome / conf / attribute-resolver.xml file after the <resolver: AttributeDefinition id = "transientId" xsi: type = "ad: TransientId"> element.

<resolver:AttributeDefinition xsi:type="PrincipalName" xmlns="urn:mace:shibboleth:2.0:resolver:ad" id="userLogin" > <resolver:AttributeEncoder xsi:type="SAML1String" xmlns="urn:mace:shibboleth:2.0:attribute:encoder" name="userLogin" /> <resolver:AttributeEncoder xsi:type="SAML2String" xmlns="urn:mace:shibboleth:2.0:attribute:encoder" name="userLogin" /> </resolver:AttributeDefinition> 

Note: in the same file, you can configure the retrieval of attributes from various data sources such as LDAP or DBMS via JDBC. Read more here https://wiki.shibboleth.net/confluence/display/SHIB2/IdPAddAttribute .

4. In order for idP to give this SAML SP attribute to the filter, we describe it in the $ shHome / conf / attribute-filter.xml file .

 <afp:AttributeFilterPolicy id="releaseUserLoginToAnyone"> <afp:PolicyRequirementRule xsi:type="basic:ANY"/> <afp:AttributeRule attributeID="userLogin"> <afp:PermitValueRule xsi:type="basic:ANY"/> </afp:AttributeRule> </afp:AttributeFilterPolicy> 

Note: Here you can set a more complex and correct rule. For example, you can specify that this attribute is transmitted only to a specific SAML SP.

5. Our Shibboleth idP should know about the nodes with which it can interact - the so-called relying party ( https://wiki.shibboleth.net/confluence/display/SHIB2/IdPUnderstandingRP ). This information is stored in the $ shHome / conf / relying-party.xml file .
Open the file and add the following element to it:

 <rp:RelyingParty id="sp.local.ru" provider="https://idp.local.ru/idp/shibboleth" defaultSigningCredentialRef="IdPCredential"> <rp:ProfileConfiguration xsi:type="saml:SAML2SSOProfile" signResponses="never" signAssertions="never" encryptNameIds="never" encryptAssertions="never" /> </rp:RelyingParty> 

Here we indicate that for SP with id = "sp.local.ru" idP will be used with id = " https://idp.local.ru/idp/shibboleth ".

Add SP to the list of aliases for localhost in the / etc / hosts file:
127.0.0.1 localhost sp.local.ru

We also instruct shibboleth idP not to sign SAML 2.0 responses and a set of assertions. Up to the current time, our shibboleth idP had no idea what the component with id = "sp.local.ru" was. Time to fix this moment. We go to the next step.

6. Add a description of our SAML 2.0 SP filter. To do this, in the $ shHome / conf / relying-party.xml file, we define the meta information for our SP, next to the <metadata: MetadataProvider element id = "IdPMD" xsi: type = "metadata: FilesystemMetadataProvider" ...>

 <metadata:MetadataProvider id="spMD" xsi:type="metadata:FilesystemMetadataProvider" metadataFile="/opt/shib/metadata/saml-sp-metadata.xml"/> 

We instructed shibboleth idP to look for the SP definition in the /opt/shib/metadata/saml-sp-metadata.xml file. Create this file with the following content:

 <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="sp.local.ru"> <md:SPSSODescriptor AuthnRequestsSigned="false" ID="sp.local.ru" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.local.ru:8443/sso/acs" index="1" isDefault="true"/> </md:SPSSODescriptor> </md:EntityDescriptor> 

Here you need to understand the following:


7. It remains to choose the way in which shibboleth idP will perform real user authentication. In the production environment there can be a variety of configurations, including authentication via LDAP, DBMS and even CAS. Here, as they say, the taste and color. We will use the already enabled Remote User Authentication mechanism ( https://wiki.shibboleth.net/confluence/display/SHIB2/IdPAuthRemoteUser ). When receiving a shibboleth idP authentication request, it will look in the context of the REMOTE_USER variable. If there is such a variable, then shibboleth idP will assume that the user has already been authenticated through the external system (for example, through the Apache Web server). In order not to complicate this article, we decided to go for the trick and set the variable REMOTE_USER artificially for each request.
This will be done in the next section when configuring Tomcat AS (step 7).

Shibboleth setup is complete, congratulations :)

Install and configure Tomcat for shibboleth idP:

  1. Download tomkat 6 http://tomcat.apache.org/download-60.cgi , unzip it into an arbitrary $ tomcatHome folder (for example: in opt / shib-tomcat).

    It is important to note that currently Tomcat 7. * cannot be used when communication between SP and idP occurs directly via the SOAP protocol. And although in the examples of this article we will use direct browser redirects to implement these communications, we still recommend using Tomcat version 6.
  2. Copy the $ shDistr / endorsed folder into the $ tomcatHome folder.
  3. We change the $ tomcatHome / bin / setenv.s h file , set the settings for the dynamic and permanent JVM memory:
    JAVA_OPTS = "$ JAVA_OPTS -Xmx512m -XX: MaxPermSize = 128m"
  4. Download the library ( https://build.shibboleth.net/nexus/content/repositories/releases/edu/internet2/middleware/security/tomcat6/tomcat6-dta-ssl/1.0.0/tomcat6-dta-ssl-1.0.0 .jar ) to support the SOAP protocol in the process of communication between SP and idP in the $ tomcatHome / lib folder.
    Open $ tomcatHome / conf / server.xml and set up access to Tomcat via HTTPS.
    To do this, we define the following Connector element:

     <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol" SSLImplementation="edu.internet2.middleware.security.tomcat6.DelegateToApplicationJSSEmplementation" scheme="https" SSLEnabled="true" clientAuth="want" keystoreFile="$shHome/credentials/idp.jks" keystorePass="$shPassword" /> 


    Remember to replace the $ shHome and $ shPassword variables with real values .
  5. Deploy the shibboleth idP application to Tomcat. To do this, create a file
    $ tomcatHome / conf / Catalina / localhost / idp.xml with contents:

     <Context docBase="$shHome/war/idp.war" privileged="true" antiResourceLocking="false" antiJARLocking="false" unpackWAR="false" swallowOutput="true" /> 


    Remember to replace the $ shHome variables with a real value.
  6. Compile 5 the following class into the arbitrary library tomcat-valve.jar:

      public class RemoteUserValve extends ValveBase{ public RemoteUserValve() { } @Override public void invoke(final Request request, final Response response) throws IOException, ServletException { final String username = "idpuser"; final String credentials = "idppass"; final List<String> roles = new ArrayList<String>(); final Principal principal = new GenericPrincipal(null, username, credentials, roles); request.setUserPrincipal(principal); getNext().invoke(request, response); } } 

    Library put in the folder $ {tomcatHome} / lib. And add the line to server.xml file
    <Valve lassName = "ru.eastbanctech.java.web.RemoteUserValve" /> inside the element
    <Host name = "localhost" appBase = "webapps" ..>. After starting the server, when accessing any Tomcat server application, the REMOTE_USER parameter with the idpuser value will be automatically put into the context of the request.

5. Implementing SP Filter for SAML 2.0 Protocol

To implement this solution, we will create a SAML 2.0 Service Provider filter whose tasks will be:
  1. The filter skips requests for public resources for which no authentication is required.
  2. The filter stores information on an authenticated user in order to reduce the number of calls to Shibboleth idP.
  3. The filter creates a SAML 2.0 authentication request in the form of a SAML 2.0 message ( AuthN ) and redirects the browser to redirect the user to Shibboleth idP.
  4. The filter processes the response from the Shibboleth idP, and if the user authentication process is successful, the system shows the originally requested resource.
  5. The filter removes the local session when the user logs out of the Java Web application.
  6. At the same time, the session on shibboleth idP continues to be active.


From a technical point of view, the filter will be an implementation of the standard javax.filter.Filter interface. The filter scope will be specified in a specific web application.

Now that the filter functionality is clear, let's proceed to the implementation:
1. Create a skeleton maven project
You can do it through the mvn plugin: archetype:
mvn archetype: generate -DgroupId = en.eastbanctech.java.web -DartifactId = saml-sp-filter -DarchetypeArtifactId = maven-archetype-quickstart -DinteractiveMode = false
The parameters groupId and artefactId can indicate your taste and color.
The structure of our project in Intellij Idea will look like this:


2. Assembly file pom.xml:

Code
 <source lang="xml"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ru.eastbanctech.web</groupId> <artifactId>saml-sp-filter</artifactId> <name>${project.artifactId}</name> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <jdk.version>1.6</jdk.version> <encoding>UTF-8</encoding> <project.build.sourceEncoding>${encoding}</project.build.sourceEncoding> <project.reporting.outputEncoding>${encoding}</project.reporting.outputEncoding> </properties> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <encoding>${encoding}</encoding> <soure>${jdk.version}</soure> <target>${jdk.version}</target> </configuration> </plugin> </plugins> </pluginManagement> </build> <dependency> <groupId>org.opensaml</groupId> <artifactId>opensaml</artifactId> <version>2.5.1-1</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.1</version> </dependency> </dependencies> </project> 

3. The heart of our filter will be the class SAMLSPFilter:

 public class SAMLSPFilter implements Filter { public static final String SAML_AUTHN_RESPONSE_PARAMETER_NAME = "SAMLResponse"; private static Logger log = LoggerFactory.getLogger(SAMLSPFilter.class); private FilterConfig filterConfig; private SAMLResponseVerifier checkSAMLResponse; private SAMLRequestSender samlRequestSender; @Override public void init(javax.servlet.FilterConfig config) throws ServletException { OpenSamlBootstrap.init(); filterConfig = new FilterConfig(config); checkSAMLResponse = new SAMLResponseVerifier(); samlRequestSender = new SAMLRequestSender(); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; /*  1:  ,      2:     Shibboleth idP,    3:     logout,     4:    ,      5:  SAML        Shibboleth idP */ } } 


In the FilterConfig class, we define the main filter variables (filter scope, idP name, idP metadata path, SP name, etc.). The values ​​of these parameters will be set in the Java web application's web.xml configuration file. The checkSAMLResponse and samlRequestSender objects are needed to check the validity of SAML 2.0 messages and send an authentication request. We will return to them later.

Code
 public class FilterConfig { /** * The parameters below should be defined in web.xml file of Java Web Application */ public static final String EXCLUDED_URL_PATTERN_PARAMETER = "excludedUrlPattern"; public static final String SP_ACS_URL_PARAMETER = "acsUrl"; public static final String SP_ID_PARAMETER = "spProviderId"; public static final String SP_LOGOUT_URL_PARAMETER = "logoutUrl"; public static final String IDP_SSO_URL_PARAMETER = "idProviderSSOUrl"; private String excludedUrlPattern; private String acsUrl; private String spProviderId; private String logoutUrl; private String idpSSOUrl; public FilterConfig(javax.servlet.FilterConfig config) { excludedUrlPattern = config.getInitParameter(EXCLUDED_URL_PATTERN_PARAMETER); acsUrl = config.getInitParameter(SP_ACS_URL_PARAMETER); spProviderId = config.getInitParameter(SP_ID_PARAMETER); idpSSOUrl = config.getInitParameter(IDP_SSO_URL_PARAMETER); logoutUrl = config.getInitParameter(SP_LOGOUT_URL_PARAMETER); } // getters and should be defined below }  OpenSamlBootstrap      SAML 2.0 : public class OpenSamlBootstrap extends DefaultBootstrap { private static Logger log = LoggerFactory.getLogger(OpenSamlBootstrap.class); private static boolean initialized; private static String[] xmlToolingConfigs = { "/default-config.xml", "/encryption-validation-config.xml", "/saml2-assertion-config.xml", "/saml2-assertion-delegation-restriction-config.xml", "/saml2-core-validation-config.xml", "/saml2-metadata-config.xml", "/saml2-metadata-idp-discovery-config.xml", "/saml2-metadata-query-config.xml", "/saml2-metadata-validation-config.xml", "/saml2-protocol-config.xml", "/saml2-protocol-thirdparty-config.xml", "/schema-config.xml", "/signature-config.xml", "/signature-validation-config.xml" }; public static synchronized void init() { if (!initialized) { try { initializeXMLTooling(xmlToolingConfigs); } catch (ConfigurationException e) { log.error("Unable to initialize opensaml DefaultBootstrap", e); } initializeGlobalSecurityConfiguration(); initialized = true; } } } 

A set of XML files contains instructions on how to parse the elements of SAML 2.0 messages and is contained in the opensaml - *. Jar library, which will connect when building a project through maven.

STEP 1: Ignore non-filter requests
The parameter excludedUrlPattern , which encloses a regular expression. If the requested resource falls into the excludedUrlPattern template, the filter does not process it:

 if (!isFilteredRequest(request)) { log.debug("According to {} configuration parameter request is ignored + {}", new Object[]{FilterConfig.EXCLUDED_URL_PATTERN, request.getRequestURI()}); chain.doFilter(servletRequest, servletResponse); return; } //     ,       private boolean isFilteredRequest(HttpServletRequest request) { return !(filterConfig.getExcludedUrlPattern() != null && getCorrectURL(request).matches(filterConfig.getExcludedUrlPattern())); } //       URL private String getCorrectURL(HttpServletRequest request) { String contextPath = request.getContextPath(); String requestUri = request.getRequestURI(); int contextBeg = requestUri.indexOf(contextPath); int contextEnd = contextBeg + contextPath.length(); String slash = "/"; String url = (contextBeg < 0 || contextEnd == (requestUri.length() - 1)) ? requestUri : requestUri.substring(contextEnd); if (!url.startsWith(slash)) { url = slash + url; } return url; } 


Step 2: If the answer came from Shibboleth idP, we process it
We are looking for the “SAMLResponse” parameter in the request and if it is found, it means that we received a response from the shibboleth idP to the authentication request. Getting to the processing of SAML 2.0 messages.

Code
 log.debug("Attempt to secure resource is intercepted : {}", ((HttpServletRequest) servletRequest).getRequestURL().toString()); /* Check if response message is received from identity provider; In case of successful response system redirects user to relayState (initial) request */ String responseMessage = servletRequest.getParameter(SAML_AUTHN_RESPONSE_PARAMETER_NAME); if (responseMessage != null) { log.debug("Response from Identity Provider is received"); try { log.debug("Decoding of SAML message"); SAMLMessageContext samlMessageContext = SAMLUtils.decodeSamlMessage((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); log.debug("SAML message has been decoded successfully"); samlMessageContext.setLocalEntityId(filterConfig.getSpProviderId()); String relayState = getInitialRequestedResource(samlMessageContext); checkSAMLResponse.verify(samlMessageContext); log.debug("Starting and store SAML session.."); SAMLSessionManager.getInstance().createSAMLSession(request.getSession(), samlMessageContext); log.debug("User has been successfully authenticated in idP. Redirect to initial requested resource {}", relayState); response.sendRedirect(relayState); return; } catch (Exception e) { throw new ServletException(e); } } 


To do this, we decode the SAML message in the SAMLUtils.decodeSamlMessage (..) method, check the validity of the SAML assertions - checkSAMLResponse.verify (..) . If all checks are performed, then create an internal SAML session SAMLSessionManager.getInstance (). CreateSAMLSession (..) and redirect the user to the originally requested resource response.sendRedirect (..).

In the SAMLUtils class, we will place useful intermediate methods when working with SAML 2.0 messages. One such method would be the decodeSamlMessage method, which decodes messages received via SAML 2.0 HTTPS.
Code
 public class SAMLUtils { public static SAMLMessageContext decodeSamlMessage(HttpServletRequest request, HttpServletResponse response) throws Exception { SAMLMessageContext<SAMLObject, SAMLObject, NameID> samlMessageContext = new BasicSAMLMessageContext<SAMLObject, SAMLObject, NameID>(); HttpServletRequestAdapter httpServletRequestAdapter = new HttpServletRequestAdapter(request); samlMessageContext.setInboundMessageTransport(httpServletRequestAdapter); samlMessageContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS); HttpServletResponseAdapter httpServletResponseAdapter = new HttpServletResponseAdapter(response, request.isSecure()); samlMessageContext.setOutboundMessageTransport(httpServletResponseAdapter); samlMessageContext.setPeerEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME); SecurityPolicyResolver securityPolicyResolver = getSecurityPolicyResolver(request.isSecure()); samlMessageContext.setSecurityPolicyResolver(securityPolicyResolver); HTTPPostDecoder samlMessageDecoder = new HTTPPostDecoder(); samlMessageDecoder.decode(samlMessageContext); return samlMessageContext; } private static SecurityPolicyResolver getSecurityPolicyResolver(boolean isSecured) { SecurityPolicy securityPolicy = new BasicSecurityPolicy(); HTTPRule httpRule = new HTTPRule(null, null, isSecured); MandatoryIssuerRule mandatoryIssuerRule = new MandatoryIssuerRule(); List<SecurityPolicyRule> securityPolicyRules = securityPolicy.getPolicyRules(); securityPolicyRules.add(httpRule); securityPolicyRules.add(mandatoryIssuerRule); return new StaticSecurityPolicyResolver(securityPolicy); } } 

In the same class we will place an auxiliary method for converting SAML objects into String. This will be useful when logging SAML messages.
 public static String SAMLObjectToString(XMLObject samlObject) { try { Marshaller marshaller = org.opensaml.Configuration.getMarshallerFactory().getMarshaller(samlObject); org.w3c.dom.Element authDOM = marshaller.marshall(samlObject); StringWriter rspWrt = new StringWriter(); XMLHelper.writeNode(authDOM, rspWrt); return rspWrt.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } 

Create a SAMLResponseVerifier class in which we place the functionality for checking SAML 2.0 messages received from shibboleth idP. In the main verify (..) method, we implement the following checks:


Code
 public class SAMLResponseVerifier { private static Logger log = LoggerFactory.getLogger(SAMLResponseVerifier.class); private SAMLRequestStore samlRequestStore = SAMLRequestStore.getInstance(); public void verify(SAMLMessageContext<Response, SAMLObject, NameID> samlMessageContext) throws SAMLException { Response samlResponse = samlMessageContext.getInboundSAMLMessage(); log.debug("SAML Response message : {}", SAMLUtils.SAMLObjectToString(samlResponse)); verifyInResponseTo(samlResponse); Status status = samlResponse.getStatus(); StatusCode statusCode = status.getStatusCode(); String statusCodeURI = statusCode.getValue(); if (!statusCodeURI.equals(StatusCode.SUCCESS_URI)) { log.warn("Incorrect SAML message code : {} ", statusCode.getStatusCode().getValue()); throw new SAMLException("Incorrect SAML message code : " + statusCode.getValue()); } if (samlResponse.getAssertions().size() == 0) { log.error("Response does not contain any acceptable assertions"); throw new SAMLException("Response does not contain any acceptable assertions"); } Assertion assertion = samlResponse.getAssertions().get(0); NameID nameId = assertion.getSubject().getNameID(); if (nameId == null) { log.error("Name ID not present in subject"); throw new SAMLException("Name ID not present in subject"); } log.debug("SAML authenticated user " + nameId.getValue()); verifyConditions(assertion.getConditions(), samlMessageContext); } private void verifyInResponseTo(Response samlResponse) { String key = samlResponse.getInResponseTo(); if (!samlRequestStore.exists(key)) { { log.error("Response does not match an authentication request"); throw new RuntimeException("Response does not match an authentication request"); } samlRequestStore.removeRequest(samlResponse.getInResponseTo()); } private void verifyConditions(Conditions conditions, SAMLMessageContext samlMessageContext) throws SAMLException{ verifyExpirationConditions(conditions); verifyAudienceRestrictions(conditions.getAudienceRestrictions(), samlMessageContext); } private void verifyExpirationConditions(Conditions conditions) throws SAMLException { log.debug("Verifying conditions"); DateTime currentTime = new DateTime(DateTimeZone.UTC); log.debug("Current time in UTC : " + currentTime); DateTime notBefore = conditions.getNotBefore(); log.debug("Not before condition : " + notBefore); if ((notBefore != null) && currentTime.isBefore(notBefore)) throw new SAMLException("Assertion is not conformed with notBefore condition"); DateTime notOnOrAfter = conditions.getNotOnOrAfter(); log.debug("Not on or after condition : " + notOnOrAfter); if ((notOnOrAfter != null) && currentTime.isAfter(notOnOrAfter)) throw new SAMLException("Assertion is not conformed with notOnOrAfter condition"); } private void verifyAudienceRestrictions( List<AudienceRestriction> audienceRestrictions, SAMLMessageContext<?, ?, ?> samlMessageContext) throws SAMLException{ // TODO: Audience restrictions should be defined below<sup>7</sup> } } 


The verifyInResponseTo method checks that the SAML 2.0 response was preceded by a request from our filter. For the implementation, an object of the SAMLRequestStore class is used, which stores the shibboleth idP sent by SAML 2.0.
Code
 final public class SAMLRequestStore { private Set<String> samlRequestStorage = new HashSet<String>(); private IdentifierGenerator identifierGenerator = new RandomIdentifierGenerator(); private static SAMLRequestStore instance = new SAMLRequestStore(); private SAMLRequestStore() { } public static SAMLRequestStore getInstance() { return instance; } public synchronized void storeRequest(String key) { if (samlRequestStorage.contains(key)) throw new RuntimeException("SAML request storage has already contains key " + key); samlRequestStorage.add(key); } public synchronized String storeRequest(){ String key = null; while (true) { key = identifierGenerator.generateIdentifier(20); if (!samlRequestStorage.contains(key)){ storeRequest(key); break; } } return key; } public synchronized boolean exists(String key) { return samlRequestStorage.contains(key); } public synchronized void removeRequest(String key) { samlRequestStorage.remove(key); } } 



To create a local session, we will use our SAMLSessionManager class. Its task will be to create / destroy local sessions, which is an object of the next class SAMLSessionInfo.

 public class SAMLSessionInfo { private String nameId; private Map<String, String> attributes; private Date validTo; public SAMLSessionInfo(String nameId, Map<String, String> attributes, Date validTo) { this.nameId = nameId; this.attributes = attributes; this.validTo = validTo; } // getters should be defined below } 

In fact, the SAMLSManager class itself, which creates and destroys local SAML sessions in the Session context of the certificate, using SAMLContext.
Code
 <source lang="java"> public class SAMLSessionManager { public static String SAML_SESSION_INFO = "SAML_SESSION_INFO"; private static SAMLSessionManager instance = new SAMLSessionManager(); private SAMLSessionManager() { } public static SAMLSessionManager getInstance() { return instance; } public void createSAMLSession(HttpSession session, SAMLMessageContext<Response, SAMLObject, NameID> samlMessageContext) { List<Assertion> assertions = samlMessageContext.getInboundSAMLMessage().getAssertions(); NameID nameId = (assertions.size() != 0 && assertions.get(0).getSubject() != null) ? assertions.get(0).getSubject().getNameID() : null; String nameValue = nameId == null ? null : nameId.getValue(); SAMLSessionInfo samlSessionInfo = new SAMLSessionInfo(nameValue, getAttributesMap(getSAMLAttributes(assertions)), getSAMLSessionValidTo(assertions)); session.setAttribute(SAML_SESSION_INFO, samlSessionInfo); } public boolean isSAMLSessionValid(HttpSession session) { SAMLSessionInfo samlSessionInfo = (SAMLSessionInfo) session.getAttribute(SAML_SESSION_INFO); if (samlSessionInfo == null) return false; return samlSessionInfo.getValidTo() == null || new Date().before(samlSessionInfo.getValidTo()); } public void destroySAMLSession(HttpSession session) { session.removeAttribute(SAML_SESSION_INFO); } public List<Attribute> getSAMLAttributes(List<Assertion> assertions) { List<Attribute> attributes = new ArrayList<Attribute>(); if (assertions != null) { for (Assertion assertion : assertions) { for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { for (Attribute attribute : attributeStatement.getAttributes()) { attributes.add(attribute); } } } } return attributes; } public Date getSAMLSessionValidTo(List<Assertion> assertions) { org.joda.time.DateTime sessionNotOnOrAfter = null; if (assertions != null) { for (Assertion assertion : assertions) { for (AuthnStatement statement : assertion.getAuthnStatements()) { sessionNotOnOrAfter = statement.getSessionNotOnOrAfter(); } } } return sessionNotOnOrAfter != null ? sessionNotOnOrAfter.toCalendar(Locale.getDefault()).getTime() : null; } public Map<String, String> getAttributesMap(List<Attribute> attributes) { Map<String, String> result = new HashMap<String, String>(); for (Attribute attribute : attributes) { result.put(attribute.getName(), attribute.getDOM().getTextContent()); } return result; } } 


Step 3: If a logout request is received, delete the local session

 if (getCorrectURL(request).equals(filterConfig.getLogoutUrl())) { log.debug("Logout action: destroying SAML session."); SAMLSessionManager.getInstance().destroySAMLSession(request.getSession()); chain.doFilter(request, response); return; } 

Note: It is worth noting that the session remains active on shibboleth idP and on the next request for authentication shibboleth idP will simply return us to the active session. The implementation of the global logout requires additional configuration and up to version 2.4.0, shibboleth idP was not supported. You can read more here https://wiki.shibboleth.net/confluence/display/SHIB2/SLOIssues

Step 4: If the user is already authenticated, give access to the resource

If the user has an active SAML session in our filter, then we give the user this resource.
 if (SAMLSessionManager.getInstance().isSAMLSessionValid(request.getSession())) { log.debug("SAML session exists and valid: grant access to secure resource"); chain.doFilter(request, response); return; } 


Step 5: Create a SAML authentication request and send the user to
Shibboleth idP


 log.debug("Sending authentication request to idP"); try { samlRequestSender .sendSAMLAuthRequest(request, response, filterConfig.getSpProviderId(), filterConfig.getAcsUrl(), filterConfig.getIdpSSOUrl()); } catch (Exception e) { throw new ServletException(e); } 

The SAMLRequestSender class creates, codes, and sends requests as SAML 2.0 messages.

Code
 <source lang="java"> public class SAMLRequestSender { private static Logger log = LoggerFactory.getLogger(SAMLRequestSender.class); private SAMLAuthnRequestBuilder samlAuthnRequestBuilder = new SAMLAuthnRequestBuilder(); private MessageEncoder messageEncoder = new MessageEncoder(); public void sendSAMLAuthRequest(HttpServletRequest request, HttpServletResponse servletResponse, String spId, String acsUrl, String idpSSOUrl) throws Exception { String redirectURL; String idpUrl = idpSSOUrl; AuthnRequest authnRequest = samlAuthnRequestBuilder.buildRequest(spId, acsUrl, idpUrl); // store SAML 2.0 authentication request String key = SAMLRequestStore.getInstance().storeRequest(); authnRequest.setID(key); log.debug("SAML Authentication message : {} ", SAMLUtils.SAMLObjectToString(authnRequest)); redirectURL = messageEncoder.encode(authnRequest, idpUrl, request.getRequestURI()); HttpServletResponseAdapter responseAdapter = new HttpServletResponseAdapter(servletResponse, request.isSecure()); HTTPTransportUtils.addNoCacheHeaders(responseAdapter); HTTPTransportUtils.setUTF8Encoding(responseAdapter); responseAdapter.sendRedirect(redirectURL); } private static class SAMLAuthnRequestBuilder { public AuthnRequest buildRequest(String spProviderId, String acsUrl, String idpUrl){ /* Building Issuer object */ IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:assertion", "Issuer", "saml2p"); issuer.setValue(spProviderId); /* Creation of AuthRequestObject */ DateTime issueInstant = new DateTime(); AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); AuthnRequest authRequest = authRequestBuilder.buildObject(SAMLConstants.SAML20P_NS, "AuthnRequest", "saml2p"); authRequest.setForceAuthn(false); authRequest.setIssueInstant(issueInstant); authRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); authRequest.setAssertionConsumerServiceURL(acsUrl); authRequest.setIssuer(issuer); authRequest.setNameIDPolicy(nameIdPolicy); authRequest.setVersion(SAMLVersion.VERSION_20); authRequest.setDestination(idpUrl); return authRequest; } } private static class MessageEncoder extends HTTPRedirectDeflateEncoder { public String encode(SAMLObject message, String endpointURL, String relayState) throws MessageEncodingException { String encodedMessage = deflateAndBase64Encode(message); return buildRedirectURL(endpointURL, relayState, encodedMessage); } public String buildRedirectURL(String endpointURL, String relayState, String message) throws MessageEncodingException { URLBuilder urlBuilder = new URLBuilder(endpointURL); List<Pair<String, String>> queryParams = urlBuilder.getQueryParams(); queryParams.clear(); queryParams.add(new Pair<String, String>("SAMLRequest", message)); if (checkRelayState(relayState)) { queryParams.add(new Pair<String, String>("RelayState", relayState)); } return urlBuilder.buildURL(); } } } 

The SAML 2.0 message with the user authentication instruction is created in the buildRequest method and is an XML object:

 <saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="https://sp.local.ru:8443/sso/acs" Destination="https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO" ForceAuthn="false" ID="_0ddb303f9500839762eabd30e7b1e3c28b596c69" IssueInstant="2013-09-12T09:46:41.882Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"> <saml2p:Issuer xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:assertion">sp.local.ru</saml2p:Issuer> </saml2p:AuthnRequest> 


AssertionConsumerServiceURL URL, shibboleth idP , ProtocolBinding (POST HTTP)
ID ,
String key = SAMLRequestStore.getInstance().storeRequest();
verifyInResponseTo SAMLResponseVerifier .

saml2p:Issuer SP. saml2p:Issuer shibboleth idP SP , ( SP).

In response to the above SAML 2.0 message, we will receive a response from idP as a SAML 2.0 message in XML format:

Code
 <source lang="xml"> <saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://sp.local.ru:8443/sso/acs" ID="_9c5e6028df334510cce22409ddbca6ac" InResponseTo="_0ddb303f9500839762eabd30e7b1e3c28b596c69" IssueInstant="2013-09-12T10:13:35.177Z" Version="2.0"> <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"> https://idp.local.ru/idp/shibboleth </saml2:Issuer> <saml2p:Status> <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </saml2p:Status> <saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_0a299e86f4b17b5e047735121a880ccb" IssueInstant="2013-09-12T10:13:35.177Z" version="2.0"> <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"> https://idp.local.ru/idp/shibboleth </saml2:Issuer> <saml2:Subject> <saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="https://idp.local.ru/idp/shibboleth"> _f1de09ee54294d4b5ddeb3aa5e6d2aab </saml2:NameID> <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml2:SubjectConfirmationData Address="127.0.0.1" InResponseTo="_0ddb303f9500839762eabd30e7b1e3c28b596c69" NotOnOrAfter="2013-09-12T10:18:35.177Z" Recipient="https://sp.local.ru:8443/sso/acs"/> </saml2:SubjectConfirmation> </saml2:Subject> <saml2:Conditions NotBefore="2013-09-12T10:13:35.177Z" NotOnOrAfter="2013-09-12T10:18:35.177Z"> <saml2:AudienceRestriction> <saml2:Audience>sp.local.ru</saml2:Audience> </saml2:AudienceRestriction> </saml2:Conditions> <saml2:AuthnStatement AuthnInstant="2013-09-12T10:13:35.137Z" SessionIndex="_91826738984ca8bef18a8450135b1821"> <saml2:SubjectLocality Address="127.0.0.1"/> <saml2:AuthnContext> <saml2:AuthnContextClassRef> urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport </saml2:AuthnContextClassRef> </saml2:AuthnContext> </saml2:AuthnStatement> <saml2:AttributeStatement> <saml2:Attribute Name="userLogin" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">idpuser</saml2:AttributeValue> </saml2:Attribute> </saml2:AttributeStatement> </saml2:Assertion> </saml2p:Response> 

The message will be processed in the already implemented method SAMLResponseVerifier.verify (..)
That's all, our filter is implemented!
The structure of our project is as follows:


Putting the implemented filter in the jar library into a local repository.
To do this, execute the command in the directory with pom.xml: mvn clean install

6. Create a Java Web application with SSO support

Create Java Web Application

For a visual example, we will create a simple Java Web application with private and public resources. Access to private resources requires user authentication through the Shibboleth idP web application. One of the private resources will make a page that displays information on the current user of the system.
The structure of our application is as follows:


pom.xml

Code
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId> ru.eastbanctech.web</groupId> <artifactId>SimpleSSOApplication</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>SimpleSSOApplication</name> <url>http://maven.apache.org</url> <!--      --> <properties> <sp.id>sp.local.ru</sp.id> <acs.url>https://sp.local.ru:8443/sso/acs</acs.url> <idp.sso.url>https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO</idp.sso.url> <logout.url>/logout</logout.url> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId> ru.eastbanctech.web</groupId> <artifactId>saml-sp-filter</artifactId> <version>1.0-SNAPSHOT</version> </dependency><sup>8</sup> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.1</version> </dependency> </dependencies> <build> <finalName>sso</finalName> <plugins> <plugin> <artifactId>maven-war-plugin</artifactId> <configuration> <webResources> <resource> <filtering>true</filtering> <directory>src/main/webapp/WEB-INF</directory> <targetPath>WEB-INF</targetPath> <includes> <include>**/*.xml</include> </includes> </resource> </webResources> </configuration> </plugin> </plugins> </build> </project> 

Here it is necessary to turn to the properties section , where the basic parameters of our
<sp.id> sp.local.ru </sp.id> filter are set - the name of the SAML 2.0 SP filter
<acs.url> https://sp.local.ru: 8443 / sso / acs </acs.url> - the URL of the filter by which it
will process SAML 2.0 messages from shibboleth idP
<idp.sso.url> https://idp.local.ru:8443/idp/profile/SAML2 / Redirect / SSO </idp.sso.url> - the URL by
which our filter will send messages shibboleth idP
<logout.url> / logout </logout.url> - logout URL

web.xml

In the web.xml file we define the parameters our filter and its scope. Make resources in the format ".jpg"open through the parameter excludedUrlPattern .

Code
 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Simple SSO Java Web Application</display-name>7. <filter> <filter-name>SSOFilter</filter-name> <filter-class> ru.eastbanctech.java.web.filter.saml.SAMLSPFilter</filter-class> <init-param> <param-name>excludedUrlPattern</param-name> <param-value>.*\.jpg</param-value> </init-param> <init-param> <param-name>idProviderSSOUrl</param-name> <param-value> ${idp.sso.url}</param-value> </init-param> <init-param> <param-name>spProviderId</param-name> <param-value>${sp.id}</param-value> </init-param> <init-param> <param-name>acsUrl</param-name> <param-value>${acs.url}</param-value> </init-param> <init-param> <param-name>logoutUrl</param-name> <param-value>${logout.url}</param-value> </init-param> </filter> <filter-mapping> <filter-name>SSOFilter</filter-name> <url-pattern>/pages/private/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>SSOFilter</filter-name> <url-pattern>${logout.url}</url-pattern> </filter-mapping> <filter-mapping> <filter-name>SSOFilter</filter-name> <url-pattern>/acs</url-pattern> </filter-mapping> </web-app> 


private / page.jsp A

page is simply a listing of the id and attributes of an authenticated user.

Code
 <%@ page import=" ru.eastbanctech.java.web.filter.saml.store.SAMLSessionManager" %> <%@ page import=" ru.eastbanctech.java.web.filter.saml.store.SAMLSessionInfo" %> <%@ page import="java.util.Map" %> <html> <body> <h2>Private Resource</h2> <% SAMLSessionInfo info = (SAMLSessionInfo)request.getSession().getAttribute(SAMLSessionManager.SAML_SESSION_INFO); out.println("User id = " + info.getNameId() + " "); out.println("<TABLE> <TR> <TH> Attribute name </TH> <TH> Attribulte value </TH></TR>"); for (Map.Entry entry : info.getAttributes().entrySet()) { out.println("<TR><TD>" + entry.getKey() + "</TD><TD>" + entry.getValue() + "</TD></TR>"); } out.println("</TABLE>"); %> <a href="<%=request.getContextPath()%>/logout">Logout</a> </body> </html> 

Build the application with the command: mvn clean package.

Checking Java Web Applications

Deploy the application to Tomcat AS and check the operation of SSO:
  1. Describes the application context in the $ {tomcatHome} /conf/Catalina/localhost/sso.xml file
     <Context docBase="$pathToWebApp" privileged="true" antiResourceLocking="false" antiJARLocking="false" unpackWAR="false" swallowOutput="true" /> 


    or simply copy our sso.war application to $ {tomcatHome} / webapps
  2. In order for tomkata applications to establish a connection with shibboleth idP via HTTPS protocol, you need to add a shibboleth idP certificate to the java keystore.
    To do this, use the Java keytool utility:

    keytool -alias idp.local.ru -importcert -file $ {shHome} /idp.crt -keystore $ {keystorePath}
  3. We start Tomcat AS
  4. Open the browser and knock on the closed resource of the application sp.local.ru : 8443 / sso / pages / private / page.jsp
  5. Check that the page is open and the system displayed the id and user name

  6. As an exercise, check that the filter skips requests for pictures in the .jpg format in the / pages / private folder.

Integration with Google Apps.

And now it's time to check that SSO really works for us.
To do this, we will use the application from the Google Apps cloud services ( http://www.google.com/enterprise/apps/business/ ).

  1. Register your domain name and super-administrator using a free trial version. After everything is completed, log in to admin.google.com under the created user (using the fully qualified domain name).
  2. idpuser, Super Administrator.
  3. « »
    «».

  4. -> .
  5. :
    URL * = https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO
    URL * = gmail.com
    URL * = gmail.com

    .
  6. shibboleth idP HTTPS
    $shHome/credentials/idp.crt


    .
  7. https://shibboleth.usc.edu/docs/google-apps/ shibboleth idP Google Apps.

    : , shibboleth idP. , RelyingParty rp:RelyingParty.
  8. logger' edu.internet2.middleware.shibboleth DEBUG
      <!-- Logs IdP, but not OpenSAML, messages --> <logger name="edu.internet2.middleware.shibboleth" level="DEBUG"/> 

    shibboleth idP https://admin.google.com ( , Google Chrome ).
    idpuser@ domain_name , domain_name – . «».
    , google apps idpuser.
    ${shHome}/logs/idp-process.log , shibboleth idP . , RemoteUserLoginHandler
     22:19:49.172 - DEBUG [edu.internet2.middleware.shibboleth.idp.authn.provider.RemoteUserLoginHandler:66] - Redirecting to <a href="https://idp.local.ru:8443/idp/Authn/RemoteUser">https://idp.local.ru:8443/idp/Authn/RemoteUser</a> 


    shibboleth idP . , .
    sp.local.ru :8443/sso/pages/private/page.jsp
    , shibboleth idP idpuser.

    Well that's all. SSO . , - .

    Notes

    1 — Service Provider . Shibboleth , Apache- Application Server'.
    2 — Shibboleth idP 2.4.0
    3 — Java 7 .
    4 — CentOS 6.3 OS. Ubuntu 12.04.
    5 — servlet-api 2.5 ${tomcatHome}/lib/catalina.jar
    6 — ru.eastbanctech.java.web.RemoteUserValve – RemoteUserValve. .
    7 — .
    8 — .

    useful links

    1. https://developers.google.com/google-apps/sso/saml_reference_implementation — SSO Google Apps. SSO Google Docs SAML.
    2. https://shibboleth.usc.edu/docs/google-apps/ — Shibboleth Google docs.
    3. http://stackoverflow.com/questions/7553967/getting-a-value-from-httpservletrequest-getremoteuser-in-tomcat-without-modify — Tomcat Valve
    4. https://wiki.shibboleth.net/confluence/display/SHIB2/Home — Shibboleth

Source: https://habr.com/ru/post/193662/


All Articles