We share our experience in integrating SSO with SAML 2.0

1. Background

Despite the fact that the function of a centralized input (Single Sign On, SSO) exists, has been discussed and applied for a long time, in practice its implementation is often accompanied by overcoming a wide variety of problems. The purpose of this article is 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. In the framework of the project, we were faced with the task of implementing (as well as clustering) the single authentication function for the following systems:

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

The customer put forward the basic requirements for building SSO:
  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.

They decided to use Shibboleth ( http://shibboleth.net/about/index.html ) as an idP as an open source system that implements SAML 1.0 && SAML 2.0 protocols in full.

The difficult moments that we encountered in solving this problem:

  1. Lack of expertise in working with SAML 2.0 protocol and Shibboleth product.
  2. Raw and not yet well-structured Shibboleth documentation from the manufacturer.
  3. The lack of high-quality examples of the implementation of the Service Provider for integrating SSO into your Java Web application.

Overcoming these barriers has become the motivation for the publication of this article. We want to share the acquired knowledge, help developers solve such problems, and also make it easier to get acquainted with the SAML 2.0 protocol.

2. Who is the article for?

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

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

3. The main components of SSO

The diagram below shows the overall functioning of our centralized entrance.



The main components and points highlighted in the diagram:
  1. 2 applications participate in the SSO system:
    a. Java Web App - Normal Java Web Application
    b. Google Apps is an application from Google Cloud Services. We will use it only to verify the operation of SSO.
  2. SP Filter - implementation of the Service Provider, the function of which will be interaction with Shibboleth idP by 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 the SP filter and the Shibboleth idP occurs over the secure HTTPS protocol.

Note: In the Shibboleth diagram, idP and Java Web applications are physically spaced apart from different Tomcat servers. However, you can deploy your environment to a single network node using just one Tomcat instance.

4. Set up the environment for Shibboleth idP

Installation and configuration of shibboleth idP:

1. Download the latest version of idP here shibboleth.net/downloads/identity-provider/latest 2 and unzip it to an arbitrary place $ shDistr .
2. Check that the JAVA_HOME variable is set correctly 3 .
We start $ shDistr / install.sh (we will consider that the UNIX-like operating system is used). 4 The
installer will ask you for the following information to keep in mind:
  • installation path (ex: / opt / shib)
  • server idP name (for example: idp.local.ru).

    Add the idP server to the list of aliases for the localhost in the / etc / hosts file:
    127.0.0.1 localhost idp.local.ru
  • The password for the java key store, which is generated during the installation process (for example: 12345).

Next, we verify that the installation process has completed successfully.

We introduce the following notation:

  • $ shHome - the directory where Shibboleth was installed;
  • $ shHost - idP server name;
  • $ shPassword - password for java key store (JKS).

3. We determine which attributes and from which sources idP will be extracted. In our case, we will pass the user login. Add the attribute description to the $ shHome / conf / attribute-resolver.xml file после элемента .

<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 receipt 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 return this SAML SP attribute to the filter, we describe it in the file $ shHome / conf / attribute-filter.xml .

<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 be passed only to a specific SAML SP.

5. Our Shibboleth idP should know about those 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 file $ shHome / conf / relying-party.xml .
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 the 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. Until now, our shibboleth idP had no idea what a 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 file $ shHome / conf / relying-party.xml define the meta-information for our SP, next to the

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

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

<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:

  • our SAML 2.0 SP has the identifier "sp.local.ru"
  • the address where shibboleth idP will return SAML 2.0 messages Location = " https://sp.local.ru:8443/sso/acs " is specified in the md: AssertionConsumerService element.
  • And finally, the Binding = "urn: oasis: names: tc: SAML: 2.0: bindings: HTTP-POST" parameter indicates that the SP response will be sent from shibboleth idP through a browser redirect.

7. It remains to choose the way with which shibboleth idP will authenticate users. 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 ). Upon receipt of an authentication request, shibboleth idP 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 authenticated through an external system (for example, through a Web Apache server). In order not to complicate this article, we decided to go to the trick and set the REMOTE_USER variable artificially for each request.
This will be done in the next section when configuring Tomcat AS (step 7).

Shibboleth setup is completed, congratulations :)

