JAAS in Java2 1.4+ - authorization



Concept/intro

You should already have read the previous JAAS example and now be familier with the most fundamental JAAS issues. No need to repeat ourselves.

Authorization is about controlling permissions of the user, once logged on (user privileges). Maybe more J2EE, but it is also about controlling permissions of executing code (code privileges). Both parts are controlled in policy files. Both will be dealth with here.

Without a security manager (and policy files), all permissions are granted to all the code in the current directory. Without authentication, there is nobody specific to grant privileges. So requirements for JAAS authentication are:

JAAS "HelloWorld" authorization example

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

Authorization stripped down, consist of following parts: Additionally, you have a TopsecurityClientAuthorization.java - to initiate the actual JAAS login and call a .doAsPrivileged or .doAs method on the client Subject.

Summarizing what's new: policy file expanded, client calls additional method on the Subject, priviledged 'Action' class on the server.

Short about the JAAS program flow

Geenrally, the procedure is: The user is authenticated. On the client from the authenticated LoginContext, a Subject is retrieved. It's .doAsPrivileged method is called - with the priviledged Action code you want to run. Then JAAS takes over, checks user privileges with the policy file, throws exception if insufficient. Then the .run() method is executes - constantly checking code privileges in the policy file.

JAAS example source code

This downloadable example contains two client files, illustrating two situations: The two client source code files are very similar - except that the client with authorization includes the shaded part below.

Code part 1 TopsecurityClientAuthorization.java

package dk.topsecurity;

import java.io.*;
import java.util.*;
import java.security.*;
import javax.security.auth.login.*;
import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.spi.*;

/**
 * <p> This demonstrates use of the JAAS API, by attempting to
 * authenticate and then authorize a user. Results of the security 
 * check are then printed out on the commandline.
 * </p>
 */

public class TopsecurityClientAuthorization {

/**
 * Interacts with user via commandline and attempts to
 * authenticate the user and authorize them to perform
 * a priveleged action.<p>
 * 
 * @param args Commandline input arguments (there are none).
 */
	public static void main(String[] args) {

		LoginContext logctx = null;
		
//1ST PART - ONLY ASSOCIATED WITH AUTHENTICATION
	try {
/* Constructor for LoginContext is intialized with a string containing
 * classname for the LoginModule implementation as specified in the JAAS 
 * login configuration file. Second parameter is an object of type 
 * CallbackHandler, which will actually handle the authentication attempts.
 */
    		logctx = new LoginContext( "TopsecurityJAASModule", new TopsecurityCallbackHandler());
	} catch( LoginException le ) {
		System.err.println("\t[TopsecurityClient] LoginContext creation failed. "+ le.getMessage());
    		System.exit( -1 );
	} catch( SecurityException se ) {
		System.err.println("\t[TopsecurityClient] LoginContext creation failed. "+ se.getMessage());
    		System.exit(-1);
	}//end try/catch 

	try {
		logctx.login(); // Attempts to authenticate user and succeesfull if no exception is thrown
		System.out.println("\t[TopsecurityClient] Authentication succeeded.");
	} catch( LoginException le ) {
		System.err.println("\t[TopsecurityClient] Authentication failed. " + le.getMessage());
		System.exit( -1 );
	}//end try/catch

//2ND PART - ONLY ASSOCIATED WITH AUTHORISATION
	try {
		Subject subj = logctx.getSubject();		// Extract the Subject
		TopsecurityPrincipal.setDebug( false );		// not interested in what Principal is created
		PrivilegedAction action = new TopsecurityAction();	// Try executing TopsecurityAction as the authenticated Subject
		Subject.doAsPrivileged( subj, action, null );	// Go!
	    	System.out.println( "\t[TopsecurityClient] Authorization succeeded\n");
//3RD PART - DOING A GRACEFULL LOGOUT
		try {
			logctx.logout();
		} catch(LoginException le) {
			System.err.println("\t[TopsecurityClient] Logout: " + le.getMessage());
		}
 	} catch( Exception ex ) {
    		System.err.println("\t[TopsecurityClient] Authorization failed: " + ex.getMessage() + "\n" );
	}//end try/catch

    }//end main()
}//end main class

