JAAS in Java2 1.4+ - JAAS RMI Server Security



Concept/intro

You should already have read the previous JAAS authentication example and now be familier with the most fundamental JAAS issues. Not much is repeated here though. It's just to get familier with the coding style.

This example is about securing a Java RMI server - authentication and authorization between multiple JVMs - in a client/server solution. The general idea is to let a client perform a JAAS authentication towards a RMI server - followed by authorisation-controlled execution of priviledged code on the server!

JAAS over RMI may be implemented in a number of slightly different ways. As the old JavaOne 2000 presentation explains, in one way or another you will need to deal with a proxy concepts when executing privileged code on the server. This also means there is no single truth about things - and you need to be carefull to implement a secure solution in many other aspects.

JAAS "HelloWorld" RMI server example

All source code here may be downloaded from this jaas_rmi.zip example. Includes ANT script. Compile and run from commandline.

Short about the JAAS program flow

JAAS now moves to the server. The client just contacts the server and receives a dumb stub, if the login authenticates successfull. The server knows the clients identity through the stub - and only allows actions, which the client is authorized for.

So what is really happening is that JAAS is carried out like before - only through a proxy stub on the server this time.

JAAS RMI secure server considerations

There are some problems, which are difficult to overcome. One is that the server publishes everything available for calling. Any client may bind to the registry, obtain a reference to an object - and call any methods on it. There is no configurable security to prevent this. The solution is to let the client call a proxy on the server, and controll security in this proxy. But the simple fact of exposing functionality not available for everybody is not smart.

JAAS RMI authentication

First have a look at authentication.

The client simple looks up the RMI server, calls a .login method. You find no JAAS on the client. All JAAS is handled on the server. After successfull authentication, a server stub is returned. This stub allows calling authorization-restricted methods. But JAAS on the server decides what to allow.



Code part 1 TopsecurityClient.java

package dk.topsecurity.client;

import java.rmi.Naming;

import dk.topsecurity.common.TopsecurityLoginInterface;
import dk.topsecurity.common.TopsecurityServerInterface;

/**
 * Test client performing authentication and testing authorisation.
 */
public class TopsecurityClient {

	static boolean debug = true;

	/**
	 * Simple console trace to system.out for debug purposes only.&Ltp>
	 *
	 * @param msg the message to be printed to the console
	 */
	private static void debugOut(String msg) {
	if( debug )
		System.out.println("\t[TopsecurityClient] " + msg);
	}

	/**
	 * Requires user arguments from commandline and attempts to
	 * authenticate the user and authorize to perform
	 * a priveleged action.&Ltp>
	 * 
	 * @param args Commandline input arguments
	 */
	public static void main(String[] args) {
		String server,user,pass;

	if (args.length!=2 && args.length!=3) {
		System.out.println("TopsecurityClient &Ltusername password> [server]");
	} else {
		if (args.length==2) {
			user = args[0];
			pass = args[1];
			server = "127.0.0.1:6000";
    		} else {
			user = args[0];
			pass = args[1];
			server = args[3];
		}

	//connect to the server through RMI
	String serverObject = "rmi://" + server + "/" + "TopsecurityRemoteLoginServer";
	debugOut("connecting to server="+serverObject);
	TopsecurityLoginInterface loginServer = null;
	try {	
//authenticate credentials
		loginServer = (TopsecurityLoginInterface) Naming.lookup(serverObject);
//login object received
    	}
    	catch (Exception e) {
      		System.out.println(e);
      		System.exit(0);
    	}
//do the login 
    	try {
		debugOut("ATTEMPTING LOGIN");
		TopsecurityServerInterface theServer = (TopsecurityServerInterface) loginServer.login(user, pass);
		debugOut("LOGIN SUCCESSFULL");
//perfom all priviledged operations
		try {
        		debugOut("ATTEMPTING 1st Operation");
        		theServer.doOperationA();
			debugOut("SUCCESSFULL 1st Operation");
		}
		catch (Exception e) {
			System.out.println(e);
		}
		try {
			debugOut("ATTEMPTING 2nd Operation");
        		theServer.doOperationB();
			debugOut("SUCCESSFULL 2nd Operation");
		}
		catch (Exception e) {
        		System.out.println(e);
      		}
	}
	catch (Exception e) {
		debugOut("error : "+e);
		e.printStackTrace();
		System.exit(0);
	}
	}
  }
}

