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!

35 comments:

kaolle said...

Thanks for a greate article!
I've used it with SEAM 1.2,
I just have one problem, when the autologin is fired by the NotLoggedIn event the identity does never get marked as LoggedIn. Maybe I need to upgrade to SEAM 2.0, any idea ?

By now I use this solution as a semi auto login where username and password are filled in by the autologin() but the user still has to fire the identity.login() in order to get the identity LoggedIn marked. It works quite nice too :-)
/Kaolle

Siarhei Dudzin said...

Well this example is created for Seam 2.0. Setting user name and password did the trick of marking the identity as logged in.

Luis Medalhas said...

Hi this article was a great help, and worked just fine when i tested on my Windows XP workstation, which authenticated in domain.
But when i put the application on a RedHat Linux Server the application doesnt recognize my username.
As i don't really understand the NTLM protocol, do you have any tip what might be the problem?

Sends this message to the log:
sourceId=null[severity=(WARN 1), summary=(Please log in first), detail=(Please log in first)]

Siarhei Dudzin said...

There can be a number of reasons. I haven't really tried it on *nix systems (only on MS Server 2003).

You may want to check if your server can authenticate against the windows domain at all.
There are number of troubleshooting tips on JCIFs site:

http://jcifs.samba.org/FAQ.html

and

http://jcifs.samba.org/src/docs/ntlmhttpauth.html

koenhandekyn said...

i've used a very similar approach to enable openId for my seam application.

however, if i see your case, we should maybe push for a generalization of the seam security framework to support authentication that is not purely username/password based

Siarhei Dudzin said...

I completely agree with you.

PVC Windows said...

This was really helpful

Anonymous said...

hi! nice job, thanks!
can you tell me something about this:

1) autologin should try to login with
the windows-user and pass at the domain controller, as it does,

but

2) if it fails with the current "user-pass"-pair, the user should have a possibility to type the another "user-pass"-pair in the text fields and submit it

then the ntlm should do the same thing (authenticate) once more - with the second (from the text fields) "user-pass"-pair on the same domain controller...

Have you any idea, how can I do that?
(The Problem is, one user can just have two or more network-accounts... accounts are changing... I can not take account of all possible accounts from the same person)

Siarhei Dudzin said...

Don't you get a username/password dialog in case it fails?

Anonymous said...

Is it possible that parts of this are not working anymore with Seam 2.0.1.GA?
I implemented the whole thing and it works up to the part where it is supposed to return to the originally captured view. No matter what I do I end up at the login page. Funny enough the user will already be logged in but end up at the login page with the message " Please log in first" and not the one he originally wanted to see.
The user has to click the login button and then he will be redirected to the page he wanted to see. Am I doing something wrong?

Anonymous said...

Is it possible that parts of this are not working anymore with Seam 2.0.1.GA?
I implemented the whole thing and it works up to the part where it is supposed to return to the originally captured view. No matter what I do I end up at the login page. Funny enough the user will already be logged in but end up at the login page with the message " Please log in first" and not the page he originally wanted to see.
The user has to click the login button and then he will be redirected to the page he wanted to see. Am I doing something wrong?

Siarhei Dudzin said...

It is quite possible. Seam security goes through heavy refactorings to introduce a better SSO support.

If you notice in the code and in one of my comments, setting (even fake) password makes Seam think the user is logged in.

I will see if I can come up with an example for Seam 2.0.1.

Anonymous said...

Thank you so much.
It would be great if you could help out with a working example an Seam 2.0.1 because I am stuck right now. Everything works but the final redirect to the captured view. :(

Siarhei Dudzin said...

Well, for Seam 2.0.1 there is good and bad news.
The good news is that it seems to be enough to add the following at login.page.xhtml:
<action execute="#{identity.login}"/>

This will ensure that you wont be redirected to the login page.

The bad news is that you would still see the "Please log in first" along with the welcome message (there are workarounds for that).
This is happening because Pages.notLoggedIn() is now called rather early which makes it difficult to overide the condition on which notLoggedIn() is happening.

I am really not sure what was the meaning of those exact changes in Seam...

Hope, this helps.

Anonymous said...

Well thanks for the effort. As long as they don't end up at the login page it is acceptable that they see the messages. Won't hurt too much.
I wonder why they changed something that was more flexible to something that that makes customizations like this harder then before. Custom SSO implementations are quite a common thing I would imagine.

Thanks again for pointing into the right direction!

Thorsten said...

I did find a way to make the redirect work again without the regular Seam messages and without calling the Seam event hooks multiple times.

Put this little method into your authenticator class:

public void ssoRedirect() throws Exception {
if(identity.isLoggedIn() == true) {
FacesMessages.instance().clear(); //clear the regular Seam messages
// FacesMessages.instance().add(new FacesMessage("SSO login successful"));
Redirect.instance().returnToCapturedView(); //return to the captured view
}
}

And now put

<action execute="#{authenticator.ssoRedirect}"/>

into your login.page.xhtml.

Hope this helps.

Siarhei Dudzin said...

I tried to do the same but it did not work because messages were added in a different thread thus not giving guarantee that removal of the messages will happen after they are added to the context.

The reason why this works for you might be that you don't put the logic in the authenticate() (which is triggered on an event) but upon entering the login page (page action), which seems to be a good workaround.

Good job!

p.s. I am waiting for the next release of Seam and will have a look whether a new/updated article is needed.

Anonymous said...

(sorry, I was on holiday)
"Don't you get a username/password dialog in case it fails?"
Yes, but...

from 2)... a possibility to type the another "user-pass"-pair in the text fields and submit it

