Saturday, July 17, 2010

How to implement JBoss Login Module

Recently I started use Jboss as an application server. I picked up one interesting feature of it, which are security utilities comes with Jboss. JBoss provides very rich security components. I was able to implement simple login authentication module using Jboss. This may be a advance step in case of authentication. Even you are not a authentication module developer, it is safe to keep some understanding, at least to develop a simple authentication module using Jboss authentication utility. Here, I have explained step by step how to implement simple login module with jboss security utilitis.
I am going to use DatabaseServerLoginModule which is a JDBC base login module comes with Jboss. DatabaseServerLoginModule is based on tow logical tables, Principals and Roles. In my case, I have created tow tables AUTO_AC_USER and AUTO_AC_ROLE, probably in your case, User and Role tables.

The relevant create table statements are as fallows.
CREATE TABLE AUTO_AC_USER (
ID int(10) unsigned NOT
    NULL AUTO_INCREMENT,
    USER_ID varchar(45) NOT NULL,
    USER_NAME varchar(45) NOT NULL,
    PASSWORD varchar(45) NOT NULL,
    PRIMARY KEY (ID)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1

CREATE TABLE AUTO_AC_ROLE (
   ROLE_ID int(10) unsigned NOT NULL AUTO_INCREMENT,
   ROLE_NAME varchar(45) NOT NULL,
   PRIMARY KEY (ROLE_ID) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1

One user can be assigned several roles, so I have created intermediate table to store user assigned roles. The create table statement for AUTO_AC_USER_ASSIGNED_ROLE table is as fallows.

CREATE TABLE AUTO_AC_USER_ASSIGNED_ROLE (
   ID int(10) unsigned NOT NULL AUTO_INCREMENT,
   USER_ID varchar(45) NOT NULL,
   ROLE_ID int(10) unsigned NOT NULL,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1

Now, we have create relation database tables required for the login module. Next, we have to add an application policy into the login-config.xml file located in ...\jboss-5.0.0.GA\server\default\conf folder.
The login-config.xml file entry for our sample login module is as fallows.
<application-policy name="AutoACLogin">
   <authentication>
      <login-module code="com.semika.autoac.security.auth.JAASLoginModule" flag="required">
          <module-option name="dsJndiName">java:/autoac</module-option>
          <module-option name="principalsQuery">
              SELECT u.PASSWORD as password FROM AUTO_AC_USER u WHERE u.USER_ID =?
          </module-option>
          <module-option name="rolesQuery">
              SELECT r.ROLE_NAME as role, 'Roles' 
              FROM AUTO_AC_ROLE r WHERE r.ROLE_ID IN (SELECT ur.ROLE_ID 
                                           FROM AUTO_AC_USER_ASSIGNED_ROLE ur,AUTO_AC_USER u 
                                           WHERE ur.USER_ID=u.USER_ID AND u.USER_ID=?)
          </module-option>
      </login-module>
   </authentication>
</application-policy>

The supported login module configuration options include the following:

dsJndiName: The JNDI name for the DataSource of the database containing the logical Principals and Roles tables. If not specified this defaults to java:/DefaultDS

principalsQuery: The prepared statement query to fetch the user principals, like user name, user id, password etc.

rolesQuery: The prepared statement query to fetch the roles assigned to user

Also note that login-module's code attribute which is com.semika.autoac.security.auth.JAASLoginModule which is my custom login module class.

Now we have finished some, but remaining a lot. Next we will see from where, login is instantiated. In my application, I am using struts2 as a front end framework. I have LoginAction.java class, which is a struts2 action class. User login request will invoke the login() method of LoginAction class, username and password as login parameters.
public String login() {
    try {
         System.setProperty("java.security.auth.login.config", "login-config.xml");
         LoginContext lc = new LoginContext("AutoACLogin", new JAASCallbackHandler(getUserName(), getPassword()));
         lc.login();
         Subject subject = lc.getSubject();
         Set principleSet = subject.getPrincipals();
    } catch (LoginException e) {
         e.printStackTrace();
         return INPUT;
    }
     return SUCCESS;
}

If login is success, it returns the login() method of LoginContext, if login is fail, it throws LoginException.

Now the time to show full code for JAASLoginModule and JAASCallbackHandler classes. This is my custom login module class. There are tow important classes of JAAS when implementing login module. That is Subject and Principal.

A Subject represents group of information for a single entity, like login User's information. Such information includes the Subject's identities like userId, userName as well as its security-related attributes like password and also the user assigned roles. In my sample application USER_ID is the subject identity. This subject identity should be represented as a Principal with in the subject.

I implemented a UserPrincipal.java class for this purpose.
package com.semika.autoac.security.auth;

import java.security.Principal;
public class UserPrincipal implements Principal {

   private final String name;

   public UserPrincipal(String name) {
      super();
      this.name = name;
   }
   @Override
   public String getName() {
      return name;
   }
   @Override
   public boolean equals(Object obj) {
      if (this==obj) return true;
      if (obj instanceof UserPrincipal) {
      UserPrincipal up = (UserPrincipal) obj;
          if (this.name!=null && this.name.equals(up.getName())) {
              return true;
          }
      }
      return false;
   }
   @Override
      public int hashCode() {
      return 31 + ((this.name != null ? this.name.hashCode() : 0) * 3 / 2);
   }
   @Override
      public String toString() {
      return "UserPrincipal : {" + this.name +"}";
   }
}

JAASLoginModule.java
package com.semika.autoac.security.auth;

import java.security.Principal;
import java.security.acl.Group;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.security.auth.login.LoginException;
import javax.sql.DataSource;

import org.jboss.security.SimpleGroup;
import org.jboss.security.auth.spi.DatabaseServerLoginModule;

public class JAASLoginModule extends DatabaseServerLoginModule  {

    private static final String QRY_FIELD_PASSWORD = "password";
    private UserPrincipal caller;

    public JAASLoginModule(){
        System.out.println("Login Module Called");
    }
    public boolean login() throws LoginException {
         System.out.println("Login Authentication Started");
         try {
            if (super.login()) {
                String userName = getIdentity().getName().trim();
                if (userName == null || userName.length() == 0) {
                    throw new LoginException("NULL or Empty user name   while       attempting to login");
                }
               caller = new UserPrincipal(userName);
               System.out.println("User Authenticated " + userName);
               return true;
             }
            return false;
         } catch (LoginException e) {
                System.out.println("User Authentication Failed");
                e.printStackTrace();
                throw e;
          }
   }  
   @Override
   protected String getUsersPassword() throws LoginException {
       String sql = (String)options.get("principalsQuery");
       String password = null;
       Connection conn = null;
       PreparedStatement stmt = null;
       ResultSet rs = null;
       try {
            conn = getDataSource().getConnection();
            stmt = conn.prepareStatement(sql);
            stmt.setString(1, getUsername().trim());
            rs = stmt.executeQuery();
            if (rs.next()) {
                 password = rs.getString(QRY_FIELD_PASSWORD);
            }
       } catch (SQLException e) {
             e.printStackTrace();
       } catch (NamingException e) {
             e.printStackTrace();
       } finally {
             try {
                 rs.close();
             } catch (SQLException e) {
                 System.out.println("Error when closing result set.");
                 e.printStackTrace();
             }
             try {
                 stmt.close();
             } catch (SQLException e) {
                 System.out.println("Error when closing statement.");
                 e.printStackTrace();
             }
            try {
                conn.close();
            } catch (SQLException e) {
                System.out.println("Error when closing connection.");
                e.printStackTrace();
            }
       }
       return password;
   }
   @Override
   protected Group[] getRoleSets() throws LoginException {
         String sql = (String)options.get("rolesQuery");
         Connection conn = null;
         PreparedStatement stmt = null;
         ResultSet rs = null;
         Group roles = null;
         try {
              conn = getDataSource().getConnection();
              stmt = conn.prepareStatement(sql);
              stmt.setString(1, getUsername().trim());
              rs = stmt.executeQuery();
              while(rs.next()){
                  roles = new SimpleGroup(rs.getString("role"));
                  roles.addMember(caller);
              }
         } catch (SQLException e) {
                e.printStackTrace();
         } catch (NamingException e) {
                e.printStackTrace();
         } finally {
               try {
                   rs.close();
               } catch (SQLException e) {
                   System.out.println("Error when closing result set.");
                   e.printStackTrace();
               }
              try {
                   stmt.close();
              } catch (SQLException e) {
                   System.out.println("Error when closing statement.");
                   e.printStackTrace();
              }
              try {
                   conn.close();
              } catch (SQLException e) {
                   System.out.println("Error when closing connection.");
                   e.printStackTrace();
              }
          }
          return new Group[]{roles};
    }

    @Override
    protected Principal getIdentity() {
         return (caller != null) ? caller : super.getIdentity();
    }

    protected DataSource getDataSource() throws NamingException {
         InitialContext ctx = null;
         Properties jndiEnv = new Properties();
         // Add Env Property : Initial Context Factory if given as module option
         if (options.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
              jndiEnv.put(Context.INITIAL_CONTEXT_FACTORY, options.get(Context.INITIAL_CONTEXT_FACTORY));
         }
         // Add Env Property : Provider URL(s) if given as module option
         if (options.get(Context.PROVIDER_URL) != null) {
              jndiEnv.put(Context.PROVIDER_URL, options.get(Context.PROVIDER_URL));
         }
         // Initialize Context with or without env props
         if (options.size() > 0) {
              System.out.println("Initializing InitialContext for JBoss Login Module with JNDI Environment : " + jndiEnv.toString());
              ctx = new InitialContext(jndiEnv);
         } else {
               System.out.println("Initializing InitialContext for JBoss Login Module with default JNDI Environment");
               ctx = new InitialContext();
         }
         // Lookup and return DS
         return (DataSource) ctx.lookup(dsJndiName);
    }
}

Since, I extended from DatabaseServerLoginModule class, I should implement tow methods necessarily. Those are getUserPassword() and getRoleSets() method.

JAASCallbackHandler.java
package com.semika.autoac.security.auth;

import java.io.IOException;
import java.security.Principal;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.jboss.security.auth.callback.SecurityAssociationHandler;

public class JAASCallbackHandler implements CallbackHandler {

   private String username;
   private String password;

   public JAASCallbackHandler (String username, String password) {
     this.username = username;
     this.password = password;
     SecurityAssociationHandler handler = new SecurityAssociationHandler();
     Principal principal = new UserPrincipal(username);
     handler.setSecurityInfo(principal, password.toCharArray());
   }
   @Override
   public void handle(Callback[] callbacks) throws IOException,
     UnsupportedCallbackException {
     System.out.println("Callback Handler - handle called");
     for (int i = 0; i < callbacks.length; i++) {
       if (callbacks[i] instanceof NameCallback) {
          NameCallback nameCallback = (NameCallback) callbacks[i];
          nameCallback.setName(username);
       } else if (callbacks[i] instanceof PasswordCallback) {
          PasswordCallback passwordCallback = (PasswordCallback) callbacks[i];
          passwordCallback.setPassword(password.toCharArray());
       } else {
          throw new UnsupportedCallbackException(callbacks[i], "The submitted Callback is unsupported");
       }
     }
   }
}
Next, we will see how to instantiate LoginContext. Before creating an instance of LoginContext,I have to set the login configuration setting. One way of doing this is, setting system property, java.security.auth.login.config.
System.setProperty("java.security.auth.login.config", "login-config.xml");  
Since I am using Jboss login utilities, login-config.xml file is where I have configure the login configuration.

LoginContext lc = new LoginContext("AutoACLogin", new JAASCallbackHandler(getUserName(), getPassword())); 
When instantiating LoginContext, I have to pass application policy name which I have declared in login-config.xml file and call back handler instance. In this case application policy name is "AutoACLogin" which helps to load the login module which is "JAASLoginModule". When creating new instance of call back handler, I have passed in user name and password required for user authentication. When invoking LoginContext.login() method, it invokes the loaded LoginModule and subsequently handle() method of callback handler. The required information for authentication are passed into the handle() method as an array of callbacks. In this case, login module passes NameCallback and PasswordCallback. The handle() method of callback handler populates those callbacks with actual information and allow to get it by login module for the sake of authentication. After the authentication process, LoginContext returns the status to the application.Success is represented by a return from the login method. Failure is represented through a LoginException being thrown by the login method.

4 comments:

Share

Widgets