loginServer.login executes TopsecurityLoginImpl.login.

Here, actual security is handled on the server by the class TopsecurityLoginImpl.java. It creates the LoginContext known from previous client examples, performs a full JAAS login according to permissions in policy file for the server. Obtains the subject to be used later for checking authorization to the methods, which the client might want to execute.



Code part 2 TopsecurityLoginImpl.java

package dk.topsecurity.server;

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

import dk.topsecurity.common.TopsecurityLoginInterface;
import dk.topsecurity.common.TopsecurityServerInterface;

/**
 * Implements the server object that allows clients to login.
 */
public class TopsecurityLoginImpl
  extends java.rmi.server.UnicastRemoteObject
  implements TopsecurityLoginInterface
{
  /** The real server object 
   */
  private TopsecurityServerInterface myServer;

  /**
   * Class constructor.
   *
   * @param theServer The real server object.
   */
  public TopsecurityLoginImpl(TopsecurityServerInterface theServer)
    throws java.rmi.RemoteException {
    myServer = theServer;
  }

  /**
   * Allows a client to login and get an interface to the server.
   */
  public TopsecurityServerInterface login(String username, String password)
    throws java.rmi.RemoteException, LoginException {

	LoginContext lc = new LoginContext("TopsecurityJAASLogin", new RemoteCallbackHandler(username, password));
	lc.login();
	Subject user = lc.getSubject();

    // Return a reference to a proxy object that encapsulates the access
    // to the server, for this client
    return new TopsecurityServerProxy(user, myServer);
  }
}


class RemoteCallbackHandler implements CallbackHandler {
	private String username;
	private String password;
	
	RemoteCallbackHandler(String username, String password){
		this.username = username;
		this.password = password;
	}
	public void handle(Callback[] cb) {
    	for (int i = 0; i < cb.length; i++){
			if (cb[i] instanceof NameCallback){
				NameCallback nc = (NameCallback)cb[i];
				nc.setName(username);
			} else if (cb[i] instanceof PasswordCallback){
				PasswordCallback pc = (PasswordCallback)cb[i];
				pc.setPassword(password.toCharArray());
				password = null;
			}
		}
	}
}

As usual, the server JAAS authentication uses the TopsecurityLoginModule.java implementation.


Code part 3 TopsecurityLoginModule.java

..completely the same compared to previous example...

and login configuration...

Code part 4 TopsecurityLogin.config

..completely the same compared to previous example...

The client is authenticated according to implemented rules in the LoginModule. After successfull authentication of the user, a TopsecurityLoginPrincipal containing the username is added internally to the LoginContext. As usual, TopsecurityLoginPrincipal is a simple class derived from java.security.Principal. A TopsecurityServerProxy object containing a reference to login credentials is created and returned to the client.

This concludes authorisation on the server.

JAAS RMI authorization

Operating with two policy files: The java.security.auth.policy is actually deprecated as of JDK 1.4+ but let that rest for a moment.

Principle authorisation is enforced through use of the javax.security.auth.Subject.doAs method, which allows a piece of code to be executed with the privileges of a specific principal. The piece of code must be an object, which implements either java.security.PrivilegedAction or java.security.PrivilegedExceptionAction (latter allows retrieval of exception thrown during the .run(..) method). In all cases, the .run(..) method is executed with privileges as the user.



Code part 5 TopsecurityServerProxy.java

package dk.topsecurity.server;

import javax.security.auth.Subject;