Installing and configuring Tomcat for shibboleth idP:

  1. Download tomcat 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 at the moment, Tomcat 7. * cannot be used when communication between SP and idP occurs directly via SOAP. And although in the examples of this article we will use direct redirects of the browser to implement these communications, we still recommend using Tomcat version 6.
  2. Copy the $ shDistr / endorsed folder to the $ tomcatHome folder .
  3. We change the
  4. $ tomcatHome / bin / setenv.s h, set the settings for dynamic and permanent JVM memory: JAVA_OPTS = "$ JAVA_OPTS -Xmx512m -XX: MaxPermSize = 128m"
  5. 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 configure access to the tomato via HTTPS.
    To do this, 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 variables $ shHome and $ shPassword with real values .
  6. Deploying the shibboleth idP app on Tomcat. To do this, create the file
    $ tomcatHome / conf / Catalina / localhost / idp.xml with the 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
  7. Compile 5 the following class into the tomcat-valve.jar arbitrary library:

      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);
          }
    
        }
    

    Put the library in the $ {tomcatHome} / lib folder. And in the server.xml file add the line
    внутри элемента
    . After the server starts, when accessing any Tomcat server application, the REMOTE_USER parameter with the idpuser value in the request context will be automatically set.

5. Implementation of SP Filter for SAML 2.0 protocol

To implement this solution, create a SAML 2.0 Service Provider filter, whose tasks will be:
  1. The filter skips requests for public resources for which authentication is not needed.
  2. The filter stores information about the 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 displays the originally requested resource.
  5. The filter deletes the local session when the user is logged out of the Java Web application.
  6. At the same time, the session on shibboleth idP continues to remain active.


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

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

Код
<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>
              <sourсe>${jdk.version}</sourсe>
              <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 SAMLSPFilter class:

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, path to idP metadata, SP name, etc.). The values ​​of these parameters will be set in the web.xml configuration file of the Java Web application. The checkSAMLResponse and samlRequestSender objects are needed to verify the validity of SAML 2.0 messages and send an authentication request. We will return to them a little later.

Код
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;
    }
  }
}

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

STEP 1: Ignore requests that are not intended for the filter
The excludedUrlPattern parameter , which encloses a regular expression. If the requested resource falls into the excludedUrlPattern template , then 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 response came from Shibboleth idP, we process it.
We look in the request for the parameter "SAMLResponse" and if it is found, then we received a response from shibboleth idP for the authentication request. Getting started processing SAML 2.0 messages.

Код
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 , and verify the SAML statements are executable - checkSAMLResponse.verify (..) . If all checks are completed, then create an internal SAML session SAMLSessionManager.getInstance (). CreateSAMLSession (..) and redirect the user to the originally requested response.sendRedirect (..) resource .

In the SAMLUtils class, we will post useful intermediate methods when working with SAML 2.0 messages. One such method will be the decodeSamlMessage method, which decodes messages received through HTTPS SAML 2.0.
Код
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 a helper method for converting SAML objects to 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;
}

Let's create the SAMLResponseVerifier class, into which we put the functionality for checking SAML 2.0 messages received from shibboleth idP. In the main verify (..) method, we implement the following checks:

  • This SAML 2.0 response from idP was preceded by a SAML 2.0 request sent by our filter.
  • The message contains a positive user authentication result via shibboleth idP.
  • The main statements in the SAML 2.0 response are fulfilled (the message has not expired, this message is intended for our SP, etc.).

Код

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 verifies that the SAML 2.0 response was preceded by a request from our filter. For implementation, an object of the SAMLRequestStore class is used, which stores the requests sent to SAML 2.0 to shibboleth idP.
Код
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. His task will be to create / destroy local sessions, which is an object of the following SAMLSessionInfo class.

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

}

Actually, the SAMLSessionManager class itself, which creates and destroys local SAML sessions in the Session context of the server using SAMLContext.
Код
<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 authentication request, shibboleth idP will simply return the active session to us. The implementation of the global logout requires additional settings, and prior to version 2.4.0 shibboleth idP was not supported. More information can be found 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 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: We create 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, encodes, and sends requests in the form of SAML 2.0 messages.

Код
<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();
    }
  }

}

A SAML 2.0 message with instructions for authenticating the user 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>


The AssertionConsumerServiceURL parameter specifies the URL at which shibboleth idP will return the response, and the ProtocolBinding parameter indicates how to return the response to our filter (POST HTTP protocol method)
The ID parameter defines the message identifier that we save when sending the message
String key = SAMLRequestStore.getInstance (). storeRequest ();
and check when parsing messages in a method verifyInResponseTo class SAMLResponseVerifier .

The saml2p: Issuer element defines the name of our SP. Using value saml2p: Issuer shibboleth idP determines from which SP the authentication request was sent, and how it should be processed (via the SP metadata).

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:

Код
<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 SAMLResponseVerifier.verify (..) method.
That's all, our filter is implemented!
The structure of our project looks like this:
We collect the implemented filter in the jar library in the local repository.
To do this, run the command in the directory with pom.xml: mvn clean install

6. Create a Java Web application with SSO support

Create a Java Web Application

For an illustrative 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. We’ll make one of the private resources a page that displays information on the current user of the system.
The structure of our application is as follows:
pom.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/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> 


