10 Replies Latest reply on Aug 20, 2008 4:55 PM by Ragav Gomatam

    JavaServer Faces and container-managed authorization not wor

    Bob Muller Novice

      I'm working with JavaServer Faces and JBoss and trying to set up JAAS and container-managed authorization using roles.

      My problem is that, after I successfully authenticate using a login form and create a login context with a subject through a custom login module (all of which works fine), the Faces navigation tries to take me to the myHome page and fails, redisplaying the login page. The myHome.xhtml page is in the directory specified in the <security-constraint> element in web.xml and requires the community_user role, which I've verified is being correctly put into the Roles group in the subject. It's definitely seeing the constraint, which has CONFIDENTIAL to require SSL, and the login page is coming back with the myHome.faces url using the SSL server address and port. All my Faces navigation rules use the option.

      The only anomaly with respect to the many examples I've seen is in the realm name. This might be a red herring, not sure. The jboss-web.xml file contains the following:

      <jboss-web>
       <security-domain>java:/jaas/tairweb</security-domain>
       <context-root>/</context-root>
       <virtual-host>bob</virtual-host>
       <resource-ref>
       <res-ref-name>jdbc/ReadOnly</res-ref-name>
       <jndi-name>java:/jdbc/ReadOnlyTairTestJTDS</jndi-name>
       </resource-ref>
       <resource-ref>
       <res-ref-name>jdbc/WebWriter</res-ref-name>
       <jndi-name>java:/jdbc/WebWriterTairTestJTDS</jndi-name>
       </resource-ref>
      </jboss-web>
      


      The login-config.xml file in server/default/conf contains this:
       <application-policy name="java:/jaas/tairweb">
       <authentication>
       <!-- Add this line to your login-config.xml to include the ClientLoginModule propogation,
       which propagates the login context to the security "interceptor" subsystem in JBoss;
       without this, JBoss won't have the Subject with which to compare roles. -->
       <login-module code="org.jboss.security.ClientLoginModule" flag="required" >
       <module-option name="password-stacking">useFirstPass</module-option>
       <module-option name="restore-login-identity">true</module-option>
       </login-module>
       <login-module code="org.tair.community.login.TairJbossLoginModule" flag="required">
       <module-option name="debug">true</module-option>
       <module-option name="user_query">
       SELECT c.community_id, p.is_tair_curator, p.is_external_curator,
       p.first_name, p.person_id, c.status
       FROM Community c JOIN
       Person p ON c.community_id = p.community_id
       WHERE c.is_obsolete = 'F' AND
       c.user_name = ? AND
       c.password = ?
       </module-option>
       </login-module>
       </authentication>
       </application-policy>
      


      and, finally, the web.xml file contains this:
       <login-config>
       <auth-method>FORM</auth-method>
       <realm-name>tairweb</realm-name>
       <form-login-config>
       <form-login-page>/community/login/login.faces</form-login-page>
       <form-error-page>/community/login/login-failed.faces</form-error-page>
       </form-login-config>
       </login-config>
      


      The anomaly is in the name. In all the examples I've seen, the login-config.xml file application-policy is just the name without the JNDI prefix, which is present only in the security-domain in jboss-web.xml. If I do that, JBoss tries for the "other" login module and doesn't find the user.properties file and gives an error. As written above, I get the login form correctly. I don't know if this is a change in JBoss behavior with 4.2.2 or what, or whether this is a symptom of something wrong.

      Here's the security constraint from web.xml:
       <security-role>
       <description>A community user</description>
       <role-name>community_user</role-name>
       </security-role>
       <security-constraint>
       <display-name>Community</display-name>
       <web-resource-collection>
       <web-resource-name>CommunityPages</web-resource-name>
       <description>All Faces pages available only to community users</description>
       <url-pattern>/community/community_user/*</url-pattern>
       </web-resource-collection>
       <auth-constraint>
       <role-name>community_user</role-name>
       </auth-constraint>
       <user-data-constraint>
       <transport-guarantee>CONFIDENTIAL</transport-guarantee>
       </user-data-constraint>
       </security-constraint>
      


      Here's the Faces navigation code from faces-config.xml:
       <navigation-rule>
       <from-view-id>/community/login/login.xhtml</from-view-id>
       <navigation-case>
       <from-outcome>success</from-outcome>
       <to-view-id>/community/community_user/myHome.xhtml</to-view-id>
       <redirect />
       </navigation-case>
       <navigation-case>
       <from-outcome>failure</from-outcome>
       <to-view-id>/community/login/login.xhtml</to-view-id>
       <redirect/>
       </navigation-case>
       <navigation-case>
       <from-outcome>request_info</from-outcome>
       <to-view-id>/community/registration/requestInfo.xhtml</to-view-id>
       <redirect />
       </navigation-case>
       <navigation-case>
       <from-outcome>register</from-outcome>
       <to-view-id>/community/registration/register.xhtml</to-view-id>
       <redirect />
       </navigation-case>
       </navigation-rule>
      


      Finally, in case it's important, here's the code for the login module:
      public class TairJbossLoginModule extends AbstractServerLoginModule implements
       LoginModule {
       /** TAIR user object */
       private User user = null;
       /** debug setting based on the configuration file stanza debug setting */
       private boolean debug = false;
       /** The query to use to retrieve the user by username and password */
       private String query;
      
       // error messages
      
       private static final String NULL_SUBJECT = "Null subject in Login Module";
       private static final String NULL_HANDLER =
       "Null callback handler in Login Module";
       private static final String NULL_OPTIONS = "Null options map in Login Module";
       private static final String NULL_QUERY =
       "No login query supplied in conf/login-config.xml";
       private static final String NOT_YET_ACTIVE =
       "Account is not yet activated by TAIR";
       private static final String INVALID_USER =
       "Invalid username or password, please reenter the correct values and submit again";
       private static final String FAILURE = "Could not log in user";
       private static final String ERROR_PREFIX = "Login Error";
       private static final String WARN_PREFIX = "Login Notification";
      
       /*
       * (non-Javadoc)
       *
       * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject,
       * javax.security.auth.callback.CallbackHandler, java.util.Map,
       * java.util.Map)
       */
       @SuppressWarnings("unchecked")
       public void initialize(Subject subject, CallbackHandler handler,
       Map sharedState, Map options) {
       // Check the inputs for nulls.
       if (subject == null) {
       throw new IllegalArgumentException(NULL_SUBJECT);
       }
       if (handler == null) {
       throw new IllegalArgumentException(NULL_HANDLER);
       }
       if (options == null) {
       throw new IllegalArgumentException(NULL_OPTIONS);
       }
      
       // Call the superclass initializer to set everything.
       super.initialize(subject, handler, sharedState, options);
      
       // Extract debug and query custom fields.
       String debugValue = (String)this.options.get("debug");
       if (debugValue != null) {
       debug = "true".equalsIgnoreCase(debugValue);
       }
      
       query = (String)this.options.get("user_query");
       if (query == null) {
       throw new IllegalArgumentException(NULL_QUERY);
       }
      
       if (debug) {
       Debugger.println("Initialized login module");
       }
       }
      
       /*
       * (non-Javadoc)
       *
       * @see javax.security.auth.spi.LoginModule#login()
       */
       @SuppressWarnings("unchecked")
       @Override
       public boolean login() throws LoginException {
       String name = null;
       StringBuilder pw = null;
      
       // initialize loginOk to false in superclass
       loginOk = false;
      
       // Set up the callbacks.
       Callback[] callbacks = new Callback[2];
       NameCallback nameCallback = new NameCallback("Name");
       PasswordCallback pwCallback = new PasswordCallback("Password", false);
       callbacks[0] = nameCallback;
       callbacks[1] = pwCallback;
      
       // Get the username and password from the callback handler.
       try {
       callbackHandler.handle(callbacks);
       name = nameCallback.getName();
       char[] tempPw = pwCallback.getPassword();
       if (tempPw == null) {
       // Use an empty array rather than a null.
       tempPw = new char[0];
       }
       // Copy the password array into a local StringBuilder and clear the
       // original.
       pw = new StringBuilder(new String(tempPw));
       pwCallback.clearPassword();
      
       // Look up the user in the database using the configured query.
       user = QueryUser.getUser(name, pw.toString(), query);
      
       // Clear the password from memory as a security measure.
       pw.delete(0, pw.length());
      
       if (user != null) {
       // Validate the user.
       loginOk = validateUser(user.getStatus());
       if (debug && loginOk) {
       Debugger.println("Logged in user " + name + " (" + user.getPersonId()
       + ")");
       } else if (debug && !loginOk) {
       Debugger.println("Failed to log in user " + name);
       }
       }
       } catch (IOException e) {
       throw new LoginException(e.getMessage() + ": "
       + e.getCause().getMessage());
       } catch (UnsupportedCallbackException e) {
       throw new LoginException(e.getMessage());
       } catch (CommunityException e) {
       throw new LoginException(e.getMessage());
       } finally {
       if (!loginOk) {
       // If validation failed, null out the user and throw login exception.
       user = null;
       throw new LoginException(FAILURE);
       }
       }
      
       return loginOk;
       }
      
       /**
       * Validate the user given the user's status. If the status is null, the user
       * is not valid. If the status is ACTIVE, the user is valid. If the status is
       * NEW, the user is not valid but the method queues a special message
       * reporting the status.
       *
       * @param status the current status of the user, if any
       * @return true if the user is active and valid, false otherwise
       */
       private boolean validateUser(String status) {
       boolean validated = false;
       // The user must be an active user to be valid.
       if (status != null && status.equals(DataConstants.getActiveStatus())) {
       validated = true;
       } else if (status != null && status.equals(DataConstants.getNewStatus())) {
       FacesContext context = FacesContext.getCurrentInstance();
       // Queue global user-not-yet-active message, no component id
       String msg = WARN_PREFIX + ": " + NOT_YET_ACTIVE;
       FacesMessage fMsg = new FacesMessage(FacesMessage.SEVERITY_WARN, msg, "");
       context.addMessage(null, fMsg);
       } else {
       FacesContext context = FacesContext.getCurrentInstance();
       // Queue global invalid-user message, no component id
       String msg = ERROR_PREFIX + ": " + INVALID_USER;
       FacesMessage fMsg =
       new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, "");
       context.addMessage(null, fMsg);
       }
       return validated;
       }
      
       /**
       * Get the user login information.
       *
       * @return a User object containing the login information for the user
       */
       public User getUser() {
       return user;
       }
      
       /*
       * (non-Javadoc)
       *
       * @see org.jboss.security.auth.spi.AbstractServerLoginModule#getIdentity()
       */
       @Override
       protected Principal getIdentity() {
       if (debug) {
       Debugger.println("Getting identity for user " + user.getUsername() + " ("
       + user.getPersonId() + ")");
       }
       return new SimplePrincipal(user.getUsername());
       }
      
       /*
       * (non-Javadoc)
       *
       * @see org.jboss.security.auth.spi.AbstractServerLoginModule#getRoleSets()
       */
       @Override
       protected Group[] getRoleSets() throws LoginException {
       if (debug) {
       Debugger.println("Getting groups for user " + user.getUsername() + " ("
       + user.getPersonId() + ")");
       }
       Group[] groups = { new SimpleGroup("Roles"), new SimpleGroup("CallerPrincipal") };
      
       // All users get the Community User role.
       groups[0].addMember(new SimplePrincipal(DataConstants.COMMUNITY_USER));
       if (debug) {
       Debugger.println(user.getUsername() + " gets Tair Community User role ");
       }
      
       // Tair and External Curator roles are defined in the database.
       if (user.isTairCurator()) {
       groups[0].addMember(new SimplePrincipal(DataConstants.TAIR_CURATOR));
       if (debug) {
       Debugger.println(user.getUsername() + " gets Tair Curator role ");
       }
       }
       if (user.isExternalCurator()) {
       groups[0].addMember(new SimplePrincipal(DataConstants.EXTERNAL_CURATOR));
       if (debug) {
       Debugger.println(user.getUsername() + " gets External Curator role");
       }
       }
      
       // Set the CallerPrincipal group Principal; JBoss uses this to respond to
       // the HTTPServletRequest.getUserPrincipal() method.
       groups[1].addMember(new SimplePrincipal(user.getUsername()));
       if (debug) {
       Debugger.println(user.getUsername() + " set as CallerPrincipal");
       }
      
       return groups;
       }
      }
      


        • 1. Re: JavaServer Faces and container-managed authorization not
          Bob Muller Novice

          The last sentence in the second paragraph should read, "All my Faces navigation rules use the &lt;redirect/&gt; option. Lost in HTML translation.

          • 2. Re: JavaServer Faces and container-managed authorization not
            Bob Muller Novice

            Solved the application-policy name. My login module code uses the full JNDI name, and it should be using just the unqualified name without the JNDI prefixes. Then the login-config.xml and web.xml references to the name can also leave off the JNDI prefixes and everything works (except for the role, still).

            Also, I replaced the login form in web.xml with BASIC authentication and the two properties files, and that worked with no problems--myHome.faces came up just fine. So I really don't think this is a Faces problem.

            I'm now trying the DatabaseServerLoginModule instead of the custom module as I figured out a way to convert our version of roles into the standard query for that module. It isn't properly comparing the password somehow ("Password Incorrect/Password Required"), looks like I'll have to try debugging with source. Any comments welcome :). It's increasingly looking like my custom login module is missing something basic that registers the authentication with the container.

            • 3. Re: JavaServer Faces and container-managed authorization not
              Bob Muller Novice

              Debugged the DatabaseServerLoginModule using source and found it was getting a blank at the end from the database, added ltrim/rtrim and got successful authentication. However, it still fails to navigate to myHome.faces. So the problem is not in the custom login module but somewhere deep in the bowls of how JBoss registers the Subject. Any advice from anyone about where to look next?

              • 4. Re: JavaServer Faces and container-managed authorization not
                Ragav Gomatam Novice

                I still suspect its an issue with your LoginModule. Code for accessing the database by extending AbstarctSeverLoginModule is posted in the forum..Check that out & compare yours...Are you setting the roles correctly ?

                • 5. Re: JavaServer Faces and container-managed authorization not
                  Bob Muller Novice

                  As I reported in my followup post, I replaced my custom login module entirely with the standard JBoss DatabaseServerLoginModule configured with queries. That works fine and authenticates, but I'm still not seeing the roles being accepted during authorization. The roles are coming back correctly from the database, I checked the spelling and stepped through the DatabseServeLoginModule code to verify that. The role "community_user" is coming from the database query and is put into the SimplePrincipal inside the Subject, and that is the role in the security-constraint in web.xml that is authorized for the myHome.faces file. It still fails to authorize and redisplays the login page.

                  Here is the login-config.xml code:

                   <authentication>
                   <!-- A JDBC based LoginModule
                   LoginModule options:
                   dsJndiName: The name of the DataSource of the database containing the Principals, Roles tables
                   principalsQuery: The prepared statement query equivalent to:
                   "select Password from Principals where PrincipalID=?"
                   rolesQuery: The prepared statement query equivalent to:
                   "select Role, RoleGroup from Roles where PrincipalID=?"
                   -->
                   <login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule" flag="required">
                   <module-option name="dsJndiName">java:jdbc/ReadOnlyTairTestJTDS</module-option>
                   <module-option name="principalsQuery">select ltrim(rtrim(password)) from Community where user_name=?</module-option>
                   <module-option name="rolesQuery">
                   SELECT "Role", 'Roles' AS RoleGroup FROM (SELECT c.user_name, 'community_user' AS "Role" FROM Person p JOIN
                   Community c ON p.community_id = c.community_id UNION SELECT c.user_name, 'tair_curator' AS "Role" FROM Person p
                   JOIN Community c ON p.community_id = c.community_id WHERE p.is_tair_curator = 'T' UNION SELECT c.user_name,
                   'external_curator' AS "Role" FROM Person p JOIN Community c ON p.community_id = c.community_id WHERE
                   p.is_external_curator = 'T') AS Roles WHERE user_name = ?
                   </module-option>
                   </login-module>
                   </authentication>
                   </application-policy>
                  

                  As you can see, the roles are hard-coded strings, which I've verified against the web.xml constraint (see that in the previous post).

                  So, what might be preventing JBoss security management from seeing the Subject?

                  • 6. Re: JavaServer Faces and container-managed authorization not
                    Bob Muller Novice

                    Some additional information. Here is the code from the managed bean action method that the Login button calls that creates the LoginContext. I've added debugging code to retrieve the subject from the authentication cache (at least I hope that's what PolicyContext does) and display everything, then I do an isUserInRole call on the role:

                     LoginContext loginContext = new LoginContext(LOGIN_APP_POLICY, this);
                     loginContext.login();
                     // If there is no exception, login succeeded.
                     returnString = SUCCESS;
                     // Remove the password from memory and Faces display.
                     password = null;
                     // Put the loginContext object into the user's session.
                     request.getSession().setAttribute(LOGIN_CONTEXT_ATTR, loginContext);
                     // TODO debugging code -- get subject from cache?
                     //Subject subject = loginContext.getSubject();
                     Subject subject = (Subject) PolicyContext.getContext("javax.security.auth.Subject.container");
                    
                     Set<Principal> principals = subject.getPrincipals();
                     for (Principal p : principals) {
                     System.out.println("Principal " + p.getName());
                     if (p.getName().equalsIgnoreCase("Roles")) {
                     Group g = (Group)p;
                     Enumeration<? extends Principal> roles = g.members();
                     while (roles.hasMoreElements()) {
                     Principal role = roles.nextElement();
                     System.out.println("Role " + role.getName());
                    
                     }
                     }
                     }
                     boolean isInRole = request.isUserInRole(DataConstants.COMMUNITY_USER);
                     if (isInRole) {
                     System.out.println("User is in role " + DataConstants.COMMUNITY_USER);
                     } else {
                     System.out.println("User is not in role "
                     + DataConstants.COMMUNITY_USER);
                     }
                    

                    The output from this follows:
                    11:08:47,790 INFO [STDOUT] Principal techteam
                    11:08:47,791 INFO [STDOUT] Principal Roles
                    11:08:47,791 INFO [STDOUT] Role tair_curator
                    11:08:47,791 INFO [STDOUT] Role community_user
                    11:08:47,791 INFO [STDOUT] User is not in role community_user
                    

                    If I'm interpreting this correctly, the cached Subject has the correct role in the Roles group but the isUserInRole() method is not finding it. It may be that the HttpRequest here is outdated, but shouldn't that method go to the cache? What am I not understanding?

                    • 7. Re: JavaServer Faces and container-managed authorization not
                      Ragav Gomatam Novice

                      Can you do a quick test ? Can you replace your jsf with plain jsp ? Also can you post your jsp/html code of the login page ?

                      Does it have

                      form action="j_security_check"

                      and
                      j_username and j_password


                      I presume it does & If it does, then WebContainer does the authentication & all this code below

                      LoginContext loginContext = new LoginContext(LOGIN_APP_POLICY, this);
                      loginContext.login();
                      // If there is no exception, login succeeded.
                      returnString = SUCCESS;
                      // Remove the password from memory and Faces display.
                      password = null;
                      // Put the loginContext object into the user's session.
                      request.getSession().setAttribute(LOGIN_CONTEXT_ATTR, loginContext);


                      is not neccessary.
                      So lets try to replace jsf with a plain jsp. If that works, we have isolated the problem to jsf managed bean.

                      • 8. Re: JavaServer Faces and container-managed authorization not
                        Bob Muller Novice

                        Here is the plain jsp login page:

                        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
                        
                        <%@ page errorPage="/jsp/common/gen_error.jsp" %>
                        
                        <jsp:include page="/jsp/includes/dyn_header.jsp" flush ="true">
                        <jsp:param name="pageName" value="Login Test" />
                        <jsp:param name="id" value="5" />
                        </jsp:include>
                        
                         <form action="j_security_check" method="post">
                         Username: <input type="text" name="j_username" size="22"/>
                         Password: <input type="password" name="j_password" size="22"/>
                         <input type="submit" value="Login" />
                         </form>
                        <jsp:include page="/jsp/includes/gen_footer.jsp" flush="true" />
                        

                        When I enter the myHome.faces URL, it displays this page, I enter the username and password, and it goes to the myHome.xhtml page as it should, so everything works properly. This is the same behavior I get when I use BASIC authentication as opposed to FORM. I verified that the login module being used is the DatabaseServerLoginModule (debugged into it and saw the principals being built).

                        So the only difference here is the plain jsp versus the Faces xhtml. Here's the code for that:
                        <?xml version="1.0" encoding="UTF-8"?>
                        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
                        
                        <html xmlns="http://www.w3.org/1999/xhtml"
                         xmlns:ui="http://java.sun.com/jsf/facelets"
                         xmlns:h="http://java.sun.com/jsf/html"
                         xmlns:f="http://java.sun.com/jsf/core" xml:lang="en" lang="en">
                        
                         <f:view>
                         <ui:composition template="/facelets/templates/standard.xhtml">
                         <ui:define name="title">TAIR - Login Page</ui:define>
                         <ui:define name="css">
                         <link rel="stylesheet" type="text/css" href="/css/page/login.css" />
                         </ui:define>
                         <ui:define name="content">
                         <h:form>
                         <h:messages layout="table" globalOnly="true" errorClass="error"></h:messages>
                         <h:panelGrid columns="2">
                         <h:outputLabel for="username">User name:</h:outputLabel>
                         <h:inputText id="username" value="#{login.username}" />
                         <h:outputLabel for="password">Password:</h:outputLabel>
                         <h:inputSecret id="password" value="#{login.password}" />
                         <h:commandButton value="Login" action="#{login.login}" />
                         <h:outputText value="" />
                         </h:panelGrid>
                         <p>
                         If you forgot your username or password,
                         <h:commandLink value=" request your login information here."
                         action="request_info" />
                         </p>
                         <p>
                         If your personal profile does not exist in our database,
                         <h:commandLink value=" register " action="register" />
                         as a new Tair user.
                         </p>
                         </h:form>
                         </ui:define>
                         </ui:composition>
                         </f:view>
                        </html>
                        
                        

                        Obviously this doesn't authenticate to the container unless the code posted earlier for the login method in the managed bean does that under the covers when it calls loginContext.login().

                        I think I can live with a Facelets version of the login form using the Tomcat authentication protocol (I'll build that and test it now), but do you have any idea why the standard LoginContext code doesn't work here? This stuff is straight out of the examples and documentation. Is there some other call I need to make to tell the container about the login context subject?

                        • 9. Re: JavaServer Faces and container-managed authorization not
                          Bob Muller Novice

                          BTW, thanks for your help--I really appreciate it very much!

                          • 10. Re: JavaServer Faces and container-managed authorization not
                            Ragav Gomatam Novice

                            HI,

                            So you are saying jsp version works ???

                            When I enter the myHome.faces URL, it displays this page, I enter the username and password, and it goes to the myHome.xhtml page as it should, so everything works properly. This is the same behavior I get when I use BASIC authentication as opposed to FORM


                            In your jsf version, where is the j_security_check ? That indicates to the container to do the JAAS login ? PLus earlier you had done the JAAS login within your managed bean . I am posting that code again
                            LoginContext loginContext = new LoginContext(LOGIN_APP_POLICY, this);
                            loginContext.login();

                            This is wrong. Container does the login . Not you. Plus after successful login, test it with
                            Principal p = request.getPrincipal()

                            and
                            boolean isInRole = request.isUserInRole(DataConstants.COMMUNITY_USER);


                            Both should return NON-NULLS.

                            My hunch is that there is an error in your jsf. Remember, j_security_check indicates to the Container to do a JAAS login, even before, the request is routed to your application's controller or welcome page or servlet.