How To/Walkthrough: JAAS and jBoss Security.
=============================================
=============================================
by Edward Kenworthy with much help from Oleg Nitz, Toby Allsopp and Rickard
Oberg

Introduction:
=============

There are two security models supported by jBoss: JAAS and not-JAAS. By
default, out-of-the zip file, jBoss is setup to use not-JAAS  security,
which consists of five classes:


ClientLoginModule } both sub-classes of LoginModule.
ServerLoginModule }
RealmMapping
SecurityManager

ClientLoginModule is used on the client and is used to "login" to the
AppServer. ServerLoginModule is used by the server and performs user
authentication and validation. RealmMapping maps users to roles, the
SimpleRealMapping class simply maps role to username and finally
SecurityManager brings these various server elements together. All of the
server side authentication and validation only happens when you try  to
invoke a method on a secure EJB bean and the SecurityInterceptor is invoked
by the container.

The JAAS version is very similar, but in someways more complicated and
others less so. ClientLoginModule is identical. However the other  classes
are all somewhat different because JAAS defines a set of classes and
interfaces to be used to model the user, their roles and their  passwords.
Roughly these translate as JAAS subject, principals and private credentials.

What I am going to describe here is how you can enhance the existing JAAS
security classes to the minimum required to give a realistic level  of
security (the default is rather simplistic and assumes if username ==
password then its a value subject with the principal of 'user'. Not
actually that useful). So what I will describe is the *minimum* you need to
do in order to extend this so that you can define users and  passwords and
those user's roles using a pair of properties files of the following format:

users.properties:
<username1>=<password1>
<username2>=<password2>
etc

roles.properties:
<username1>=<role1>,<role2>,<role3> etc
<username2>=<role21,<role22> etc
etc

This involves writing only ONE new class, which is deployed on the server,
and integrating it with jBoss. it does require some changes to the  client
and server configurations.

I will use the CD example available for jBoss as my example code, although
most of the actual code is generic.

Secure CD Catalogue
===================

I'm assuming you've installed jBoss in c:\jboss, as it suggests in the
documentation. If you haven't then substitute your install directory  for
c:\jboss. Also, I am using Windows NT but I don't believe there should be
anything different for Unix (other than the usual backslash,  forward slash
thing).

The following classes are involved:

ClientLoginModule - unchanged
CDServerLoginModule - all the code changes happen here
JaasSecurityManager - unchanged but see comments later. (JaasSecurityManager
is both RealmMappingService and SecurityManagerService).

Firstly you need to setup your client to use the ClientLoginModule, the
simplest way to do this is to add the following to your run command  line:
-Djava.security.auth.login.config=c:\jboss\client\auth.conf

This points it to the default client auth.conf that comes with jBoss and
which in turn points to the ClientLoginModule.

Then to login in the client you need to include the relevant security
classes:

import javax.security.auth.login.*;
import javax.security.auth.callback.*;

and then implement the following code in your class that does the login:

        try
        {
                LoginContext yuleLogin = new LoginContext("TestClient", new
AppCallbackHandler(name, password)); // It's Christmas ;-)
                System.out.println("Created LoginContext.");
                yuleLogin.login();
                // If you havent set
-Djava.security.auth.login.config=c:\jboss\client\auth.conf then you'll get
an exception here.
        }
        catch (LoginException le)
        {
                System.out.println("Login failed :(");
                le.printStackTrace();
        }
        try
        {
                jndiContext = new InitialContext();
                System.out.println("Got context");
        }
        catch (Exception e)
        {
                e.printStackTrace();
        }

The scope of this login is the JVM.

You'll also need this class, which I defined as an inner class but you don't
have to.

        class AppCallbackHandler implements CallbackHandler
        {
                private String _username;
                private char[] _password;

                public AppCallbackHandler(String username, char[] password)
                {
                        _username = username;
                        _password = password;
                }

                public void handle(Callback[] callbacks) throws
java.io.IOException, UnsupportedCallbackException
                {
                        for (int i = 0; i < callbacks.length; i++)
                        {
                                if (callbacks[i] instanceof
TextOutputCallback)
                                {
                                        // display the message according to
the specified type
                                        TextOutputCallback toc =
(TextOutputCallback)callbacks[i];
                                        switch (toc.getMessageType())
                                        {
                                                case
TextOutputCallback.INFORMATION:
        
System.out.println(toc.getMessage());
                                                        break;
                                                case
TextOutputCallback.ERROR:
        
System.out.println("ERROR: " + toc.getMessage());
                                                        break;
                                                case
TextOutputCallback.WARNING:
        
System.out.println("WARNING: " + toc.getMessage());
                                                        break;
                                                default:
                                                      throw new
IOException("Unsupported message type: " + toc.getMessageType());
                                        } // switch
                                }
                                else if (callbacks[i] instanceof
NameCallback)
                                {
                                        NameCallback nc =
(NameCallback)callbacks[i];
                                        nc.setName(_username);
                                }
                                else if (callbacks[i] instanceof
PasswordCallback)
                                {
                                        PasswordCallback pc =
(PasswordCallback)callbacks[i];
                                        pc.setPassword(_password);
                                }
                                else
                                {
                                        throw new
UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
                                }
                        } // for
                } // handle
        } // AppCallbackHandler class