Тут нужно обратить на секцию properties , где задаются основные параметры нашего фильтра
sp.local.ru — название SAML 2.0 фильтра SP
https:§§sp.local.ru:8443§sso§acs — URL фильтра, по которому он
будет обрабатывать SAML 2.0 сообщения от shibboleth idP
https:§§idp.local.ru:8443§idp§profile§SAML2§Redirect§SSO — URL, по
которому наш фильтр будет отправлять сообщения shibboleth idP
§logout — logout URL

web.xml

In the web.xml file, we determine the parameters of our filter and its scope. Let's make the resources in the ".jpg" format open through the excludedUrlPattern parameter .

Код
<!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 The
page is simply the output of the id and attributes of the authenticated user.

Код
<%@ 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>

We assemble the application with the command: mvn clean package.

Testing Java Web Application Performance

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


    or just copy our sso.war application to $ {tomcatHome} / webapps
  2. In order for tomcat applications to be able to connect to shibboleth idP over HTTPS, you need to add the shibboleth idP certificate to java keystore.
    To do this, use the Java utility keytool:
    keytool -alias idp.local.ru -importcert -file $ {shHome} /idp.crt -keystore $ {keystorePath}
  3. Launch Tomcat AS
  4. We open the browser and knock on the closed resource of the sp.local.ru
  5. application : 8443 / sso / pages / private / page.jsp
  6. Check that the page opens and the system displays the id and username

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

Integration with Google Apps.

Now is the 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. After everything is completed, go to admin.google.com under the created user (using the fully qualified domain name).
  2. Using the administrative panel to create the idpuser user, give him Super Administrator privilege.
  3. Select “Add controls” at the bottom of the screen and click
    on the “Security” item
  4. in the drop-down list
    .

  5. Then select Advanced settings -> Set up single sign-on.
  6. Mark the item Allow single sign-on and set the parameters:
    URL of the input page * = https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO
    URL of the exit page * = gmail.com
    Change password URL * = gmail. com
    Click on the Save Changes button .
  7. Download the certificate for working with shibboleth idP via HTTPS. The
    certificate is located in $ shHome / credentials / idp.crt.
    Click the Save Changes button.
  8. Using the instruction https://shibboleth.usc.edu/docs/google-apps/ to configure shibboleth idP to work with Google Apps.

    Note: specify the schema name for the added elements, otherwise you will get an error when starting shibboleth idP. For example, rp: RelyingParty should be specified instead of RelyingParty.
  9. For a logger named edu.internet2.middleware.shibboleth, set the DEBUG level
        <!-- Logs IdP, but not OpenSAML, messages -->
        <logger name="edu.internet2.middleware.shibboleth" level="DEBUG"/>
    

    We restart shibboleth idP and go to the page https://admin.google.com in a new browser session (you may need to delete cookies, you can use Incognito mode in Google Chrome).
    Enter idpuser @ domain_name , where domain_name is the name of your registered domain and password. Click "Login."
    We accept non-signed certificates and make sure that you are logged into google apps as idpuser.
    In the log $ {shHome} /logs/idp-process.log shibbolet you should see how shibboleth idP handles your request. There you will see that the authentication process is going through 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>
    
    


    In general, the logs in shibboleth idP are quite simple and at the same time informative. We recommend spending a little time to understand them.
    Next, open our application url sp.local.ru : 8443 / sso / pages / private / page.jsp
    and look in the logs that shibboleth idP finds an existing session for the user idpuser.

    That's it. Our simplest SSO system is operational. We hope you find something useful for yourself.

    Notes

    1 - You can also use the Service Provider from the manufacturer. In the case of Shibboleth, this leads to a complication of the application infrastructure, since it is necessary to put an additional Apache server in front of the Application Server.
    2 - At the time of this writing, the latest version of Shibboleth idP 2.4.0
    3 - We used Java 7 in our environment.
    4 - We used CentOS 6.3 as the OS. Also tested on Ubuntu 12.04.
    5 - To compile, you need the servlet-api 2.5 library and $ {tomcatHome} /lib/catalina.jar
    6 - ru.eastbanctech.java.web.RemoteUserValve - the full path to the RemoteUserValve class. In your case, you need to fix it based on the package hierarchy.
    7 - We offer to implement it yourself as an exercise.
    8 - Change the parameters highlighted in red depending on your environment.

    useful links

    1. https://developers.google.com/google-apps/sso/saml_reference_implementation - SSO service for Google Apps. Explains how to integrate SSO into Google Docs using SAML.
    2. https://shibboleth.usc.edu/docs/google-apps/ - Instructions for integrating Shibboleth with Google docs.
    3. http://stackoverflow.com/questions/7553967/getting-a-value-from-httpservletrequest-getremoteuser-in-tomcat-without-modify - How to implement your Tomcat Valve
    4. https://wiki.shibboleth.net/confluence/display/SHIB2/Home - Shibboleth documentation