Tuesday, April 3, 2007

JBoss custom login module : a simple example

In this tutorial we will see how to write a very simple custom login module for the JBoss application server.
Even if JBoss ships with some useful login modules, there are not adapted to every situations, most notably with custom LDAP directories or strange authentication methods.
Chapter 8 of the JBoss documentation is a must read on this subject, and reading it is recommended before trying to write your own custom login module.

Our custom authentication scheme
In this example we want to implement a very stange authentication method :
- dynamic user login names in the form userX where X is a decimal number
- dynamic administrators login names in the form adminY where Y is what you want and is not very important
- a guest user name for anonymous connections
- dynamic user passphrase in the form userX loves you
- dynamic administrator passphrase in the form adminY is the boss
- empty password for the guest user


A configurable login module
The login module must be fully configurable.
We want :
- a regular expression to indentify guest users (optional, "anonymous" if not configured)
- a regular expression to indentify normal users
- a regular expression to indentify administrators
- a model for the users passphrases
- a model for the administrator passphrases

JBoss configuration
Default JBoss login modules configuration file is ${JBOSS_HOME}/server/default/conf/login-config.xml.
Every JBoss login module must be given a JAAS security domain name. We will call our security domain "MyLoginModule".
Add the following to the configuration file just before the "other" security domain :

com.blogger.xtechteam.examples.jboss.loginmodule.CustomJBossLoginModule
is the name of our custom login module class.

The custom login module
Our custom login module is a class derived from org.jboss.security.auth.spi.UsernamePasswordLoginModule.
By derivating UsernamePasswordLoginModule we only need to implements the following methods :
- initialize() : called by JBoss after creating a new instance of our login module
- validatePassword() : called by JBoss each time a user try to login
- getRoleSets() : called by JBoss after a user have been successfully authenticated to get the list of roles for this user
This is the code of our login module :

public class CustomJBossLoginModule extends UsernamePasswordLoginModule {

/* Regular expressions used for user login name */
private Pattern guestRegEx;
private Pattern userRegEx;
private Pattern adminRegEx;
/* Magic phrases used in place of user password */
private String userMagic;
private String adminMagic;

/* Currently authenticated user roles */
private boolean isGuest;
private boolean hasUserRole;
private boolean hasAdminRole;

/* called by JBoss when loading the login module */
@Override
public void initialize (
Subject subject,
CallbackHandler callbackHandler,
Map sharedState,
Map options) {
super.initialize (subject, callbackHandler, sharedState, options);
log.info ("CustomJBossLoginModule : initialize");

/* load the optional login module option */
String guestParam = (String)options.get ("guestRegEx");
if (guestParam == null) {
guestParam = "anonymous";
}

/* load the mandatory login module options */
guestRegEx = Pattern.compile (guestParam);
userRegEx = Pattern.compile ((String)options.get ("userRegEx"));
adminRegEx = Pattern.compile ((String)options.get ("adminRegEx"));
userMagic = (String)options.get ("userMagic");
adminMagic = (String)options.get ("adminMagic");
}

public CustomJBossLoginModule () {
super ();
}


/* called by JBoss to validate a user password */
@SuppressWarnings ("unchecked")
@Override
protected boolean validatePassword (String inputPassword, String expectedPassword) {
log.info ("CustomJBossLoginModule : validatePassword for " + getUsername ());

/* validate the user name */
isGuest = guestRegEx.matcher (getUsername ()).matches ();
if (!isGuest) {
hasUserRole = userRegEx.matcher (getUsername ()).matches ();
if (!hasUserRole) {
hasAdminRole = adminRegEx.matcher (getUsername ()).matches ();
}
}

/* validate the password */
boolean validPassword = isGuest;
if (hasUserRole) {
validPassword = inputPassword.equals (
String.format (userMagic, getUsername ()));
} else if (hasAdminRole) {
validPassword = inputPassword.equals (
String.format (adminMagic, getUsername ()));
}

/* save some informations for the application */
if (validPassword) {
Set principals = subject.getPrincipals ();
principals.add (new SimplePrincipal (getUsername ()));
principals.add (new SimplePrincipal (inputPassword));
principals.add (new SimplePrincipal ("Hello from CustomJBossLoginModule"));
principals.add (new SimplePrincipal (Boolean.toString (isGuest)));
principals.add (new SimplePrincipal (Boolean.toString (hasUserRole)));
principals.add (new SimplePrincipal (Boolean.toString (hasAdminRole)));
log.info (String.format ("CustomJBossLoginModule : user %s authenticated (guest : %b, user : %b, admin : %b)",
getUsername (), isGuest, hasUserRole, hasAdminRole));
} else {
log.info ("CustomJBossLoginModule : Invalid credentials");
}
return validPassword;
}

@Override
protected String getUsersPassword () throws LoginException {
return "";
}

/* fill the role collection */
@Override
protected Group[] getRoleSets () throws LoginException {
SimpleGroup userRoles = new SimpleGroup ("Roles");
if (isGuest) {
userRoles.addMember (new SimplePrincipal ("Guest"));
}
if (hasUserRole) {
userRoles.addMember (new SimplePrincipal ("User"));
}
if (hasAdminRole) {
userRoles.addMember (new SimplePrincipal ("Admin"));
}
Group[] roleSets = { userRoles };
return roleSets;
}

}

Compilation
To compile this login module you need two .jar files from JBoss : jboss-common.jar and jbosssx.jar.

Deployment
Put the login module class in a .jar file and copy this archive to ${JBOSS_HOME}/server/default/lib.


Getting informations about the user from the application
You (normally) have noticed that our login module save some informations in the collection of principals for the subject (the authenticated user).
This is needed to have this informations from our web applications or from our EJBs in a application-server independant way.
This is an example of a class that can be used to get this informations :

public class AuthenticatedUserInformations {

public class InvalidUserException extends Exception {};

/** The JACC PolicyContext key for the current Subject */
private static final String SUBJECT_CONTEXT_KEY =
"javax.security.auth.Subject.container";

private String username;
private String password;
private String hello;
private boolean isGuest;
private boolean isAdmin;
private boolean isUser;

public AuthenticatedUserInformations () throws InvalidUserException {
Subject caller;
try {
caller = (Subject) PolicyContext.getContext (SUBJECT_CONTEXT_KEY);
if (caller == null) {
throw new InvalidUserException ();
}
Iterator it = caller.getPrincipals ().iterator ();
username = it.next ().getName ();
password = it.next ().getName ();
hello = it.next ().getName ();
isGuest = Boolean.valueOf (it.next ().getName ());
isUser = Boolean.valueOf (it.next ().getName ());
isAdmin = Boolean.valueOf (it.next ().getName ());
} catch (PolicyContextException e) {
throw new InvalidUserException ();
}
}

public String getUsername () {
return username;
}

public String getPassword () {
return password;
}

public boolean isGuest () {
return isGuest;
}

public boolean isAdmin () {
return isAdmin;
}

public boolean isUser () {
return isUser;
}

}




Web application example
Finally, write a web application example to test our login module.
First, the web.xml file must contain the classic security configuration elements :
Second, the jboss-web.xml must declare the security domain :


Third, a simple JSP page to demonstrate the use of the AuthenticatedUserInformations class.
In an empty JSP page add the following lines before the HTML body :

and some JSP and HTML code in the body :
Deploy your application and give it a try !

No comments: