Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Sharing Experience of SSO Integration via SAML 2.0 Resources

4.62/5 (5 votes)
24 Mar 2014CPOL16 min read 294.9K   396  
Sharing Experience of SSO Integration via SAML 2.0 Resources

1. Background

Despite the fact that Single Sign On (SSO) exists, is discussed and has been used for a long time, practice shows that it is not always easy to implement. The purpose of this article is to show how to implement a custom Service Provider¬ (SP) for SAML 2.0 identity provider (IdP) and use it to integrate SAML SSO into your Java Web application.

In one of our recent projects, we had to deploy a clustered portal solution for a major university. One of the tasks was to implement SSO for the following systems:

  1. Liferay portal
  2. Simple custom Java web application
  3. Google apps

Additional Requirements and Considerations Included

  1. Shibboleth was already chosen and used as an IdP. Shibboleth is an open source system that fully implements SAML 1.0 and SAML 2.0 protocols (http://shibboleth.net/about/index.html).
  2. SAML 2.0 was selected as the main protocol for SSO integration.
  3. Jasig CAS was already configured as Shibboleth authentication provider.

In addition to more flexible authentication user experience and flow configuration, such a setup also enabled CAS-compatible systems to participate in SSO without a need to implement.

LDAP was configured as an authentication provider for CAS and as an attributed provider for Shibboleth.

Challenges That We Faced Included

  1. The Shibboleth documentation from the manufacturer was not always current and complete.
  2. There are few comprehensive examples of SAML SP implementations for Java web application.
  3. Using Shibboleth integrated with CAS to enable both SAML and CAS SSO complicates SSO session management and requires special attention.

After resolving these difficulties, we couldn’t help but share our experience with the readers to make it easier for other developers to understand and use SAML 2.0.

2. Target Audience

  1. Developers who integrate SSO feature in their projects using SAML 2.0.
  2. Java developers who are looking for a practical example of SSO functionality integration using SAML 2.0 for their application.
  3. Java developers, who want to try Shibboleth as a SAML Identity Provider (IdP).

To better understand this article, basic knowledge of SAML 2.0 specification is recommended.

3. SSO Main Components

The diagram below shows generic operation of SSO in the system:

Image 1

The diagram shows:

  1. 2 applications to participate in SSO:
    1. Java Web App – A java servlet-based web application deployed to Apache Tomcat
    2. Google Apps – Google cloud services
  2. SP Filter — Service Provider implementation. It interacts with SAML Shibboleth IdP via SAML 2.0 protocol
  3. Shibboleth IdP is the application for authentication and authorization tools via SAML 1.0 and SAML 2.0
  4. Tomcat AS is Java Application Server
  5. The interaction between the SP filter and Shibboleth IdP occur via secure HTTPS protocol

Please, note that the chart shows Shibboleth IdP and Java Web-application distributed to different Tomcat servers. However, you can deploy the environment on a single node using only one instance of Tomcat.

4. Setting Up the Environment for Shibboleth idP

Installing and Configuring Shibboleth idP:

  1. Download the latest IdP version from http://shibboleth.net/downloads/identity-provider/latest/ and unzip any directory; we will use $shDistr to refer to the location
  2. Check if the variable JAVA_HOME is set up correctly .

    Run $shDistr/install.sh (we assume that one of Linux flavors is used).

    The installer will ask for the following information:

    • installation path (for example: /opt/shib)
    • IdP name server (for example: idp.local.com).

      To test multi-domain SSO configuration on your local machine, you can use a simple trick with /etc/hosts – just add IdP server domain name in the list of aliases to localhost in the file/etc/hosts: 127.0.0.1 localhost idp.local.com

    • password for java key store, which is generated during the installation (for example: 12345).

      Next, check that the installation process is successfully completed.

      We will use the following placeholders hereafter:

      • $shHome — the directory where Shibboleth was installed
      • $shHost — idP server name
      • $shPassword — the password for Java Key Store (JKS)
  3. Now, as a part of IdP configuration, we need to specify “an attribute provider” – a component within IdP responsible for retrieving user attributes. In our case, we will only need the IdP to share user login.

    We add a description of the attribute to the file $shHome/conf/attribute-resolver.xml after the element.

    XML
    <resolver:AttributeDefinition id="transientId" xsi:type="ad:TransientId">.
    
    <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: You can use this file to specify any additional attributes the IdP may potentially share with Service Providers; different data sources may be used, such as LDAP or DBMS via JDBC. Attribute provider configuration is described in more detail here.

  4. We will also need to modify a file $shHome/conf/attribute-filter.xml to make sure that the IdP can share this attribute with our SP:
    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 specify more complex and precise rule. For example, you can specify that the attribute be transferred only to a certain SAML SP.

  5. Our Shibboleth IdP should know about the nodes that it can interact with – 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.

    Let’s open the file and add the following element:

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

    Note that SAML specification requires all parties such as IdP and SP to be identified by a unique “entity id”. Often domain names or URL of corresponding components are used as entity IDs, although it is not required. We are using entity id=”sp.local.com” for the SP and entity id=" https://idp.local.com/idp/shibboleth " for the IdP.

    We will test the whole configuration locally, so we will also need to add the SP domain name into the list of aliases for localhost in /etc/hosts: 127.0.0.1 localhost idp.local.com sp.local.com

    ProfileConfiguration” element states that the relying party will need SAML 2.0 SSO services from this IdP, and that it will not sign or encrypt messages and responses.

  6. At this step, we will specify how the IdP will retrieve detailed SP information. For this, we will add the SP metadata provider in the file $shHome/conf/relying-party.xml next to the IdP metadata provider element:
    XML
    <metadata:MetadataProvider id="IdPMD" xsi:type="metadata:FilesystemMetadataProvider" .. >
    
    <metadata:MetadataProvider id="spMD" xsi:type="metadata:FilesystemMetadataProvider"
                                metadataFile="/opt/shib/metadata/saml-sp-metadata.xml"/>

    Thus, we give Shibboleth IdP instructions to look up the definition of the SP in the file /opt/shib/metadata/saml-sp-metadata.xml. Let’s create this file with the following content:

    XML
    <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, we need to understand the following:

    • Our SAML 2.0 SP has an identification «sp.local.ru»
    • The address Location=" https://sp.local.ru:8443/sso/acs” where shibboleth IdP will be returning SAML 2.0 messages is specified in the element md:AssertionConsumerService.
    • Finally, the parameter Binding=”urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST” shows that the SP response will be sent from shibboleth IdP via browser redirect.
  7. What’s left now is to choose how shibboleth IdP will conduct a real user authentication – or define “authentication provider” in Shibboleth terms. In a production environment, there can be various configurations, including authentication via LDAP, DBMS, and even CAS. We will be using Remote User Authentication mechanism that’s already included (https://wiki.shibboleth.net/confluence/display/SHIB2/IdPAuthRemoteUser).

    During the authentication request, shibboleth IdP will look for the REMOTE_USER variable. If such variable is found, shibboleth IdP will assume that the user has already been authenticated through an external system (for example, through Web Apache server).

    For our example, we are going to set the variable REMOTE_USER for each request by means of simple Tomcat configuration in order not to overcomplicate the article.

    Congratulations! The Shibboleth set-up is complete.

Downloading and Configuring Tomcat for Shibboleth IdP

  1. First, download Tomcat 6 from http://tomcat.apache.org/download-60.cgi, unzip to any folder $tomcatHome (for instance, in opt/shib-tomcat folder).

    It is important to mention that at the moment Tomcat 7.* cannot be used when the communication between SP and idP is happening directly via SOAP. Although in this article’s examples, we will be using direct browser redirects to perform these communications, we still recommend using Tomcat version 6.

  2. Copy $shDistr/endorsed folder to $tomcatHome folder.
  3. Change the file $tomcatHome/bin/setenv.sh to specify minimal JVM memory parameters:
    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 the folder $tomcatHome/lib for SOAP protocol support in the communication between the SP and IdP.
  5. Now let’s open the $tomcatHome/conf/server.xml and set up access to Tomcat via HTTPS. To do this, let’s define the following Connector element:
    XML
    <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" />

    Do not forget to replace the variables $shHome and $shPassword with their real values.

  6. It’s time to deploy Shibboleth IdP application in Tomcat. To do this, let’s create a file $tomcatHome/conf/Catalina/localhost/idp.xml with the following content:
    XML
    <Context docBase="$shHome/war/idp.war"
    privileged="true"
    antiResourceLocking="false"
    antiJARLocking="false"
    unpackWAR="false"
    swallowOutput="true" />

    Do not forget to replace the variable $shHome with the correct value.

  7. Compile the following class and jar put it in a jar file tomcat-valve.jar:
    Java
    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);
        }
      }
    

    Place the jar file in folder ${tomcatHome}/lib. Add the following line of code into the file server.xml:

    XML
    <Valve ?lassName="com.eastbanctech.java.web.RemoteUserValve" />

    inside the element <Host name=«localhost» appBase=«webapps» ...>

    This valve is only needed to make sure that Tomcat sets REMOTE_USER variable for each request thus modelling authentication process for the IdP.

