How to add SAML and OpenID authentication to your Seam application

Version 2

    Introduction

     

    When securing your Seam based application, you need to choose where to store the identity information that is needed for user authentication. In earlier days, you would create your own username/password store or connect to the corporate directory service. You would write custom code for managing the passwords of your users. And you would annoy your users with another password to remember, and another login action to perform before getting to the actual application.

     

    Happily, this time is over, and you can connect with trusted external parties that securely manage the identity of your users. Those external parties could be IDaaS (Identity as a Service) providers (e.g. SSOCircle, myOpenID and Janrain), identity services of your partners (suppliers, customers or affiliated organizations) or companies running popular websites with huge numbers of accounts (such as Google, Yahoo or MySpace).

     

    There are two major technologies that make it possible to securely connect with those external identity providers in an interoperable and standardized way: SAML and OpenID. When you are running a Seam application, you are lucky: the Seam module of PicketLink makes it possible to connect with multiple SAML and OpenID providers at the same time. In fact you create your own hybrid circle of trust by configuring the PicketLink Seam module. You will get the best of both technologies.

     

    This article will guide you through the steps that are needed to connect your JBoss Seam application to external identity services.

     

    Step 1: Add Maven dependency

     

    I assume that your application is built using Maven, and that you use a JBoss application server running Java 1.6. In that case, you need to add the following dependency:

     

    <dependency>

      <groupId>org.picketlink</groupId>

        <artifactId>picketlink-seam</artifactId>

        <version>1.0.2</version>

        <exclusions>

          <exclusion>

          <groupId>sun-jaxb</groupId>

          <artifactId>jaxb-api</artifactId>

        </exclusion>

        <exclusion>

          <groupId>sun-jaxb</groupId>

          <artifactId>jaxb-impl</artifactId>

        </exclusion>

        <exclusion>

          <groupId>stax</groupId>

          <artifactId>stax-api</artifactId>

        </exclusion>

        <exclusion>

          <groupId>apache-log4j</groupId>

          <artifactId>log4j</artifactId>

        </exclusion>

      </exclusions>

    </dependency>

     

    This article is based on PicketLink version 1.0.2. The steps explained here are not applicable to previous versions of PicketLink. The exclusion of jaxb and stax is only needed if you run with Java 1.6, which provides its own version of these packages. The log4j artifact needs to be excluded because log4j is contained in JBoss AS.

     

    Step 2: Create a Java Key Store (JKS)

     

    You need to create a Java Key Store with the private key that is used for signing outgoing SAML messages. You can create the keystore and generate the private/public key pair with the keytool executable that is included in the JDK. If you don't use SAML or if your SAML identity providers don't require signed messages, you can just use a dummy keystore, for example the keystore that is provided in the example PicketLink Seam application.

     

    Step 3: Import metadata for SAML Identity Providers

     

    Create a file saml-entities.xml which will end up in the classpath of your application. The file contains an EntitiesDescriptor element with metadata for all identity providers. For each trusted identity provider, you retrieve the metadata (in a way specific for that provider) and add an EntityDescriptor element. The following example contains two identity providers:

     

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>

    <EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="urn:oasis:names:tc:SAML:2.0:metadata

            http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd">

      <EntityDescriptor entityID="http://localhost:8888/opensso">

          ...

      </EntityDescriptor>

      <EntityDescriptor entityID="http://idp.ssocircle.com"

          ...

      </EntityDescriptor>

    </EntitiesDescriptor>

     

    The PicketLink Seam module will use the information in the metadata to find out the locations and bindings of the external services, to retrieve the public keys for validating signatures on incoming messages, to find out whether outgoing messages need to be signed, etcetera.

     

    Step 4: Configure the PicketLink Seam module

     

    You need to provide the PicketLink Seam module with an XML file external-authentication-config.xml, somewhere on the application classpath. The schema file describing the configuration options is contained in the picketlink-seam jar file:

     

    schema/config/external-authentication-config.xsd

     

    Here is an example of a configuration file, with two SAML IDPs configured and OpenID configured:

     

    <?xml version="1.0" encoding="UTF-8"?>

    <ExternalAuthenticationConfig

        xmlns="urn:picketlink:identity-federation:seam:config:1.0"

        xmlns:tns="urn:picketlink:identity-federation:seam:config:1.0">

      <ServiceProvider

          protocol="http"

          hostname="localhost"

          unsolicitedAuthenticationUrl="PublicPage.seam"

          loggedOutUrl="http://localhost:8080/seam-sp/PublicPage.seam"

          failedAuthenticationUrl="http://localhost:8080/seam-sp/FailedAuthenticationPage.seam"

          internalAuthenticationMethod="#{authenticator.internalAuthenticate}">

        <SamlConfig

            serviceProviderEntityId="http://localhost:8080/seam-sp"

            keyStoreUrl="classpath:/picketlink_test_keystore.jks"

            keyStorePass="store456"

            signingKeyAlias="servercert"

            signingKeyPass="pass456">

          <SamlIdentityProvider entityId="http://idp.ssocircle.com" />

          <SamlIdentityProvider entityId="http://localhost:8888/opensso" />

        </SamlConfig>

        <OpenIdConfig>

          <Attribute

              Alias="name"

              TypeUri="http://schema.openid.net/namePerson"

              Required="true" />

          <Attribute

              Alias="email"

              TypeUri="http://schema.openid.net/contact/email"

              Required="true" />

        </OpenIdConfig>

      </ServiceProvider>

    </ExternalAuthenticationConfig>

     

    For a complete description of options and default, refer to the schema file. Here is a short description of the most important aspects:

     

    • The configuration can contain one or multiple service providers. Most of the times, you would use just one service provider. You'll need more if you host multiple domains in your application (virtual hosting situation). For example, if you offer a SaaS service to your customers, and you use different host names to distinguish the areas of the different customers, you would create a separate service provider for each customer.
    • For a service provider, you configure a hostname, and optionally a protocol and a port. These settings determine the location where the external authentication filter will run. This filter is responsible for exchanging messages with the identity providers. Therefore, it is wise to choose the https protocol (which is the default). You should ensure that your web application listens to requests for the hostname, protocol and port.
    • For each service provider you set the URLs where users must be redirected to after an unsolicited login (initiated by the identity provider), a logout or a failed authentication.
    • You can configure OpenID only (no OpenIdConfig element), SAML only (no SamlConfig element), or both.
    • In the SAML configuration, you list all trusted identity providers with their entity IDs. Be sure that each entity ID corresponds with the entity ID of an EntityDescriptor element in the saml-entities.xml file.
    • If you want to work with one single identity provider, you specify either the defaultOpenIdProvider of the OpenIdConfig element, or the defaultIdentityProvider element of the SamlConfig element. In that case you won't need a login page (step 5) because the PicketLink Seam module doesn't need to ask the user which identity provider she wants to use.
    • The SAML configuration contains two attributes that control message signing: authnRequestsSigned and wantAssertionsSigned.
      • The authnRequestsSigned attribute determines whether outgoing authentication messages will be signed. If so, the private key of the keystore will be used. This attribute is false by default. If one of your identity providers has WantAuthnRequestsSigned set in its meta data, don't forget to set authnRequestsSigned to true.
      • The wantAssertionsSigned attribute is true by default. Don't set this attribute to false unless you are sure that checking the integrity of the assertions is not needed in your environment.

     

    Step 5: Create a login page

     

    When someone tries to access a secured page of your application (marked with login-required in the pages.xml) , the request will be intercepted and the user will be redirected to the login page (the login-view-id in the pages element of pages.xml). This standard Seam behaviour is somewhat different in the case you specified a default identity provider in the external authentication. In that case, the login page is not shown, and the browser is directed to the default identity provider's website.

     

    Apart from the automatic redirection after user access to a private page, a user could also explicitly press a login button and be redirected to the login page. When using external authentication configuration, the login page is in fact an identity provider discovery page: the user needs to choose which external identity she wants to use for logging in. The login page could look like this:

     

    login-page.tiff

     

    The page looks like this:

     

    <h2>Please choose your identity provider</h2>

      <h:form>

        <h:commandLink

            action="#{externalAuthenticator.openIdSignOn('https://www.google.com/accounts/o8/id')}">

          <h:graphicImage .../>

        </h:commandLink>

        <h:commandLink

            action="#{externalAuthenticator.openIdSignOn('https://me.yahoo.com')}">

        <h:graphicImage .../>

        </h:commandLink>

        <h:commandLink

            action="#{externalAuthenticator.openIdSignOn('myspace.com')}">

          <h:graphicImage .../>

        </h:commandLink>

        <h:commandLink

            action="#{externalAuthenticator.openIdSignOn('https://myopenid.com')}">

          <h:graphicImage .../>

        </h:commandLink>

        <h:commandLink

            action="#{externalAuthenticator.samlSignOn('http://idp.ssocircle.com')}">

          <h:graphicImage .../>

        </h:commandLink>

        <mytaglib:showOpenIdPopup>

          <h:graphicImage .../>

        </mytaglib:showOpenIdPopup>

    </h:form>

     

    For starting the external sign on process, we use the page-scoped externalAuthenticator component. The component lives in a PicketLink namespace. You should add an import for this namespace to components.xml if you use the unqualified component name in your pages:

     

    <import>org.picketlink.identity.seam.federation</import>

     

    The component contains the following methods:

     

    public void samlSignOn(String idpEntityId)

    public void openIdSignOn()

    public void openIdSignOn(String openId)

    public void singleLogout()

     

    public String getReturnUrl()

    public void setReturnUrl(String returnUrl)

    public String getOpenId()

    public void setOpenId(String openId)

     

    The returnUrl property must have been set before calling one of the signOn methods. It contains the URL where the browser is redirected after a succesful sign on. You need to add a page parameter to the login page in the pages.xml file:

     

    <page view-id="/Login.xhtml" scheme="https">

      <param name="returnUrl" value="#{externalAuthenticator.returnUrl}" />

    </page>

     

    When the Seam module automatically redirects a user to the login page, it will pass the requested URL to the login page as a request parameter named returnUrl. If you redirect to the login page directly when the user presses a login button, you should set the returnUrl parameter yourself. However, it is easier to let the login button navigate to a secured page.

     

    The openId property is only needed if you let the user type in her personal OpenID URL, instead of letting here choose an OpenID provider. In the example page, she would click on OpenID, and a popup will be shown with an input box bound to #{externalAuthentication.openId}). In that case, we would call the parameterless openIdSignOn() to start the external sign on process.

     

    Step 6: Create an internal authentication method

     

    If the identity provider returns a succesful authentication result, you probably will need to do some extra checks to find out whether the user is allowed to log in. This process is called internal authentication in terms of the PicketLink Seam module. When configuring the service provider, you bind the internal authentication to a method in your application. This is achieved by assigning an EL method expression to the internalAuthenticationMethod attribute. The method should have this signature:

     

    public Boolean internalAuthenticate(Principal principal, List<String> roles)

     

    In the method you should do two things:

     

    1. Find out whether the externally authenticated user (the principal) is allowed to login. Return true only if she may login.
    2. If the user may log in, add her roles to the roles list.

     

    You would typically check whether the external identity (OpenID identifier or SAML nameID) is linked to an internal user account in your database. In that case, you would give the user the roles that belong to that account, or rely on the attributes of the external identity to determine the roles.

     

    The principal contains a lot of information about the externally verified identity of the user. It is either an instance of SamlPrincipal or OpenIdPrincipal. Both contain the id of the user, the identity provider, and attributes. The SAML principal also contains the verified assertion (as a JAXB object).

     

    Step 7: Secure your pages

     

    This step is just what you would do for any other non-external authentication method: as soon as the identity of your user has been established, the user can only access the pages and components for which it has permission. You can use the regular Seam constructs for specifying the permission, for example

     

    • <s:hasRole>, <s:hasPermission> in EL expressions on the xhtml pages
    • <restrict> elements in  pages.xml

     

    They will use the roles that are assigned with the internal authentication method created in step 6. You can also use more custom methods that read SAML or OpenID specific properties of the principal. You can find the principal by calling the getPrincipal() method on the identity component. It will evaluate to a SamlPrincipal or OpenIdPrincipal object.

     

    Step 8: Add single logout button

     

    If the current user is logged in using SAML, it is possible to log her out not only locally, but also at her identity provider and at all other websites where she logged in with the same identity provider. You do this by calling the singleLogout() method of the externalAuthentication component. After the single logout has taken place, the user will be redirected to the URL that you specified with the loggedOutUrl attribute in the service provider configuration.

     

    Step 9: Export SAML SP metadata

     

    For each SAML identity provider, you need to add the SP metadata in a way that is specific to the identity provider. Your identity provider will probably support the upload of SP metadata. This metadata is served by your running application at the following URL:

     

    <protocol>://<hostname>:<port>/<context-path>/MetaDataService.seam

     

    Remark that if you configured multiple service providers, they all have their own metadata, served at their own hostnames.