import dk.topsecurity.common.TopsecurityServerInterface;
import dk.topsecurity.permissionvalidation.TopsecurityMethodcallValidate;

import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import dk.topsecurity.permissions.TopsecurityServerPermission;


/**
 * Proxy imlementing server interface.
 * 
 * Protects the server from direct calls by clients. All calls 
 * passes security check and the server implementation is not 
 * visible for the client.
 */
public class TopsecurityServerProxy extends java.rmi.server.UnicastRemoteObject implements TopsecurityServerInterface {

  /** A reference to the real server object 
   */
  private TopsecurityServerInterface theServer;

  /** The user associated with this proxy 
   */
  private Subject         theUser;

  /**
   * Class constructor. Saves subject generated during JAAS 
   * .login() and a reference to the active server.
   *
   * @param user A subject representing the user for this proxy.
   * @param theServer The real server object.
   */
  public TopsecurityServerProxy(Subject user, TopsecurityServerInterface theServer) throws java.rmi.RemoteException {
    this.theServer = theServer;
    this.theUser      = user;
  }

  /**
   * Proxy implementation of (1st) method in the server interface.
   * 
   * The client calls this method. If he client has the
   * appropriate permissions, the call goes through.
   */
  public void doOperationA() throws java.rmi.RemoteException, SecurityException {
    checkPermission("doOperationA");
    theServer.doOperationA();
  }

  /**
   * Proxy implementation of (2nd) method in the server interface.
   * 
   * The client calls this method. If he client has the
   * appropriate permissions, the call goes through.
   */
  public void doOperationB() throws java.rmi.RemoteException, SecurityException {
    checkPermission("doOperationB");
    theServer.doOperationB();
  }


  /**
   * Check if the current client can call a certain method.
   * The check is made through JAAS and its policy file.
   *
   * @param methodName The method that will be called.
   * @throws SecurityException If the client doesn't have the necessary
   * permissions.
   */
  private void checkPermission(String methodName) throws SecurityException {
    // Assume the identity of the user, and validate if he can
    // call this method
    try {
      Subject.doAs(theUser, new TopsecurityMethodcallValidate(methodName));
    }
    catch (java.security.PrivilegedActionException e) {
      throw (SecurityException) e.getException();
    }
  }
}

When javax.security.auth.Subject.doAs(user, new ValidateMethodCall(methodName)) is performed, the .run(..) method in ValidateMethodCall is executed with privileges for user as specified in policy file java.security.auth.policy. The method .run(..) does a java.security.AccessController.checkPermission(..) check on the permission to execute a method name. This check consults the policy file and cause an exception for no privileges. java.security.AccessController performs security checks according to the current context. When installing a security manager in Java, calls are normally propagated to the AccessController class.

Code part 6 ValidateMethodCall.java

package dk.topsecurity.permissionvalidation;

import java.security.AccessController;
import java.security.PrivilegedExceptionAction;

import dk.topsecurity.permissions.TopsecurityServerPermission;

/**
 * Assures that the a certain method can be called in the context
 * of the running code.
 */
public class TopsecurityMethodcallValidate implements PrivilegedExceptionAction {

  /** The method to be called */
  private String priveledgedMethodName;

  /**
   *  Make sure that the current user (defined by its context) has
   *  the permissions to call the "methodName" method. For checking
   *  this, a ServerPermission is required.
   *
   *  authrmi.permissions.ServerPermission "methodName"
   *    -> authorizes the call of a certain method
   *  authrmi.permissions.ServerPermission "*"
   *    -> authorizes the call of all methods
   */
  public TopsecurityMethodcallValidate(String methodName) {
    priveledgedMethodName = methodName;
  }

  public Object run() {
    // Only has to check if the appropriate ServerPermission is owned by
    // the user. If not an exception is thrown.
    AccessController.checkPermission(new TopsecurityServerPermission(priveledgedMethodName));
    return null;
  }
}