The priviledged piece of code is passed to the Subject and need to implementing the interface java.security.PrivilegedAction. Then its .run method is executed. The class used here is listed below:

Code part 2 TopsecurityAction.java

package dk.topsecurity;

import java.io.File;
import java.security.PrivilegedAction;

/**
 * <p>The class implements PrivilegedAction, which requires authentication
 * and authorisation through JAAS to be executed. Also the actions 
 * <code>System.getProprety</code> and file operations require a security
 * manager with proper security policy file definitions to be executed.</p>
 */
public class TopsecurityAction implements PrivilegedAction {

  /**
   * Privileged method
   */
  public Object run() {

    System.out.println( "Authorization successfull.\n\n" +
    			"Classpath property value is: "
                + System.getProperty("java.class.path" ) );


	File file = new File( "topsecurity.txt" );
	System.out.print( "\ntopsecurity.txt does " );
	    
	if( !file.exists() )
		System.out.print( "not " );
	System.out.println( "exist in the current working directory.\n" );
	return null;
	
  }//end Object run()
}//end class TopsecurityAction

Privleges for the Action class is determined by the policy file. Policy file is devided in two parts:

  • principal authentification privileges for the first 3 .jar files (topsecuritymodule.jar: LoginModule/CallbackHandler/Principal topsecurityauthorization.jar: TopsecurityClientAuthorization topsecurityauthentication.jar: TopsecurityClientAuthentication).
  • code authentification privileges for actions System.getProperty and File operations.


  • Code part 3 Topsecurity.policy
    
    /* Java 2 Access Control Policy for the JAAS Application */
    
    /* grant the sample LoginModule permissions */
    grant codebase "file:./topsecuritymodule.jar" {
        permission javax.security.auth.AuthPermission "modifyPrincipals";
    };
    
    grant codebase "file:./topsecurityauthorization.jar" {
    
       permission javax.security.auth.AuthPermission "createLoginContext.TopsecurityJAASModule";
       permission javax.security.auth.AuthPermission "doAsPrivileged";
    };
    
    grant codebase "file:./topsecurityauthentication.jar" {
    
       permission javax.security.auth.AuthPermission "createLoginContext.TopsecurityJAASModule";
       permission javax.security.auth.AuthPermission "doAsPrivileged";
    };
    
    
    /* User-Based Access Control Policy for the TopsecurityAction class
     * instantiated by TopsecurityClientAuthorization
     */
    grant codebase "file:./topsecurityaction.jar", Principal dk.topsecurity.TopsecurityPrincipal "user1" {
    
       permission java.util.PropertyPermission "java.class.path", "read";
       permission java.io.FilePermission "topsecurity.txt", "read";
    };
    

    Sure we have other classes TopsecurityCallbackHandler.java, TopsecurityLoginModule.java, TopsecurityPrincipal.java which are just listed for completeness. Not much happening here, which was not covered during the authentication example. The CallbackHandler just handles an additional credential...



    Code part 4 TopsecurityCallbackHandler.java
    
    package dk.topsecurity;
    
    import java.io.*;
    import java.util.*;
    import javax.security.auth.login.*;
    import javax.security.auth.*;
    import javax.security.auth.callback.*;
    
    /**
     * <p>This class implements the CallbackHandler so that it can handle the JAAS
     * login callback methods.</p>
     *
     */
    class TopsecurityCallbackHandler implements CallbackHandler {
    
        /**
         * Processes an array of callbacks to handle the login process.<p>
         *
         * @param callbacks an array of <code>Callback</code> objects containing
         *			the information requested by an underlying security
         *			service to be retrieved or displayed.
         * @exception java.io.IOException if an input or output error occurs. <p>
         * @exception UnsupportedCallbackException if the implementation of this
         *			method does not support one or more of the Callbacks
         *			specified in the <code>callbacks</code> parameter.
         * 
         * </p>
         */
        
        public void handle( Callback[] callbacks )
        	throws IOException, UnsupportedCallbackException {
          
    	for( int i = 0; i < callbacks.length; i++ ) {
    		if( callbacks[i] instanceof NameCallback ) {
    	 		NameCallback nc = (NameCallback)callbacks[i]; 
    	 		System.err.print( nc.getPrompt() ); // prompt the user for a username
    	 		System.err.flush();
    	 		nc.setName( (new BufferedReader
    				(new InputStreamReader(System.in))).readLine() );
     	    	} else if( callbacks[i] instanceof PasswordCallback ) {
    	 		PasswordCallback pc = (PasswordCallback)callbacks[i]; 
    	 		System.err.print(pc.getPrompt()); // prompt the user for sensitive information
    	 		System.err.flush();
    			String pw = (new BufferedReader
    				(new InputStreamReader(System.in))).readLine();
    			char[] passwd = new char[pw.length()];
    			    pw.getChars(0, passwd.length, passwd, 0);
    	 		pc.setPassword( passwd );
     	    	} else {
     			throw new UnsupportedCallbackException
     				( callbacks[i], "Unrecognized Callback" );
     	    }//end if/else
    	}//end for()
        }//handle()
    }//end class TopsecurityCallbackHandler
    

    Notice the added code to handle the password credential. This is not related to authentication - but only for authentication of the extra (password) credential. Also take a look at the TopsecurityLoginModule.java. Not much new here either.

    Code part 5 TopsecurityLoginModule.java
    
    package dk.topsecurity;
    
    import java.util.*;
    import java.io.IOException;
    import javax.security.auth.*;
    import javax.security.auth.callback.*;
    import javax.security.auth.login.*;
    import javax.security.auth.spi.*;
    
    /**
     * <p> LoginModule authenticates users, using a password.
     * 
     * <p> Includes a sequence (arrays) of users and their passwords. Indexes in the
     * arrays are the same for corresponding user and password.
     *
     * <p> On successfull authentication, a <code>TopsecurityPrincipal</code> object 
     * with the user name is added to the Subject.
     */
    public class TopsecurityLoginModule implements LoginModule {
    
    /** 
     * <p> LoginModule recognizes the debug option (in the 'initialize' method). 
     * If initialized true in the login Configuration, debug will be output to System.out.
     */
    	private boolean debug = false;
    
    	// validation objects
    	private Subject subject;
    	private TopsecurityPrincipal entity;
    	private CallbackHandler callbackHandler;
    	private Map sharedState;
    	private Map options;
    
    	// tracking authentication status
    	private static final int NOT = 0, OK = 1, COMMIT = 2;
    	private int status; 
    
    	// login info
    	private static final String[] userNames = { "guest", "user1", "user2" };
    	private static final String[] passwords = { "guest", "pass1", "pass2" };
    	
    	// current user
    	private String username;
    	private char[] password;
    	
    
        /**
         * Initializes the LoginModule.<p>
         *
         * @param subject the Subject to be authenticated as provided through the JAAS interface. <p>
         * @param callbackHandler a CallbackHandler for retrieving username and password from the user <p>
         * @param sharedState shared LoginModule state. <p>
         * @param options options specified in the login Configuration for this particular LoginModule 
         *   (in the java.security.auth.login.config file).
         */
        public void initialize(Subject subject, CallbackHandler callbackHandler,
    			Map sharedState, Map options) {
     
    		status = NOT;
    		this.subject = subject;
    		this.callbackHandler = callbackHandler;
    		this.sharedState = sharedState;
    		this.options = options;
    	
    		// sets options according to the java.security.auth.login.config file
    		TopsecurityPrincipal.setDebug( "true".equalsIgnoreCase((String)options.get("debug")) );
    
        }//end initialize()
    
    
        /**
         * Simple console trace to system.out for debug purposes only.
         * <p>
         *
         * @param msg the message to be printed to the console
         *
         */
        private void debugOut(String msg) {
    	if( debug )
    		System.out.println("\t[TopsecurityLoginModule] " + msg);
        }
    
        /**
         * Authenticate the user based on a username and password. 
         * If the combination of user name/password occurs anywhere in the list,
         * authentication succeeds - otherwise not.
         *
         * <p>
         *
         * @return true in all cases since this LoginModule should not be ignored.
         * @exception FailedLoginException if authentication fails. <p>
         * @exception LoginException if this LoginModule is unable to perform the authentication.
         */
        public boolean login() throws LoginException {
    
    		// prompt for a user name and password
    		if( callbackHandler == null )
    		    throw new LoginException( "Error: no CallbackHandler available to retrieve user credentials");
    	
    		Callback[] callbacks = new Callback[2];
    		callbacks[0] = new NameCallback( "\nuser name: " );
    		callbacks[1] = new PasswordCallback( "password: ", false );
    	 
    		try {
    		    callbackHandler.handle(callbacks); //get the user credentials
    		    username = ((NameCallback)callbacks[0]).getName();
    		    char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
    		    if (tmpPassword == null) // treat a NULL password as an empty password
    			tmpPassword = new char[0];
    		    password = new char[tmpPassword.length];
    		    System.arraycopy( tmpPassword, 0,password, 0, tmpPassword.length );
    		    ((PasswordCallback)callbacks[1]).clearPassword(); //wipe out occurrences in memory
    		} catch( java.io.IOException ioe ) {
    		    throw new LoginException(ioe.toString());
    		} catch( UnsupportedCallbackException uce ) {
    		    throw new LoginException( "Error: " + uce.getCallback().toString() +
    				" not available to authenticate user." );
    		}//end try/catch
    
    		// verify the username/password
    		String passwordString = new String( password );
    	
    		debugOut("user entered user name: " + username );
    		debugOut("user entered password: " + passwordString + "\n");
    		
    		for( int i = 0; i < userNames.length; i++ ) {
    			if( username.equals( userNames[i] ) && passwordString.equals( passwords[i] ) ) {
    		
    			    // because the username/passwords matches in list, authentication succeeded!!!
    			    debugOut("User #" + i + " authentication succeeded." );
    			    status = OK;
    			    return true;
    			} else {
    		
    			    // authentication failed -- clean out state
    			    debugOut("User #" + i + " authentication failed." );
    			}//end if/else
    		}//end for(;;)
    		return false;
        }//end login()
    
        /**
         * <p> Method called if the LoginContext's
         * overall authentication succeeded (the relevant REQUIRED, 
         * REQUISITE, SUFFICIENT and OPTIONAL LoginModules
         * mentioned in file java.security.auth.login.config.. did succeed).
         *
         * <p> If this LoginModule's authentication succeeded 
         * (status stored in the variable 'status' by the
         * .login() method), then the .commit() method associates a
         * TopsecurityPrincipal with the Subject located in the
         * LoginModule. If this LoginModule's authentication failed, 
         * any state originally saved is removed.
         *
         * <p>
         *
         * @exception LoginException if the commit fails.
         * @return true if this LoginModule's own .login() and .commit()
         *		attempts succeeded, false otherwise.
         */
        public boolean commit() throws LoginException {
    		if(status == NOT || subject == null) {
    		    return false;
    		} else {
    		    // add a Principal (authenticated identity) to the Subject
    	
    		    // assume the user we authenticated is the TopsecurityPrincipal
    		    entity = new TopsecurityPrincipal(username);
    		    Set entities = subject.getPrincipals();
    		    if( !entities.contains(entity) )
    				entities.add(entity);
    	
    		    debugOut("added TopsecurityPrincipal to Subject");
    	
    		    // in any case, clean out state
    		    username = null;
    		    password = null;
    	
    		    status = COMMIT;
    		    return true;
    		}//end if
        }//end commit()
    
        /**
         * <p> This method is called if the LoginContext's
         * overall authentication failed.
         * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
         * mentioned in file java.security.auth.login.config.. did not succeed).
         *
         * <p> If this LoginModule's authentication succeeded 
         * (status stored in the variable 'status' by the
         * .login() method and .commit() methods),
         * then the .abort() method cleans up any state that was originally saved.
         *
         * <p>
         *
         * @exception LoginException if the abort fails.
         * @return false if this LoginModule's own login and/or commit attempts
         *		failed, true otherwise.
         */
        public boolean abort() throws LoginException {
    		if(status == NOT) {
    		    return false;
    		} else if( status == OK ) {
    		    // login succeeded but overall authentication failed
    		    username = null;
    		    if( password != null ) password = null;
    		    entity = null;
    		} else {
    		    // overall authentication succeeded and commit succeeded,
    		    // but someone else's commit failed
    		    logout();
    		}//end if/else
    		status = NOT;
    		return true;
        }//end abort()
    
        /**
         * Logout of the user.
         *
         * <p>Removes the TopsecurityPrincipal added by the .commit() method.<p>
         *
         * @exception LoginException if the logout fails.
         * @return true in all cases since this LoginModule should not be ignored.
         */
        public boolean logout() throws LoginException {
    
    		subject.getPrincipals().remove(entity);
    		status = NOT;
    		username = null;
    		if( password != null ) password = null;
    		entity = null;
    		return true;
        }//end logout()
    }//end class TopsecurityLoginModule
    

    The principal is exactly the same except for a few added methods, like .hash for the hashcode implementation..

    Code part 6 TopsecurityPrincipal.java
    
    package dk.topsecurity;
    
    import java.security.Principal;
    
    /**
     * <p> TopsecurityPrincipal class implements the 
     * java.security.Principal interface and represents a user, who is
     * successfully authenticated in the LoginModule..
     *
     * <p> Principals such as this <code>TopsecurityPrincipal</code>
     * may be associated with a particular <code>Subject</code>
     * to augment that <code>Subject</code> with an additional
     * identity. Authorization decisions can then be based upon 
     * the Principals associated with a <code>Subject</code>.
     * 
     * @see java.security.Principal
     * @see javax.security.auth.Subject
     */
    public class TopsecurityPrincipal implements Principal, java.io.Serializable {
    
        /**
         * Debug Flag
         */
        private static boolean DEBUG;
        
        /**
         * The name of the principal
         */
        private String name;
    
        /**
         * Create TopsecurityPrincipal representing user with username provided.<p>
         *
         * @param name the username for this user.
         * @exception NullPointerException if the <code>name</code> is <code>null</code>.
         */
        public TopsecurityPrincipal( String name ) {
    	if( name == null )
    	    throw new NullPointerException( "illegal null input" );
    	
    	this.name = name;
    	if( DEBUG ) System.out.println( "\t[TopsecurityPrincipal] Principal " +
    			 name + " successfully created." );
        }//end TopsecurityPrincipal( String )
    
        /**
         * Returns username for this <code>TopsecurityPrincipal</code>.<p>
         *
         * @return the username for this <code>TopsecurityPrincipal</code>
         */
        public String getName() {
    		return name;
        }//end getName()
    
        /**
         * Toggles debug status on or off.<p>
         * 
         * @param debug flag to set the debug status
         */
        public static void setDebug( boolean debug ) {
    		DEBUG = debug;	
        }//end setDebug( boolean )
    	
        /**
         * Return a string representation of this <code>TopsecurityPrincipal</code>.<p>
         *
         * @return a string representation of this <code>TopsecurityPrincipal</code>.
         */
        public String toString() {
    	return( "TopsecurityPrincipal:  " + name );
        }//end toString()
    
        /**
         * Compares the specified Object with this <code>TopsecurityPrincipal</code>.
         * True if the given object is also a <code>TopsecurityPrincipal</code> 
         * with the same username.<p>
         *
         * @param o Object to be compared for equality with .this
         * @return true if the specified Object is equal equal to .this
         */
        public boolean equals( Object o ) {
    	if( o == null )
    	    return false;
    	if( this == o )
    	    return true;
    	 
    	if( !(o instanceof TopsecurityPrincipal) )
    	    return false;
    	TopsecurityPrincipal that = (TopsecurityPrincipal)o;
    	
    	if( this.getName().equals(that.getName()) )
    	    return true;
    	return false;
        }//end equals()
     
        /**
         * Return a hash code for this <code>TopsecurityPrincipal</code>.<p>
         *
         * @return a hash code for this <code>TopsecurityPrincipal</code>.
         */
        public int hashCode() {
    	return name.hashCode();
        }//end hashCode()
    }//end class TopsecurityPrincipal
    

    Done with the source code. Finally to the configuration part. The Login config file is exactly the same since we use the same name for the LoginModule and the JAAS module name. In this configuration file, the entry is named TopsecurityJAASModule, which is the name that TopsecurityClient.java uses to refer to this entry. The entry specifies that the LoginModule should be used to perform the authentication. This module is required in order for the authentication to be considered successful. It is successful if the user enters correct credentials according to what is expected in the LoginModule.

    Code part 7 TopsecurityLogin.config
    
    /** Login Configuration File for JAAS Sample App **/
    
    TopsecurityJAASModule {
       dk.topsecurity.TopsecurityLoginModule required debug=true;
    };
    

    And a build file to get things assembled. Not running as -jar this time. Now using a number of .jar files - each with individually set privileges in the policy file.

    Code part 8 build.xml
    
    <project name="javacert" default="all" basedir=".\">
    
      <target name="clean">
        <delete quiet="true" dir=".\build"/>
        <delete quiet="true" dir=".\javadoc"/>
        <mkdir dir="./build"/>
      </target>
    
      <target name="compile">
        <javac srcdir="./source" destdir=".\build"/>
      </target>
    
      <target name="package">
    
        <jar destfile="./topsecurityaction.jar" basedir="./build" includes="dk/topsecurity/TopsecurityAction.class"/>
        <jar destfile="./topsecuritymodule.jar" basedir="./build" includes="dk/topsecurity/TopsecurityLoginModule.class dk/topsecurity/TopsecurityPrincipal.class dk/topsecurity/TopsecurityCallbackHandler.class"/>
        <jar destfile="./topsecurityauthorization.jar" basedir="./build" includes="dk/topsecurity/TopsecurityClientAuthorization.class"/>
        <jar destfile="./topsecurityauthentication.jar" basedir="./build" includes="dk/topsecurity/TopsecurityClientAuthentication.class"/>
    
      </target>
    
      <target name="all" depends="clean,compile,package">
      </target>
    </project>
    

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



    Compile



    Running the JAAS authentication - while typing correct username and password, according to what is recorded in the LoginModule:

    Run successfull

    Running the JAAS authorization - - while typing correct username and password, according to what is recorded in the LoginModule:

    Run unsuccesfull


    Maybe you could try creating the "topsecurity.txt" file and run the client again...

    Conclusion

    The JAAS is a set of APIs that enable services to authenticate and enforce access controls on principals (users/processes). JAAS authentication is pluggable in the respect that another LoginModule may be specified in the policy file - and the client application code will authenticate according to other security rules without code re-compilation. In this way the Java Java applications is not dependent of underlying authentication mechanism, and JAAS authorization provides a neat "wrapper" without interfering significantly with the actual application implementation. It's configurable and startup-parameter controlled.

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

    Ressource: Sun JAAS tutorial article