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]