13 Replies Latest reply on Dec 19, 2007 4:34 PM by sweetlandj

    Security: Better support for single sign on?!

    stephen.friedrich

      I need to integrate my seam app with an existing custom made SSO solution.
      After some trial and error I came up with this solution that is basically working, even with login redirection configured (using redirect.captureCurrentView/returnToCapturedView exactly like documented in the seam reference).

      @Name("authenticator")
      @Scope(ScopeType.CONVERSATION)
      public class Authenticator {
       @In
       private Identity identity;
      
       private UserData userData;
      
       // This method is configured in pages.xml as an action called for all pages:
       // <page view-id="/*" login-required="true" action="#{authenticator.checkLogin}"/>
       public void checkLogin() {
       // if already logged on, simply continue
       if (identity.isLoggedIn()) {
       return;
       }
      
       // try SSO auto login
       HttpServletRequest request = FacesUtil.getServletRequest();
       userData = new SsoAuthenticator().validateSsoToken(request); // results in a web service call
       if(userData != null) {
       identity.login(); // Don't know another, more direct way to login, so store
       // userData in field and check it in the authenticate method
       }
       }
      
       // This method is configured in components.xml to as the identity's authenticate-method:
       // <security:identity authenticate-method="#{authenticator.authenticate}"/>
       public boolean authenticate() {
       if (userData != null) {
       // previously a sso token has been validated - log in automatically
       userData = null;
       return true;
       }
      
       String userName = Identity.instance().getUsername();
       String password = Identity.instance().getPassword();
       userData = new SsoAuthenticator().login(userName, password); // results in a web service call
      
       if (userData == null) {
       FacesMessages.instance().add("Invalid username/password");
       return false;
       }
      
       return true;
       }
      }
      


      Small problem: After a successful sso auto login the next page displays
      Warning

      1. Please log in first
      2. Welcome, Stephen

      How can I prevent these messages or clear them afterwards?

      Open issue:
      If the user hit the login page directly (as opposed to being redirected when trying to access another page) I'd like to redirect after the login to different pages depending on the user's roles.
      Any suggestions?

      Proposal: Here's a way to make integration into an SSO solution easier:
      Add an attribute to identity that lets me specify a method that is used to try auto-login:
       <security:identity authenticate-method="#{authenticator.authenticate}" auto-login-method="#{authenticator.tryAutoLogin}"/>
      


        • 1. Re: Security: Better support for single sign on?!
          stephen.friedrich

          Forgot to add the most important comment:
          Thanks a million for implementing Seam's security features!
          Just looking at pure JAAS gives me the creeps...

          • 2. Re: Security: Better support for single sign on?!
            stephen.friedrich

            (I hate replying to myself, but somehow the last couple of lines in my first post got clipped.)

            Like the authenticate-method the auto-login-method on successful login would set roles at identity and return true.
            That would spare me from configuring a page action for all pages. It would prevent the spurious "Please login. Welcome." messages. Also I would neither need to check for "already logged in" condition nor would I need to pass user data in a field from the checkLogin method to the authenticate method like I did above.

            What do you think?

            • 3. Re: Security: Better support for single sign on?!
              shane.bryzak

              If you don't want the auto-generated messages then use identity.authenticate() instead of identity.login(), although you'll need to handle any exceptions yourself if you do this. Identity also has an overloaded isLoggedIn(boolean) method which if passed true will attempt to do perform a silent login if the user credentials are set. Perhaps you can use this method to perform an SSO login without displaying your login page, probably by specifying a navigation rule in your pages.xml.

              • 4. Re: Security: Better support for single sign on?!
                stephen.friedrich

                Thanks Shane!
                Using identity.authenticate() got rid of the "Welcome, Stephen" message.
                To get rid of the "Please log in first" message I did this:

                @Scope(ScopeType.APPLICATION)
                @Intercept(NEVER)
                @Name("org.jboss.seam.core.pages")
                @Install(precedence=Install.APPLICATION)
                public class SsoPages extends Pages {
                
                 /**
                 * Overridden to prevent "Please log in first" faces message
                 */
                 protected void notLoggedIn() {
                 Events.instance().raiseEvent("org.jboss.seam.notLoggedIn");
                 }
                }


                The "auto-login-method" should be quite easy to implement in Seam, don't you think? It would be much more straightforward than this current solution.
                Can I create a Jira issue for this feature? Will you accept a patch if I supply it?

                • 5. Re: Security: Better support for single sign on?!

                  Hello,

                  I want to chime in with a friendly reminder regarding this task http://jira.jboss.org/jira/browse/JBSEAM-967 (which if you search the forum, shane, you'll find that you offered to add this if I posted a JIRA issue). Anyway, by the time the Seam web-app gets to process the request, the user is already authenticated and authorized by the SSO system + Tomcat Realm. What I really need is a way to have my authenticator's authenticate method invoked without redirecting to a login page. Rather, it would be nice if (maybe) there was a special event that you could assign to call the authenticate method in components.xml.

                  I am using the CAS SSO system http://www.ja-sig.org/products/cas/ for SSO authentication - I used one of the CAS client code packages and wrote a Tomcat Realm + Valve implementation. If we use servlet security directly, this works great - we'ld like to use Seam security.

                  Any help / discussion about this is appreciated.

                  Thanks,
                  Brad Smith

                  • 6. Re: Security: Better support for single sign on?!
                    shane.bryzak

                    You could probably define a navigation rule in pages.xml for your login page that uses isLoggedIn(true) to attempt an authentication and if successful redirect to another page without displaying the login page. You may need to extend Identity (or RuleBasedIdentity) and override the isCredentialsSet() method depending on how your SSO solution stores the user credentials.

                    Unfortunately, JBSEAM-967 has a low priority at the moment.

                    • 7. Re: Security: Better support for single sign on?!
                      stephen.friedrich

                      Brad, can't you use a similar checkLogin() method like in my code above?
                      It call identity.login(); which will call your authenticate method.
                      In that method I use
                      redirect.setViewId(nextPage);
                      to redirect to a different page depending on the users role.
                      (With "@Redirect redirect;" in my Authenticator.)
                      Not the most beautiful construct, but it gets the job done without showing a login page.

                      Or did I misunderstand your problem?

                      • 8. Re: Security: Better support for single sign on?!

                         

                        "stephen.friedrich" wrote:
                        Brad, can't you use a similar checkLogin() method like in my code above?
                        It call identity.login(); which will call your authenticate method.
                        In that method I use
                        redirect.setViewId(nextPage);
                        to redirect to a different page depending on the users role.
                        (With "@Redirect redirect;" in my Authenticator.)
                        Not the most beautiful construct, but it gets the job done without showing a login page.

                        Or did I misunderstand your problem?


                        Stephen,

                        I will try your approach.

                        One quick question - why do you @In the Identity component and use Identity.instance()? Can you not use the injected Identity in all places in your solution?

                        Thanks,
                        Brad

                        • 9. Re: Security: Better support for single sign on?!
                          stephen.friedrich

                          Brad, thanks for spotting that.
                          No particular reason other than I have been through quite some trial and error until I arrived at this code. I'll switch completely to Identity.instance().

                          • 10. Re: Security: Better support for single sign on?!

                             

                            "stephen.friedrich" wrote:


                            Proposal: Here's a way to make integration into an SSO solution easier:
                            Add an attribute to identity that lets me specify a method that is used to try auto-login:
                             <security:identity authenticate-method="#{authenticator.authenticate}" auto-login-method="#{authenticator.tryAutoLogin}"/>
                            


                            Like the authenticate-method the auto-login-method on successful login would set roles at identity and return true.
                            That would spare me from configuring a page action for all pages. It would prevent the spurious "Please login. Welcome." messages. Also I would not need to pass user data in a field from the checkLogin method to the authentiocate method like I did above.

                            What do you think?


                            I like this suggestion most. It's consistent with the JIRA issue I've mentioned earlier in the thread. I wish they would add support for this - I still don't have a working solution / even using your approach posted earlier - sigh....

                            • 11. Re: Security: Better support for single sign on?!

                               

                              "shane.bryzak@jboss.com" wrote:
                              You could probably define a navigation rule in pages.xml for your login page that uses isLoggedIn(true) to attempt an authentication and if successful redirect to another page without displaying the login page. You may need to extend Identity (or RuleBasedIdentity) and override the isCredentialsSet() method depending on how your SSO solution stores the user credentials.

                              Unfortunately, JBSEAM-967 has a low priority at the moment.


                              What exactly should this look like? I've tried:

                               <page view-id="/index.xhtml" action="#{identity.isLoggedIn(true)}"/>
                              


                              and

                               <page view-id="/*" action="#{identity.isLoggedIn(true)}"/>
                              


                              And watching the server log, there is no evidence that this action method is ever invoked.

                              • 12. Re: Security: Better support for single sign on?!
                                stephen.friedrich

                                Sorry, should have included that already. For me this works fine:

                                <pages login-view-id="/login.xhtml">
                                
                                 <page view-id="/*" action="#{authenticator.checkLogin}"/>
                                
                                

                                The checkLogin method is the very one I already posted.

                                • 13. Re: Security: Better support for single sign on?!
                                  sweetlandj

                                  I have a slightly different solution that avoids the need to add action parameters to pages. Basically I extended Identity and overrode the isLoggedIn method. Here is some code that will implement a trivial and unsecure SSO across many co-located applications simply by passing the username and password around in a session cookie:

                                  package foo.seam;
                                  
                                  import java.util.Map;
                                  import javax.faces.context.ExternalContext;
                                  import javax.faces.context.FacesContext;
                                  import javax.servlet.http.Cookie;
                                  import javax.servlet.http.HttpServletResponse;
                                  import org.jboss.seam.ScopeType;
                                  import org.jboss.seam.annotations.Install;
                                  import org.jboss.seam.annotations.Name;
                                  import org.jboss.seam.annotations.Scope;
                                  import org.jboss.seam.annotations.Startup;
                                  import org.jboss.seam.annotations.intercept.BypassInterceptors;
                                  import org.jboss.seam.security.Identity;
                                  
                                  @Name("org.jboss.seam.security.identity")
                                  @Scope(ScopeType.SESSION)
                                  @Install(precedence = Install.APPLICATION)
                                  @BypassInterceptors
                                  @Startup
                                  public class SSOIdentity extends Identity {
                                   @Override
                                   public boolean isLoggedIn(boolean attemptLogin) {
                                   FacesContext facesCtx = FacesContext.getCurrentInstance();
                                   ExternalContext extCtx = facesCtx.getExternalContext();
                                   Map<String, Object> cookies = extCtx.getRequestCookieMap();
                                   Cookie username = (Cookie)cookies.get("sso.username");
                                   Cookie password = (Cookie)cookies.get("sso.password");
                                   if((username != null) && (password != null)) {
                                   setUsername(username.getValue());
                                   setPassword(password.getValue());
                                   }
                                   return super.isLoggedIn(attemptLogin);
                                   }
                                  
                                   @Override
                                   protected void postAuthenticate() {
                                   FacesContext facesCtx = FacesContext.getCurrentInstance();
                                   ExternalContext extCtx = facesCtx.getExternalContext();
                                   HttpServletResponse response = (HttpServletResponse)extCtx.getResponse();
                                  
                                   Cookie username = new Cookie("sso.username", getUsername());
                                   username.setMaxAge(-1);
                                   username.setPath("/");
                                   response.addCookie(username);
                                  
                                   Cookie password = new Cookie("sso.password", getPassword());
                                   password.setMaxAge(-1);
                                   password.setPath("/");
                                   response.addCookie(password);
                                  
                                   super.postAuthenticate();
                                   }
                                  
                                   @Override
                                   public void logout() {
                                   super.logout();
                                  
                                   FacesContext facesCtx = FacesContext.getCurrentInstance();
                                   ExternalContext extCtx = facesCtx.getExternalContext();
                                   Map<String, Object> cookies = extCtx.getRequestCookieMap();
                                   HttpServletResponse response = (HttpServletResponse)extCtx.getResponse();
                                  
                                   Cookie username = (Cookie)cookies.get("sso.username");
                                   username.setMaxAge(0);
                                   username.setValue(null);
                                   username.setPath("/");
                                   response.addCookie(username);
                                  
                                   Cookie password = (Cookie)cookies.get("sso.password");
                                   password.setMaxAge(0);
                                   password.setValue(null);
                                   username.setPath("/");
                                   response.addCookie(password);
                                   }
                                  }
                                  
                                  


                                  Just drop this class in the EJB module of each app you're working with (or web module if you're using the J2EE packaging strategy with POJOs). It should work with a customer authenticate method, but I haven't tried it (I'm using the LDAP JAAS authenticate module).