Sunday 26 September 2010

JAAS with Tomcat

As JAAS has been included as part of JRE core since Java 1.4, it should be quite prevalent by now. However, I cannot seem to find many online resources that detail the  steps involved to set up JAAS with Tomcat. Here, I outline these steps from a beginner’s perspective.

Step 1 – Implement the JASS interfaces.

The basic interfaces to be implemented are

  1. Principal – there should be two implementations of this interface: One representing the user; another one (or more) represent the roles. The important method of this interface is getName() which returns the name of the user or role that it represents.
  2. LoginModule – this is the main class that usually serves as the wrapper for customised authentication logic. The login() method is where the customised login logic should be invoked.

Once implemented, JAR up these classes including the customised login classes and put it in $CATALINA_BASE/lib.

See the code segments at the end for example implementations.

Step 2 – Configure Tomcat Server

Edit $CATALINA_BASE/conf/server.xml to put in the JAASRealm configuration. As instructed by Tomcat’s Realm-HOW-TO documentation, this can be done at various levels - <Engine>, <Host> or <Context>. I added it inside the <Host> tag:



	
	
...

Notice the userClassNames and roleClassNames are the classes that were created in step 1.

Step 3 – The JRE login configuration

Create a JAAS Login Configuration file. I called it jaas.config and put it under $CATALINA_BASE/conf:
acczk {
  com.laws.acc.jaas.AccLoginModule required;
};

There are two ways to configure the JRE to load the login ocnfiguration file. I prefer to specify it in JAVA_OPTS. For Tomcat, this means to create a setenv.sh or setenv.bat in the $CATALINA_BASE/bin directory. Here is my setenv.bat:

set JAVA_OPTS=-Djava.security.auth.login.config==C:/Tools/apache-tomcat-7.0.2/conf/jaas.config

Step 4 – The Login Form

The login method can be BASIC or FORM-based. I prefer FORM. A vanilla HTML form should do. I happen to be using ZK so my HTML login form is in the file login.zul:






Welcome to ${c:l('client_name')}!

import com.laws.acc.ws.*; String errMsg = null; LoginResponse loginResponse=AccSoapClientProxy.getLoginResponse(); if(loginResponse!=null) { switch(loginResponse.getResultCode()) { case ResultCode.NOT_LOGGED_IN: errMsg="Not logged in."; break; case ResultCode.CONNECTION_ERROR: errMsg="Cannot login to server:\n"+loginResponse.getErrorDescription(); break; default: errMsg="Login failure:\n"+loginResponse.getErrorDescription(); } } Username : Password :
${errMsg}
j_username.focus();
The important part here is the HTML form employing j_username, j_password and j_security_check.

Step 5 - Configure web.xml