The class TopsecurityServerPermission represents permissions. The class extends java.security.BasicPermission. When JAAS is parsing the policy file, it automatically instantiates objects of this class for representing the permissions for users.



Code part 7 TopsecurityServerPermission.java

package dk.topsecurity.permissions;

/**
 * Permission to call a method on the server.
 *
 * The functionality is already provided in the base class
 * java.security.BasicPermission. Thus, all that this class 
 * is doing is serving as a type name.
 *
 * Permissions to call methods are specified as follows:
 *
 *  authrmi.permissions.ServerPermission "methodName"
 *    -> authorizes the call of a certain method
 *  authrmi.permissions.ServerPermission "*"
 *    -> authorizes the call of all methods
 */
public class TopsecurityServerPermission extends java.security.BasicPermission {

  /**
   * Creates a permission with a name.
   */
  public TopsecurityServerPermission(String name) {
    super(name);
  }

  /**
   * Creates a permission with a name and an action string.
   * The action string is not used, but this constructor must exist
   * so that the policy file parser works.
   */
  public TopsecurityServerPermission(String name, String actions) {
    super(name, actions);
  }
}

If .checkPermissions(..) succeeds, the actually priviledged methods (.doOperation) are called in the server implementation:

Code part 8 TopsecurityServerImpl.java

package dk.topsecurity.server;

import dk.topsecurity.common.TopsecurityServerInterface;

/**
 * The actual implementation of the server.
 */
public class TopsecurityServerImpl implements TopsecurityServerInterface {
  /**
   * The first priviledged operation.
   */
  public void doOperationA()
  {
    System.out.println("Operation A!");
  }

  /**
   * The second priviledged operation.
   */
  public void doOperationB()
  {
    System.out.println("Operation B!");
  }
}

To summarize it all:

The client calls a .doOperationX method on the proxy. Before priviledged code are actually executed on the server, a check Subject.doAs(theUser, new TopsecurityMethodcallValidate(".doOperationX")) is performed on the server. This check performs a AccessController.checkPermission(new TopsecurityServerPermission(".doOperationX")) in the context of the user - checking permissions in policy file. If check fails, an exception is thrown and the actual priviledged code is never reached.

Remaining server code, is the piece which actually starts the server on a physical port and place it under a name in the registry.



Code part 9 TopsecurityServer.java

package dk.topsecurity.server;

import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Date;

import dk.topsecurity.common.TopsecurityLoginInterface;
import dk.topsecurity.common.TopsecurityServerInterface;

/**
 * The main class for the server.
 */
public class TopsecurityServer {

