Version 2

    The Java Authentication and Authorization API (JAAS) provides a standard way for applications to handle user identification and permission issues. Many people compare JAAS to PAM modules on Unix/Linux systems.

     

    It is not the intent of this document to discuss JAAS in detail, but the nickle tour follows. Under JAAS, when a user logs into the system they are given a Subject which contains a set of one or more Principal.  Each Principal provides the user's identity, e.g., the user's ID, and can also be used to identify roles and groups. Subjects can also contain private and public credentials, e.g., X.509 certificates, private keys, etc. JAAS handles authorization via declarative Permission maintained in an external Policy.  Authorization is beyond the scope of this document.  The actual login process is handled by a LoginModule, discussed below.  If you would like more information, a good introduction to JAAS is available at http://www.javaworld.com/javaworld/jw-09-2002/jw-0913-jaas.html

     

    The first step in implementing a Hibernate JAAS LoginModule is to define one or more Principal.  A typical example follows, but it's important to remember that you can stuff anything you want into a Principal -- user ID, email address, phone number, public key -- all goodies that could reasonably be found in any persisted User object.

     

    final public class HibernatePrincipal implements Principal {
    
       private String name;
    
       public HibernatePrincipal() {
          name = "";
       }
    
       public HibernatePrincipal(String name) {
          this.name = name;
       }
    
       public String getName() {
          return name;
       }
    
       public int hashCode() {
          return name.hashCode();
       }
    
       public boolean equals(Object o) {
          if (!(o instanceof HibernatePrincipal)) {
             return false;
          }
          return name.equals(((HibernatePrincipal) o).name);
       }
    
       public String toString() {
          return name;
       }
    }
    

     

    N.B., you must be careful to prevent a malicious party from extending your class in order to manufacture their own credentials. You can do this by either declaring your Principal final, or by always verifying that the credential is   the expected type.

       public void foo(Principal p) {
    
          // DO NOT USE THIS
          if (p instanceof HibernatePrincipal) {
             ...
          }
    
          // use this instead
          if (p.getClass().equals(HibernatePrincipal.class)) {
             ...
          }
    
          // or even this
          if (p.getClass().getName().equals(HibernatePrincipal.getClass().getName()) {
             ...
          }
       }
    }
    

     

    We don't discuss credentials in detail in this document, but it should be easy to see how a User attribute could map to Subject credentials.  A username, for instance, would be a reasonable candidate as a public credential.

    Now that we have a Principal, we can wrap some standard Hibernate code with the glue required for a JAAS LoginModule

    /**
     * HibernateLoginModule is a LoginModule that authenticates
     * a given username/password credential against a Hibernate
     * session.
     *
     * @see javax.security.auth.spi.LoginModule
     */
    public class HibernateLoginModule implements LoginModule {
    
       // initial state
       CallbackHandler handler;
       Subject subject;
       Map sharedState;
       Map options;
       Digest digest;
    
       // temporary state
       Vector principals;
    
       // authentication status
       boolean success;
    
       // configurable options
       boolean debug;
    
       /** Hibernate session factory  */
       SessionFactory sf = null;
    
       /** Hibernate query */
       private static final String query =
          "from u in class " + User.class + " where u.name=?";
    
       public HibernateLoginModule() {
          credentials = new Vector();
          principals = new Vector();
          success = false;
          debug = false;
       }
    
       /**
        * Initialize our state.
        */
       public void initialize (Subject subject, CallbackHandler handler,
          Map sharedState, Map options) {
    
          this.handler = handler;
          this.subject = subject;
          this.sharedState = sharedState;
          this.options = options;
    
          if (options.containsKey("debug")) {
             debug = "true".equalsIgnoreCase((String) options.get("debug"));
          }
          if (options.containsKey("digest")) {
             digest = new Digest((String) options.get("digest"));
          } else {
             digest = new Digest();
          }
    
          // elided: standard code to get Hibernate =SessionFactory=.
       }
    
       /**
        *  First phase of login process.
        */
       public boolean login() throws LoginException {
          if (handler == null) {
             throw new LoginException("Error: no CallbackHandler available");
          }
    
          try {
             Callback[] callbacks = new Callback[] {
                new NameCallback("User: "),
                new PasswordCallback("Password: ", false)
             };
    
             handler.handle(callbacks);
    
             String username = ((NameCallback) callbacks[0]).getName();
             char[] password = ((PasswordCallback) callbacks[1]).getPassword();
    
             ((PasswordCallback) callbacks[1]).clearPassword();
    
             success = validate(username, password);
    
             callbacks[0] = null;
             callbacks[1] = null;
    
             if (!success) {
                throw new LoginException("Authentication failed: Password does not match");
             }
             return true;
          } catch (LoginException e) {
             throw e;
          } catch (Exception e) {
             success = false;
             throw new LoginException(e.getMessage());
          }
       }
    
       /**
        * Second phase of login - by now we know user is authenticated
        * and we just need to update the subject.
        */
       public boolean commit() throws LoginException {
          if (success) {
             if (subject.isReadOnly()) {
                throw new LoginException("Subject is read-only");
             }
    
             try {
                Iterator i = principals.iterator();
                subject.getPrincipals().addAll(principals);
                principals.clear();
                return true;
             } catch (Exception e) {
                throw new LoginException(e.getMessage());
             }
          } else {
             principals.clear();
          }
          return true;
       }
    
       /**
        * Second phase - somebody else rejected user so we need to
        * clear our state.
        */
       public boolean abort() throws LoginException {
          success = false;
          logout();
          return true;
       }
    
       /**
        *  User is logging out - clear our information from the subject.
        */
       public boolean logout() throws LoginException {
          principals.clear();
    
          // remove the principals the login module added
          Iterator i = subject.getPrincipals(HibernatePrincipal.class).iterator();
          while (i.hasNext()) {
             HibernatePrincipal p = (HibernatePrincipal) i.next();
             subject.getPrincipals().remove(p);
          }
    
          return true;
       }
    
       /**
        * Validate the user name and password.  This is the Hibernate-specific
        * code.
        */
       private boolean validate(String username, char[] password) throws Exception {
          boolean valid = false;
          List users = null;
    
          Session s = null;
          try {
             s = sf.openSession();
             users = (List) s.find(query, username, Hibernate.STRING);
          } catch (Exception e) {
          } finally {
             if (s != null) {
                try { s.close(); } catch (HibernateException e) { }
             }
          }
    
          // are there no matching records?...
          if (users == null || users.size() == 0) {
             return false;
          }
    
          // compare passwords...
          User user = (User) users.get(0);
          String hash = user.getPassword(); 
          if (hash != null && password != null && password.length > 0) {
             valid = hash.equals(digest.digest(new String(password)));
          }
    
          if (valid) {
             this.principals.add(new HibernatePrincipal(user.getId(),
                user.getName()));
          }
          return valid;
       }
    }
    

     

    In this example we have used a password digest function borrowed from the Tomcat library (and you need to link catalina.jar for the HexUtils class.)

    import org.apache.catalina.util.HexUtils;
    
    /**
     * Quick and dirty password digest function.  The HexUtils class
     * comes from the Tomcat catalina.jar.
     */
    public class Digest {
    
       static MessageDigest md = null;
    
       public Digest() {
          this("MD5");
       }
    
       public Digest(String digest) {
          try {
             md = MessageDigest.getInstance(digest);
          } catch (NoSuchAlgorithmException e) {
             try {
                md = MessageDigest.getInstance("MD5");
             } catch (NoSuchAlgorithmException e) { }
          }
       }
    
       /**
        * Digest function from Tomcat.
        */
       public String digest(String credentials) {
          if (md == null) {
             return credentials;
          }
    
          synchronized (this) {
             try {
                md.reset();
                md.update(credentials.getBytes());
                return (HexUtils.convert(md.digest()));
             } catch (Exception e) {
                return credentials;
             }
          }
       }
    }
    

     

    The final step is telling our application about our Hibernate login module. We do this by creating a JAAS configuration file and passing it to the application via the java.security.auth.login.config parameter.  In this case we define the JAAS realm "Example."

    Example {
        HibernateLoginModule required debug="true";
    };

     

    We can now authenticate against our Hibernate module from any JAAS-aware application.  A simple test application follows.

    /** 
     * simple CallbackHandler suitable for testing purposes 
     */
    public static class Handler implements CallbackHandler {
      
       private Test t;
       private String username;
       private char[] credentials;
    
       public Handler(Test t, String username, char[] credentials) {
          super();
          this.t = t;
          this.username = username; 
          this.credentials = credentials;
       }
    
       public void handle(Callback callbacks[])
          throws IOException, UnsupportedCallbackException {
    
          for (int i = 0; i < callbacks.length; i++) {
             if (callbacks[i] instanceof NameCallback) {
                ((NameCallback) callbacks[i]).setName(username); 
             }
             else if (callbacks[i] instanceof PasswordCallback) {
                ((PasswordCallback) callbacks[i]).setPassword(credentials);
             } else {
                throw new UnsupportedCallbackException(callbacks[i]);
             }
          }
       }
    }
    
    /**
     * Simple JAAS-aware application.
     */
    public class Test {
    
       LoginContext l = null;
    
       /**
        * attempt to log in as the user, returning the =Subject=
        * if successful.
        */
       public Subject login(String username, char[] credentials) {
          try {
             CallbackHandler cb = new Handler(this, username, credentials);
             l = new LoginContext("Example", cb);
          } catch (LoginException e) {
             return null;
          }
    
          Subject subject = null;
          try {
             l.login();
             subject = l.getSubject();
             if (subject == null) {
                return null;
             }
          } catch (AccountExpiredException e) {
          } catch (CredentialExpiredException e) {
          } catch (FailedLoginException e) {
          } catch (LoginException e) {
          }
          return subject;
       }
    
       /**
        * log out of application
        */
       public void logout() {
          if (l != null) {
             try {
                l.logout();
             } catch (LoginException e) {
             }
          }
       }
    
       public static void main(String[] args) throws Exception {
          Test t = new Test();
          String username = "test";
          String password = "test";
     
          Subject subj = t.login(username, password.toCharArray());
          if (subj != null) {
             Iterator i = subj.getPrincipals(HibernatePrincipal.class).iterator();
             while (i.hasNext()) {
                HibernatePrincipal p = (HibernatePrincipal) i.next();
                System.out.println("logged in as: " + p.getName());
             }
             t.logout();
          }
          else {
             System.out.println("unable to log in as user");
          }
       }
    }
    

    As alluded to above, the real power of JAAS is not the flexiblity in handling user logins, it's the declarative security model provided by Permissions and the mechanism for handling credentials.  More to follow...