and further:

then the ntlm should do the same thing (authenticate) once more - with the _second_ (from the text fields) "user-pass"-pair _on the same domain controller_...

- And I have no Idea, how can I put "name-pass" from the text field into the current SessionContext instead of Windows login values,
so that
Object autoLogin = sessionContext.get("NtlmHttpAuth");
... String username = ntlm.getUsername();
have the new values
and then
if ( autoLogin != null && (autoLogin instanceof NtlmPasswordAuthentication) ) pass through.

Siarhei Dudzin said...

You might want to use only kerberos authentication and use another (not windows) sso solution. I have an impression that it will be easier for you.

Anonymous said...

Can you tell me some more detailed, how can I do that?

Eric said...

I am having problem setting up the session replication for JCIFS on JBoss Clustering.

Here is my environment:

Web Server: 1 X Apache 2.2.8 (mod_jk 1.2.26 for load balancing) on SUN Sparc T2000 Solaris 10
Application Server: 2 X JBoss 4.2.2 GA (Clustering) on SUN Sparc T2000 Solaris 10
JCIFS: 1.2.18

The error I have is that JCIFS.UniAddress is not Serializable and not able to replicate the session for it.

I have read some of articles on-line and it all mentioned about not able to get the load-balance/clustering to work properly on JCIFS.

Is there any way to make it work?
Thank you very much.

Siarhei Dudzin said...

@Anonymous:

Check http://jcifs.samba.org/src/jcifs-krb5-1.2.13.zip they have working examples.

As for SSO you could use JBoss SSO...

Siarhei Dudzin said...

Eric, one way to solve it is to release a patch and submit it to JCIFs. Another way is to use kerberos for authentication and another SSO solution that is more java/internet friendly.

Eric said...

Hi, Siarhei:
Thank you for your response.
Our agency is on Windows Active Directory and JCIFS seems to be a suitable solution without requiring user to type in login information.

Anonymous said...

Hi,

We implemented this and it actually works great. One question though. Sometimes our domain controller goes down. How would you do to specify a secondary domain controller?

Siarhei Dudzin said...

This is really a JCIFS question...

linuxtuxie said...

I am running Jboss 4.2.2.GA and Seam 2.0.2.SP1

When I reach the autoLogin() function it skips the
" if ( autoLogin != null && ( autoLogin instanceOf NtlmPasswordAuthentication) )" part

In debug mode I can see that the autoLogin object has data in it and it seems to be an instance of NtlmPasswordAuthentication.

When I remove the instanceOf check, I receive the following error:


java.lang.ClassCastException: jcifs.smb.NtlmPasswordAuthentication cannot be cast to jcifs.smb.NtlmPasswordAuthentication

Now I am boggled ?!?

Can someone throw some hints?

linuxtuxie said...

Ok, I have figured out my problem. The jcifs.jar was included in both my ear & war file...and that's why I got the strange message.

I only included the jcifs.jar file in my ear... and now everything works as advertised :)

Anonymous said...

I am having a problem when integrating jcifs with new release of Seam 2.1.0.GA

on debug i find that sessionContext.get("NtlmHttpAuth") is returning null :(
plz let me know if there is a problem or i am missing somethig

Siarhei Dudzin said...

You probably missing something. Check if the filter is running...

Anonymous said...

thanks for reply!
Please let me know how can i check that filter is running ?
actually it is failing on
final NtlmPasswordAuthentication auth = (NtlmPasswordAuthentication) sessionContext.get("NtlmHttpAuth");

sessionContext does not have any varibale NtlmHttpAuth

but when i test with the old seam version the it has the NtlmHttpAuth parameter in SessionContext

Thanks in advance!

Anonymous said...

Hello!

Thank you for the great article!

Currently we are building a Seam application, which needs to implement Windows SSO and I have tried with the method you are proposing here. After some tweaking, it worked, but a very unexpected bug appeared.

For the web front-end of out application we are extensively using richfaces components. After deploying the Ntlm filter, the rich:fileUpload component does not function anymore. I've tried to troubleshoot the problem and it appears that, although the POST carries the whole content of the file (PDF in our case), no uploadEvent ever gets fired, since the upload listener method never gets called. When I turn off the filter (by removing its entry from web.xml and also the autoLogin method from components.xml), everything works fine.

The only relevant information, which I have found on the internet, is here: http://social.msdn.microsoft.com/forums/en-US/iewebdevelopment/thread/0a842834-6957-46fb-a765-0a736bd3030d
But in the current JAAS .jar library there is no JaasLoginFilter class.

Any information on how I could better troubleshoot the problem or any workaround are very welcome!

Thank you in advance!

Ivan Stefanov

pr0n_j3r3my said...

Worked great the first time. Thanks for the code.

pr0n_j3r3my said...

Worked great the first time. Thanks for the code.

Ravinandan said...

Thanks for the article.
I used seam 2.0 along with richfaces.
After making it work and then accessing the application from some other system(other credentials) I found that richfaces stylesheet/css was not being applied.
After couple of hrs of checking in seam community forum and google I found that login-required = true in pages.xml was causing the issue.
Removal of which I was able to get richfaces loaded.