5. Implementation of SP Filter for SAML 2.0 Protocol

Now, we will implement a SAML 2.0 Service Provider servlet filter responsible for the following tasks:

  1. The filter passes through public resources requests that do not need authentication.
  2. The filter caches authenticated user information to reduce the number of requests to Shibboleth IdP.
  3. The filter creates a SAML 2.0 authentication request message (Authn) and passes it to Shibboleth IdP using browser redirect.
  4. The filter processes a response message from Shibboleth IdP, and if user authentication is successful, the system displays initially requested resource.
  5. The filter removes local session when a user logs out from Java web application.

At the same time, the Shibboleth idP session remains active.

From a technical standpoint, the filter will be the implementation of a standard interface javax.filter.Filter. The scope of the filter will be set at a particular web-application.

Now when the filter functionality is clear, let's start the implementation:

  1. Create a maven project structure.

    This may be done using maven archetype plugin:

    mvn archetype:generate -DgroupId=ru.eastbanctech.java.web 
    -DartifactId=saml-sp-filter -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

    You may specify parameters groupId and artefactId to your liking.

    The structure of our project in Intellij Idea will look like this:

    Image 2

  2. Main pom.xml:
    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>
            <!-- General settings -->
            <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>
                  <source>${jdk.version}</source>
                  <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. Class SAMLSPFilter is at the heart of our filter:
    Java
    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;
       /*
         Step 1: Ignore requests that are not addressed to the filter
         Step 2: If the answer comes from Shibboleth idP, we handle it
         Step 3: If you receive a request for logout, delete the local session
         Step 4: If the user has already been authenticated, then we can grant access to a resource
         Step 5: Create a SAML запрос на аутентификацию and send the user to Shibboleth idP
      */ 
    }
    }

    FilterConfig specifies basic filter parameters (the excluded resources, the IdP name, the path to IdP Metadata, SP name, etc.). Values of these variables are specified in configuration file web.xml of Java web-application.

    Objects checkSAMLResponse and samlRequestSender are necessary for checking the validity of the SAML 2.0 messages and for sending an authentication request. We will get back to them later.

    Java
    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 class initializes libraries to work with SAML 2.0 messages:
    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;
        }
      }
    }

    XML file selection specified in the xmlToolingConfigs array contains instructions on how to process elements of SAML 2.0 messages. The XML files themselves are located in the library opensaml-*.jar.