This is standard configuration to enable security constraints.

      ACC Security Constraint
      
         Protected Area
         
         /index.zul
	 /secure/*
         
         DELETE
         GET
         POST
         PUT
      
      
         
         Acc User
      
    
	
	  Generic authenticated ACC user
	  Acc User
	
    
    
      FORM
      Form-Based Authentication Area
      
        /login.zul
        /login.zul
      
    
  	
  	120	
  

My JAAS implementation files:

AccLoginModule.java:
package com.laws.acc.jaas;

import java.io.IOException;
import java.util.Map;

import javax.security.auth.Subject;
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 javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import com.laws.acc.ws.AccSoapClientProxy;
import com.laws.acc.ws.LoginResponse;

public class AccLoginModule implements LoginModule {
	// initial state
    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map sharedState;
    private Map options;
    
    private AccPrincipal principal;
    private boolean committed = false;

	@Override
	public boolean abort() throws LoginException {
		if(!committed)
			return false;
		if(principal!=null) {
			logout();
			principal=null;
		}
		return true;
	}

	@Override
	public boolean commit() throws LoginException {
		if (principal == null){
            return false;
        }
        if (!subject.getPrincipals().contains(principal))
            subject.getPrincipals().add(principal);
	// hardcoded role assignment.
        AccRole role=new AccRole();
        if(!subject.getPrincipals().contains(role))
        	subject.getPrincipals().add(role);
        committed = true;
        return (true);
	}

	@Override
	public void initialize(Subject subject, CallbackHandler callbackHandler,
			Map sharedState, Map options) {
		this.subject = subject;
		this.callbackHandler = callbackHandler;
		this.sharedState = sharedState;
		this.options = options;
	}

	@Override
	public boolean login() throws LoginException {
		if (callbackHandler == null)
            throw new LoginException("No CallbackHandler specified");
        Callback callbacks[] = new Callback[2];
        callbacks[0] = new NameCallback("Username: ");
        callbacks[1] = new PasswordCallback("Password: ", false);
 
        // Interact with the user to retrieve the username and password
        String username = null;
        String password = null;
        try {
            callbackHandler.handle(callbacks);
            username = ((NameCallback) callbacks[0]).getName();
            password = new String(((PasswordCallback) callbacks[1]).getPassword());
            LoginResponse response = AccSoapClientProxy.login(username,password);
            if (response.getResultCode()!=0)
                return false;
            principal  = new AccPrincipal(response);
            return true;
        } catch (IOException e) {
            throw new LoginException(e.toString());
        } catch (UnsupportedCallbackException e) {
            throw new LoginException(e.toString());
        }
	}

	@Override
	public boolean logout() throws LoginException {
		committed=false;
		subject.getPrincipals().remove(principal);

		return false;
	}

}
AccPrincipal.java:
package com.laws.acc.jaas;

import java.io.Serializable;
import java.security.Principal;
import com.laws.acc.ws.*;

public class AccPrincipal implements Principal, Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 5002820876845306935L;
	private LoginResponse loginResponse=null;
	
	public AccPrincipal(LoginResponse lr) {
		this.loginResponse=lr;
	}
	
	@Override
	public String getName() {
		return loginResponse.getUserName();
	}

	@Override
	public boolean equals(Object other) {
		if(other==null) return false;
		if(other==this) return true;
		if(other instanceof AccPrincipal) {
			AccPrincipal that=(AccPrincipal) other;
			return loginResponse.getUserId()== that.getLoginResponse().getUserId();
		} else
			return false;
	}
	
	public LoginResponse getLoginResponse() {
		return loginResponse;
	}

	public void setLoginResponse(LoginResponse loginResponse) {
		this.loginResponse = loginResponse;
	}

	@Override
	public int hashCode() {
		return loginResponse.getUserId();
	}
}
AccRole.java:
package com.laws.acc.jaas;

import java.security.Principal;

public class AccRole implements Principal {

	@Override
	public String getName() {
		return "Acc User";
	}
	
	@Override
	public boolean equals(Object other) {
		if (other==null) return false;
		if(other==this) return true;
		if(other instanceof AccRole) {
			return this.getName().equals(((AccRole) other).getName());
		} else
			return false;
	}

	@Override
	public int hashCode() {
		return getName().hashCode();
	}
}

Tuesday 21 September 2010

Java Logging & Properties File

I prefer to have the properties file at the application level or JAR level if I am writing a library, with as many configuration entries in the same file as logically possible so that I end up with as few files as possible. So my properties file is at the root level of the JAR file.

To load the config.properties file at the root of the JAR:

// ResourceBundle expectes the file extension to be .properties by default.
ResourceBundle bundle = ResourceBundle.getBundle("config",
    Locale.getDefault(),
    Thread.currentThread().getContextClassLoader());
String dmsAddress = bundle.getString("dmsAddress");
Similarly when I use Java Logging I put the logging.properties file at the same location and load it upon application start up using LogManager so that I don't have to tamper with the properties file at JRE/lib.

The logging.properties file:

handlers = java.util.logging.ConsoleHandler
.level=INFO

java.util.logging.ConsoleHandler.level = INFO
com.laws.acc.level = ALL
com.laws.acc.ws.level = ALL
It is better to copy the JRE's logging.properties file and modify it. To load the logging configuration:
LogManager logMan=LogManager.getLogManager();
logMan.readConfiguration(Thread.currentThread().getClass().getResourceAsStream("/logging.properties"));

Resources: Smartly Load Your Properties