Some notes: The class LoginContext also allows you to define just a username
and password in its ctor, however this will generate an  exception when you
call it's login method as the jBoss implementation exclusively uses
callbackhandlers (hence the need for  AppCallbackHandler). Secondly, the no
real authentication is done at this stage, so entering an invalid
username/password will succeed at  this point. Thirdly, you'll notice my
braces are all in the correct position ;-)

Now whenever you try and invoke a method on an EJB bean, eg:

    try
    {
      Object ref =
ApplicationController.jndiContext.lookup("cd/CDCollection");
      CDCollectionHome home = (CDCollectionHome)
PortableRemoteObject.narrow(ref, CDCollectionHome.class);
      collection = home.create();
/*      ^^^^^^^^^^^^^^^^^^^^^
 Security checking will happen here on the server, so if the password,
username or role are invalid for this bean you'll get an exception  thrown.
*/
    }
    catch (javax.naming.NamingException ne)
    {
      ne.printStackTrace();
    }
    catch (java.rmi.ServerException se)
    {
      se.printStackTrace();
    }
    catch (javax.ejb.CreateException ce)
    {
      ce.printStackTrace();
    }
    catch (java.rmi.RemoteException re)
    {
      re.printStackTrace();
    }


However nothing will happen yet, well not as you would expect, has we
haven't configured the server yet.


What you have to do, is:

1. Tell jBoss to use JaasSecurityManager.
2. Write a custom ServerLoginModule.
3. Write a user.properties and a roles.properties files that will tell our
custom ServerLoginModule about are users their passwords and  roles.
4. Tell jBoss to use our custom ServerLoginModule.
5. Modify the deployment descriptors for your EJBs to enable security and
define roles.

1. Tell jBoss to use JaasSecurityManager.
--------------------------------------------------

In c:\jboss\conf\default open the file jboss.conf.

It should already have the following entry:

        <MLET CODE = "org.jboss.security.JaasSecurityManagerService"
ARCHIVE="jboss.jar" CODEBASE="../../lib/ext/">
</MLET>

You can delete any other security related entries (SimpleRealmMappingService
and EJBSecurityManagerService).

The JaasSecurityManager doesn't need amending but it does have a dubious way
of looking up roles on subjects. It assumes all roles will be  defined as
public credentials (I think they should be Principals). However, to minimise
our work we won't change this.

2. Write a custom ServerLoginModule.
--------------------------------------------------

This is the only piece of server side code you need to write. I took the
existing ServerLoginModule and modified that to use two properties  files
which define user & passwords and users & roles, rather than hardcoding: if
(username==password) then valid user and set role to  'user'.

In essence what it does is: When it's initialised it reads in
users.properties (which defines users and a single password for each user)
and  roles.properties which defines a user's roles with each role comman
separated, eg EdwardKenworthy=FinancialAdvisor,SalesManager.