Step 1: Ignore requests that are not addressed to the filter.

The parameter excludedUrlPattern is a regular expression. If the requested resource URL matches the excludedUrlPattern, the filter ignores the request:

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

// We add method to the filter class that will check if the request needs to be handled
private boolean isFilteredRequest(HttpServletRequest request) {
  return !(filterConfig.getExcludedUrlPattern() != null &&
            getCorrectURL(request).matches(filterConfig.getExcludedUrlPattern()));
   }
   // Also add the auxiliary method for receiving the correct 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 comes from Shibboleth IdP, we handle it.

We are looking for "SAMLResponse" in the HTTP request and if it is found, it means that we have received a response from Shibboleth IdP for authentication request. Then we get to processing of SAML 2.0 messages.

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

We need to decode SAML message using SAMLUtils.decodeSamlMessage(..) method, and verify included SAML assertions – checkSAMLResponse.verify(..). When everything is verified, we create an internal SAML session SAMLSessionManager.getInstance().createSAMLSession(..), and redirect the user to the originally requested resource response.sendRedirect (..).

In SAMLUtils class, we include useful auxiliary methods for SAML 2.0 message processing. One of them is decodeSamlMessage that decodes SAML 2.0 messages received via HTTPS.

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

Let’s place an additional method of converting SAML objects into String in the same class. This will be helpful for logging SAML messages.