  public static void main(String[] args)
  {
    /* Ensures that the identifiers generated for the server objects
     * will be secure
     */
    System.setProperty("java.rmi.server.randomIDs", "true");

    /* Binds the server object with the login interface to the registry */
    try
    {
      TopsecurityServerInterface theServer  = new TopsecurityServerImpl();
      TopsecurityLoginInterface loginObject = new TopsecurityLoginImpl(theServer);

      Registry loginRegistry =
        LocateRegistry.createRegistry(6000);
      loginRegistry.bind("TopsecurityRemoteLoginServer", loginObject);

      System.out.println((new Date()) + ": Server up and running");
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Remaining is just server interfaces, allowing the client access to the server implementation.



Code part 10 TopsecurityLoginInterface.java

package dk.topsecurity.common;

import javax.security.auth.login.LoginException;


/**
 * Client interface for for server login.
 */
public interface TopsecurityLoginInterface extends java.rmi.Remote
{
  /**
   * Method allowing login and, returning interface to the server.
   *
   * @param username The name of the user.
   * @param password The password of the user.
   * @return A reference to a proxy of the server object.
   * @throws SecurityException If the client is not allowed to login.
   */
  public TopsecurityServerInterface login(String username, String password)
    throws java.rmi.RemoteException, LoginException;
}

and

Code part 11 TopsecurityServerInterface.java

package dk.topsecurity.common;

/**
 * Server interface
 */
public interface TopsecurityServerInterface extends java.rmi.Remote {
  /**
   * 1st priviledged operation
   *
   * @throws SecurityException when client doesn't have proper permissions
   * for executing this method.
   */
  public void doOperationA() throws java.rmi.RemoteException, SecurityException;

  /**
   * 2nd priviledged operation.
   *
   * @throws SecurityException when client doesn't have sufficient permissions
   * for executing this method.
   */
  public void doOperationB() throws java.rmi.RemoteException, SecurityException;
}

And the ant script to build it all:

Code part 13 build.xml

<project name="javacert" default="all" basedir=".\">

  <target name="clean">
    <delete quiet="true" dir=".\build"/>
    <delete quiet="true" dir=".\javadocs"/>
    <mkdir dir="./build"/>
  </target>


  <target name="compile">
    <javac srcdir="./source" destdir=".\build" classpath="dk"/>
    <rmic classname="dk.topsecurity.server.TopsecurityLoginImpl" base="./build" classpath="."/>
    <rmic classname="dk.topsecurity.server.TopsecurityServerProxy" base="./build" classpath="."/>
  </target>

  <target name="generatedocs">
    <javadoc 
           destdir="javadoc"
           author="true"
           version="true"
           use="true"
           windowtitle="API">

    <fileset dir="./source" defaultexcludes="yes" include="client/**/*.java common/**/*.java server/**/*.java"/>

    <doctitle><![CDATA[<h1>SunJavaDeveloperCertificationAssignment<br>FlyByNight project</h1>]]></doctitle>
    <bottom><![CDATA[<i>Generated 2003 by StigValentini for the Sun Java Developer Certification © Sun Corp. </i>]]></bottom>
    <tag name="todo" scope="all" description="To do:" />
    <group title="Client Package" packages="suncertify.client.*"/>
    <group title="Server Package" packages="suncertify.server.*"/>
    <group title="Database Package" packages="suncertify.db.*"/>
    <link offline="true" href="http://java.sun.com/products/jdk/1.2/docs/api/" packagelistLoc="C:\tmp"/>
    <link href="http://developer.java.sun.com/developer/products/xml/docs/api/"/>
  </javadoc>  </target>


  <target name="package">
    <jar destfile="./actions.jar" basedir="./build" includes="dk\topsecurity\permissionvalidation\*.class"/>

    <jar destfile="./client.jar" basedir="./build" includes="dk\topsecurity\client\*.class"/>

    <jar destfile="./server.jar" basedir="./build" includes="dk\topsecurity\server\*.class"/>
    <jar destfile="./server.jar" basedir="./build" includes="dk\topsecurity\permissions\*.class" update="true"/>

    <jar destfile="./common.jar" basedir="./build" includes="dk\topsecurity\common\*.class"/>

    <jar destfile="./server_stub.jar" basedir="./build" includes="dk\topsecurity\server\*_Stub.class"/>
  </target>

  <target name="all" depends="clean,compile,package">
  </target>
</project>

Put enerything into a temporary directory (using correct dir structure) and compile:



Compile



Starting up the registry:

Background RMI server

Starting up the server:

Run server


Running the client:

Run client


Traces on the server after client running:

Server output


Conclusion

JAAS RMI secure server is very similar to JAAS other places. The only change is that the client is calling a proxy on the server - which is performing the actual JAAS checks on behalf of the client.

When you are operating with a client/server solution you will almost always find yourself working with proxies.

This example has some advantages over the example in chapter 8, part 4 of the book J2EE Security by Pankaj Kumar. Here the "proxy" is operating more or less entirely a client concept. The server sends back a stub for ServerImpl which is enrolled in a proxy on the client. His scope is different.. but I don't completely approve of his strategy anyhow. But read and make your own opinion.

/www.topsecurity.dk (2004-1-24)