When the ServerLoginModule is asked to authenticate a user and approve it's
access to a beans method (a chain of events that starts with the  container
invokinf SecurityInterceptor - but we don't have to worry about that) it
checks the user's password against the one in the  properties file and the
role against the user's roles. If they are all ok then it authenticates the
user.

Anyway, here's the code:

package uk.co.crispgroup.p303.server.security;

import java.util.*;
import java.io.*;

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


/*
  Authenticating a user on the server is a two step process (as many
LoginModules can be chained
  together and if one fails they all fail.

  Phase 1 validates the user. If that works then phase 2 happens and the
login actually happens
  (in this case we assign the user his roles.)
*/

public class CrispServerLoginModule implements LoginModule
{
    private Subject _subject;
    private CallbackHandler _callbackHandler;

  // users+passwords, users+roles
    private Properties _users;
    private Properties _roles;

    // username and password
    private String _username;
    private char[] _password;

    /**
     * Initialize this LoginModule.
     */
    public void initialize(Subject subject, CallbackHandler callbackHandler,
Map sharedState, Map options)
    {
        _subject = subject;
        _callbackHandler = callbackHandler;

// Load the properties file that contains the list of users and passwords
        try
        {
          LoadUsers();
          LoadRoles();
        }
        catch (Exception e)
        {
          System.out.print("PANIC! Couldn't load users/passwords/role
files.");
          e.printStackTrace();
        }
    }

    /**
     * Method to authenticate a Subject (phase 1).
     */
    public boolean login() throws LoginException
    {
        if (_users == null || _roles == null)
        {
          throw new LoginException("Missing _users or _roles properties
file.");
        }

        Callback[] callbacks = new Callback[2];
        // prompt for a username and password
        if (_callbackHandler == null)
        {
            throw new LoginException("Error: no CallbackHandler available "
+
                "to garner authentication information from the user");
        }
        callbacks[0] = new NameCallback("User name: ", "guest");
        callbacks[1] = new PasswordCallback("Password: ", false);
        try
        {
            _callbackHandler.handle(callbacks);
            _username = ((NameCallback)callbacks[0]).getName();
            char[] tmpPassword =
((PasswordCallback)callbacks[1]).getPassword();
            if (tmpPassword != null)
            {
                _password = new char[tmpPassword.length];
                System.arraycopy(tmpPassword, 0, _password, 0,
tmpPassword.length);
                ((PasswordCallback)callbacks[1]).clearPassword();
            }
        }
        catch (java.io.IOException ioe)
        {
            throw new LoginException(ioe.toString());
        }
        catch (UnsupportedCallbackException uce)
        {
            throw new LoginException("Error: " +
uce.getCallback().toString() +
                    " not available to garner authentication information " +
                    "from the user");
        }
        String userPassword = _users.getProperty(_username, null);
        if (_password == null || userPassword == null || !(new
String(_password)).equals(userPassword))
        {
            System.out.print("Bad password.");
            throw new FailedLoginException("Password Incorrect/Password
Required");
        }
        System.out.print("User '" + _username + "' authenticated.");
        return true;
    }

    /**
     * Method to commit the authentication process (phase 2).
     */
    public boolean commit() throws LoginException
    {
        Set roles = _subject.getPublicCredentials();
        StringTokenizer roleList = new
StringTokenizer(_roles.getProperty(_username), ",");
        while (roleList.hasMoreTokens())
        {
          roles.add(roleList.nextToken());
        }
        return true;
    }

    /**
     * Method to abort the authentication process (phase 2).
     */
    public boolean abort() throws LoginException
    {
        _username = null;
        if (_password != null)
        {
            for (int i = 0; i < _password.length; i++)
            _password[i] = ' ';
            _password = null;
        }
        return true;
    }

    public boolean logout() throws LoginException
    {
        return true;
    }


// utility methods
    private void LoadUsers() throws IOException
    {
      _users = LoadProperties("users.properties");
    }

    private void LoadRoles() throws IOException
    {
      _roles = LoadProperties("roles.properties");
    }

    private Properties LoadProperties(String propertiesName) throws
IOException
    {
      Properties bundle = null;
      try
      {
        InputStream is =
this.getClass().getResourceAsStream(propertiesName); // <-- searches the
class path for <package name>/filename
        if (null != is)
        {
          bundle = new Properties();
          bundle.load(is);
        }
        else
        {
          throw new IOException("Properties file/class " + propertiesName +
" not found");
        }
      }
      catch (IOException ioe)
      {
          throw ioe;
      }
      catch (Exception e)
      {
      }// catch
      return bundle;
    }
}

Compile this in the usual way.


3. Write a user.properties and a roles.properties files that will tell our
custom ServerLoginModule about are users their passwords and  roles.
--------------------------------------------------

Here are a couple of examples

user.properties:
EdwardKenworthy=EdwardsPassword
JulianGovier=JuliansPassword
MartinWebster=MartinsPassword

roles.properties:
EdwardKenworthy=FinancialAdvisor,SalesManager
JulianGovier=FinancialAdvisor
MartinWebster=SalesManager

These should be in the same directory as the CrispServerLoginModule.class
file.

Jar these three files up in crispsecurity.jar and copy it to c:\crisp.

Note that if CrispServerLoginModule.class, users.properties and
roles.properties are all in:

        c:\crispsecurity\uk\co\crispgroup\p303\server\security

Then you should run jar in c:\crispsecurity like this:

        jar -cvf crispsecurity.jar uk\co\crispgroup\p303\server\security\*

(I am assuming that only those three files are in that directory).

The copy the jar file to c:\crisp.


4. Tell jBoss to use our custom ServerLoginModule.
--------------------------------------------------

Open c:\jboss\conf\default\auth.conf. Edit it so that it looks like this:

        other
        {
                uk.co.crispgroup.p303.server.security.CrispServerLoginModule
required;
        }

You can just // comment the rest of the file out rather than deleting it
all.

(Note that by putting in the 'other' section this means it will apply to all
beans that don't have their own section defined.)

You'll also need to add the location of CrispServerLoginModule to the
classpath in the run.bat located in c:\jboss\bin

I used:

        REM Crisp Security
        set CLASSPATH=%CLASSPATH%;c:\crisp\crispsecurity.jar


5. Modify the deployment descriptors for your EJBs to enable security and
define roles.
----------------------------------------------------------------------------
-----------

You'll need to add some entries to your deployment descriptor in the
assembly-descriptor section.

For the CD example add:

        <security-role>                                         
                <role-name>FinancialAdvisor</role-name>
        </security-role>

        <method-permission>
                <role-name>FinancialAdvisor</role-name>
                <method>
                        <ejb-name>CDCollectionBean</ejb-name>
                        <method-name>*</method-name>
                </method>
        </method-permission>

        <method-permission>
                <role-name>FinancialAdvisor</role-name>
                <method>
                        <ejb-name>CDBean</ejb-name>
                        <method-name>*</method-name>
                </method>
        </method-permission>

Within the assembly-descriptor section.

This means your ejb-jar.xml file will now look like this:

<?xml version="1.0"?>

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd">

<ejb-jar>
  <display-name>MusicCDs</display-name>
  <enterprise-beans>

    <entity>
      <description>Models a music CD</description>
      <ejb-name>CDBean</ejb-name>
      <home>com.web_tomorrow.cd.CDHome</home>
      <remote>com.web_tomorrow.cd.CD</remote>
      <ejb-class>com.web_tomorrow.cd.CDBean</ejb-class>
      <persistence-type>Container</persistence-type>
      <prim-key-class>java.lang.String</prim-key-class>
      <reentrant>False</reentrant>
      <cmp-field><field-name>id</field-name></cmp-field>
      <cmp-field><field-name>title</field-name></cmp-field>
      <cmp-field><field-name>artist</field-name></cmp-field>
      <cmp-field><field-name>type</field-name></cmp-field>
      <cmp-field><field-name>notes</field-name></cmp-field>
      <primkey-field>id</primkey-field>
    </entity>

    <session>
      <description>Models a music CD collection</description>
      <ejb-name>CDCollectionBean</ejb-name>
      <home>com.web_tomorrow.cd.CDCollectionHome</home>
      <remote>com.web_tomorrow.cd.CDCollection</remote>
      <ejb-class>com.web_tomorrow.cd.CDCollectionBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
          <ejb-ref>
         <ejb-ref-name>ejb/CD</ejb-ref-name>
         <ejb-ref-type>Entity</ejb-ref-type>
         <home>com.web_tomorrow.cd.CDHome</home>
         <remote>com.web_tomorrow.cd.CD</remote>
         <ejb-link>CDBean</ejb-link>
          </ejb-ref>
    </session>

  </enterprise-beans>

  <assembly-descriptor>
    <container-transaction>
      <method>
        <ejb-name>CDBean</ejb-name>
        <method-name>*</method-name>
      </method>
      <trans-attribute>Required</trans-attribute>
    </container-transaction>

        <security-role>
                <role-name>FinancialAdvisor</role-name>
        </security-role>

        <method-permission>
                <role-name>FinancialAdvisor</role-name>
                <method>
                        <ejb-name>CDCollectionBean</ejb-name>
                        <method-name>*</method-name>
                </method>
        </method-permission>

        <method-permission>
                <role-name>FinancialAdvisor</role-name>
                <method>
                        <ejb-name>CDBean</ejb-name>
                        <method-name>*</method-name>
                </method>
        </method-permission>


  </assembly-descriptor>
</ejb-jar>


Having made this change run make_cd_jar.bat and copy the cd.jar file to
c:\jboss\deploy

        
Now if you start up jBoss and use your client you will find that usernames,
passwords and role-based access to your beans is enforced by the  server. Eg
if you login as EdwardKenworthy or JulianGovier you will be able to access
all of the methods of CDCollection and CD (as  EdwardKenworthy and
JulianGovier are both FinancialAdvisors). However MartinWebster won't be
able to as he is only a SalesManager.


Feedback, corrections, comments to: [EMAIL PROTECTED]


--
--------------------------------------------------------------
To subscribe:        [EMAIL PROTECTED]
To unsubscribe:      [EMAIL PROTECTED]
Problems?:           [EMAIL PROTECTED]

Reply via email to