Java
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 class SAMLResponseVerifier that will contain message verification functionality for SAML 2.0 messages received from Shibboleth IdP. In the main method, verify (..) we implement the following verifications:

  • IdP SAML 2.0 message is a response to a certain SAML 2.0 request sent by our filter.
  • The message contains a positive user authentication by Shibboleth IdP.
  • The main assertions in the SAML 2.0 response are met (the message is not expired, the message is intended for our SP, etc.).
Java
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
}
}

The method verifyInResponseTo verifies that the SAML 2.0 response was preceded by a request from our filter. For implementation, an object from class SAMLRequestStore is used, which stores SAML 2.0 requests sent to the Shibboleth IdP.

Java
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 own class SAMLSessionManager. Its purpose is to create/destroy local sessions represented by objects of SAMLSessionInfo class.

Java
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
}

The class SAMLSessionManager creates and destroys local SAML sessions in the servlet Session context using SAMLContext.

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 you receive a request for logout, delete the local session.

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

Note: The session remains active on Shibboleth IdP and when prompted for authentication, Shibboleth IdP simply will return us to an active session. Implementation of a global logout requires additional configurations that were not supported by Shibboleth IdP before version 2.4.0. You can read more about it here.

Step 4: If the user has already been authenticated, then we can grant access to a resource.

If the user has an active SAML session in our filter, we give the user this resource:

Java
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.

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

SAMLRequestSender class creates, encodes (potentially encrypting and/or signing), and sends requests as SAML 2.0 messages.

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

}

SAML 2.0 message with user authentication instructions is built using buildRequest method and is actually an XML object:

XML
<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 parameter AssertionConsumerServiceURL specifies the URL for Shibboleth IdP to return a response, and the parameter parametrProtocolBinding indicates how to return the response to our filter (HTTP POST in this case).

The parameter ID specifies the message identifier. We save it during sending messages String key = SAMLRequestStore.getInstance().storeRequest(); and verify when analyzing the message using method verifyInResponseTo of SAMLResponseVerifier class.

The element saml2p:Issuer specifies the name/entityId of our SP. Using the value saml2p:Issuer, Shibboleth IdP determines from which SP request for authentication is sent, and how it should be processed (via Metadata SP).

We will receive a SAML 2.0 message in XML format from the IdP in response to it:

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 SAMLResponseVerifier.verify(..) method.

That's about it - our filter is implemented!

Our project structure looks like this:

Image 3

Let’s build the filter as a jar library in the local repository.

To do this, we run the command in the directory with c.pom.xml: mvn clean install.

6. Creating Java Web Application with SSO Support

Creating Java Web application

To illustrate this, we will create a simple Java web-application with private and public resources. The access to private resources requires user authentication via Shibboleth IdP web application. One of the private resources will be a page that displays information about the current user of the system.

The structure of our application project looks like this:

Image 4

pom.xml

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>

<!-- Determine the value for our application -->
<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>
   <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>

Now it is important to pay attention to session properties, where the basic parameters are set for our filter.

XML
<sp.id>sp.local.ru</sp.id> - the name/entityId of SAML 2.0 filter SP
<acs.url>https://sp.local.ru:8443/sso/acs</acs.url> - URL of the filter to process SAML 2.0 messages from Shibboleth IdP
<idp.sso.url>https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO</idp.sso.url> - URL for our filter to send Shibboleth IdP messages
<logout.url>/logout</logout.url> - logout URL

web.xml

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

XML
<!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 simply shows the user id and attributes of the authenticated user.

XML
<%@ 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() + "<BR/>");
  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>

Let’s build the application using command:mvn clean package.

Testing the Java Web application performance

