Sunday, December 2, 2007

Windows SSO with JBoss Seam

Unified authentication has been one of the frequently asked functionality for quite some time now. Users have to remember username/password for various systems or having to login for each system while all those systems are part of the same intranet and this doesn't make life/work easier.
The goal of this article is to show how it is possible to have a very simple solution to use Single Sign-On for WIntel based intranets to get access (and authenticate) to web sites without having to fill-in their user name and password (by allowing browser to use the windows' user login silently).
Please, also note, that this is merely a demo and in no way a "bullet proof" or production-ready example.

This example uses NTLM implemented by JCIFS that implements the CIFS/SMB networking protocol in 100% Java. CIFS is the standard file sharing protocol on the Microsoft Windows platform. JCIFS already has an implementation of a filter that does the authentication against windows domain contoller. This article simply shows how to make this solution work closely with JBoss Seam built-in security functionality.

For the following example I used JBoss 4.2.2 with default profile, Seam 2.0 GA, JCIFS 1.2.17. And a simple application generated by seam-gen.

  1. Of course you need to get JCIFS library, you can put the jar in /lib directory of your application and modify the 'ear' target in your build script to include the library in EAR during packaging.

  2. Next step would be configuring JCIFS http authentication filter to authenticate incoming requests against the domain controller. This is no different from the way it is done by JCIFS authentification filter:


    <filter>
    <filter-name>NtlmHttpFilter</filter-name>
    <filter
    -class>jcifs.http.NtlmHttpFilter</filter-class>
    <init-param>
    <param-name>jcifs.http.domainController</param-name>
    <param-value>your domain controller ip or name</param-value>
    </init-param>
    <init-param>
    <param-name>jcifs.smb.client.domain</param-name>
    <param-value>your domain</param-value>
    </init-param>
    <init-param>
    <param-name>jcifs.smb.lmCompatibility</param-name>
    <param-value>3</param-value>
    </init-param>
    <init-param>
    <param-name>jcifs.util.loglevel</param-name>
    <param-value>2</param-value>
    </init-param>
    </filter>
    <filter-mapping>
    <filter-name>NtlmHttpFilter</filter-name>
    <url-pattern>
    /*</url-pattern>
    </filter-mapping>



  3. Add autoLogin() method to the generated Authenticator that does the authentication without the need to fill in user name and password by getting the authentication information from the browser. Please note, we are setting a fake password as in this case we don't even know it, but by having a password set we actually make Seam recongize the identity as logged-in:


    /**
    * Performs the windows authentication.
    * @return true if auto login was successful
    */
    public boolean autoLogin() {

    // trying auto-login
    Object autoLogin = sessionContext.get("NtlmHttpAuth");
    boolean isAuthenticated = false;

    if ( autoLogin != null && (autoLogin instanceof NtlmPasswordAuthentication) ) {
    NtlmPasswordAuthentication ntlm = (NtlmPasswordAuthentication) autoLogin;
    String username = ntlm.getUsername();
    identity.setUsername( username );
    identity.setPassword("jibberish"); // trusting NTLM - not setting real password and even better if we don't
    isAuthenticated = true; // user is authenticated successfully via NTLM
    identity.addRole("admin");
    }

    return isAuthenticated;
    }

    I have also modified the generated authenticate() method so it can re-use the auto-login functionality. Note, it does not have any particular purpose in this concrete example but shows how you can re-use SSO if you login manually:

    @In Context sessionContext;

    public boolean authenticate()
    {
    boolean isAuthenticated = autoLogin();
    log.info("authenticating #0", identity.getUsername());
    if ( ! isAuthenticated ) {
    //write your authentication logic here,
    //return true if the authentication was
    //successful, false otherwise
    }
    // if we are here then the user is authenticated against NTLM or login dialog
    return true;
    }



    Furthermore, for manual form-based authentication (this is where authenticator.authenticate() is used) you can use "arbitrary user credentials from an application" as specified in the JCIFS FAQ.

  4. Now we need to make Seam to actually call our autoLogin() method when authentication is needed so that our application would have the correct identity in the session scope. This is done by making our authenticator.autoLogin() method to listen to "org.jboss.seam.notLoggedIn" event. Since the generated application already has this event registered we simply add our call to the event registration:

    <event type="org.jboss.seam.notLoggedIn">
    <action execute="#{authenticator.autoLogin}"/>
    <action execute="#{redirect.captureCurrentView}"/>
    </event>

  5. We can also protect all our pages by forcing the login in pages.xml:

    <page view-id="*" login-required="true">
    <navigation>
    <rule if-outcome="home">
    <redirect view-id="/home.xhtml"/>
    </rule>
    </navigation>
    </page>


This was the final step! To get the whole picture this is what is happening:
By forcing the login-required we are forcing the authentication in the application. Since for the first time authentication fails we we have "org.jboss.seam.notLoggedIn" event fired which will call our #{authenticator.autoLogin}. In turn autoLogin does the windows authentication and sets the user name and fake password on Identity.
Because notLoggedIn event is fired before the authentication for the page we requested fails, initializing the injected identity with the user name and password marks the identity as already logged and thus let's us reach the page without the visiting the login page.

p.s. If you authenticating against a workstation make sure you have simple file sharing switched off! This setting can be found at (Win XP) Explorer->Tools->Folder Options->View->Use simple file sharing - uncheck this checkbox!