Let’s deploy the application to Tomcat AS and test the SSO performance:

  1. We describe the application context in a file ${tomcatHome}/conf/Catalina/localhost/sso.xml:
    XML
    <Context docBase="$pathToWebApp" privileged="true" antiResourceLocking="false"
            antiJARLocking="false"    unpackWAR="false" swallowOutput="true" />

    or simply copying our sso.war application in ${tomcatHome}/webapps.

  2. For Tomcat application to connect to Shibboleth IdP via HTTPS protocol, it is necessary to add a Shibboleth IdP certificate in java trusted keystore.

    We use Java keytool utility for that:

    keytool -alias idp.local.ru -importcert -file ${shHome}/idp.crt -keystore ${keystorePath}
  3. Launch Tomcat AS.
  4. Open the browser window and open a secured application resource https://sp.local.ru:8443/sso/pages/private/page.jsp
  5. Make sure that the page has opened and we can see the user id and user name:

    Image 5

  6. As an exercise, make sure that the filter allows requests for pictures in .jpg format in the folder /pages/private.
  7. Integration with Google Apps

It is time to make sure that our SSO really works. To do that, let’s use Google Apps as another service provider (http://www.google.com/enterprise/apps/business/).

  1. Register your domain name and a Super-administrator using a free trial version. Once everything is complete, log in to http://admin.google.com/ using those credentials (fully qualified domain name).
  2. Create idpuser user and assign the Super Administrator rights to him using administrative panel.
  3. On the bottom of the screen select "Add Controls" in the drop down menu, select “security”.

    Image 6

  4. Then select Advanced Settings -> Set-up SSO.
  5. Mark “Enable SSO” and set the following parameters:
    Entry Page URL * = https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO
    Exit Page URL * =  gmail.com
    Change Password URL * =  gmail.com

    Click on Save Changes button.

  6. Download a certificate to work with Shibboleth IdP using HTTPS. The certificate is located in $shHome/credentials/idp.crt

    Image 7

    Click Save Changes button.

  7. Using the instructions here, configure Shibboleth IdP to work with Google Apps.

    Note: Specify the schema name for the added elements, otherwise you will get an error at Shibboleth idP launch. For example, instead of RelyingParty, you need to put rp: RelyingParty.

  8. For the logger with the name edu.internet2.middleware.shibboleth, set the DEBUG level:
    XML
    <!-- Logs IdP, but not OpenSAML, messages -->
    <logger name="edu.internet2.middleware.shibboleth" level="DEBUG"/>
    
  9. Restart Shibboleth IdP and go to the page https://admin.google.com in a new browser session (you may need to delete cookies or use Incognito mode in Google Chrome).
  10. Type in idpuser@domain_name, where domain_name is the name of your registered domain and password. Press "Enter".
  11. Accept un-Signed certificates and make sure that you are logged in google apps as idpuser.

    In the Shibboleth log ${shHome}/logs/idp-process.log you should see how Shibboleth IdP is processing your request. There, you will see that the process of authentication is going via RemoteUserLoginHandler.

    22:19:49.172 - DEBUG [edu.internet2.middleware.shibboleth.idp.authn.provider.RemoteUserLoginHandler:66] - Redirecting to https://idp.local.ru:8443/idp/Authn/RemoteUser

    In general, all logs in Shibboleth IdP are quite simple and at the same time informative. We recommend spending some time to understand them.

  12. Then let’s open our app in URL https://sp.local.ru:8443/sso/pages/private/page.jsp and see in the logs that Shibboleth IdP is finding an available session for the user idpuser.

Well, that's all. Our simple system of SSO is functioning. We hope you have found this article useful.

Useful Links

  1. https://developers.google.com/google-apps/sso/saml_reference_implementation - SSO service for Google Apps. It explains how to integrate SSO with Google Docs using SAML.
  2. https://shibboleth.usc.edu/docs/google-apps/ - Instructions on Shibboleth and Google docs integration.
  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 - Documentation of Shibboleth

¬Alternatively, you can often use generic Service Provider integration modules provided by IdP. In case of Shibboleth, it means that an additional Apache server needs to be installed with mod_shib module configured in front of the Application Server.

  • At the time of writing this article, Shibboleth IdP 2.4.0 is the latest version.
  • We used Java 7 in our environment.
  • We used CentOS 6.3 as our OS. Also it was tested on Ubuntu 12.04. servlet-api 2.5 and ${tomcatHome}/lib/catalina.jar are required to compile.
  • We suggest to implement it on your own as an exercise.
  • Change the setting in red depending on your environment.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)