Writing a Custom JAAS LoginModule

Today I wrote a custom JAAS LoginModule and configured it to work with JBoss 4.2.2. This post summarizes the steps involved. This is not light reading — this is mostly reference information in case you ever have to write your own LoginModule implementation.

Why do This?

We wanted to add authentication to some web apps, but our administrative database is different than you might expect. Without going into too many proprietary details, the out-of-the-box JAAS LoginModule implementations did not work for us. I had to write a custom implementation of the LoginModule interface.

Now that we have a LoginModule implementation configured, we can use the standard <security-constraint>, <login-config>, and <security-role> tags in our web.xml deployment descriptors.

To summarize, we want to:

  • authenticate against our proprietary administrative database
  • utilize declarative security configuration in web.xml
  • when possible, stick to standard JAAS APIs, avoiding compile-time JBoss dependencies

Implement the Interfaces

The first step involves writing three classes. I am changing the names here in order to avoid publishing proprietary code (sorry!):

  • MyPrincipal – implements java.security.Principal. This class represents either a user or a role.
  • MyGroup – implements java.security.acl.Group. This holds a collection of roles.
  • MyLoginModule – implements javax.security.auth.spi.LoginModule. This ties into JAAS to perform the actual authentication against the database.

The Principal

This is a trivial interface that represents the user’s name, such as “harry”. It also represents a role he belongs to, like “admin” or “guest”. Your Principal should look something like this:

public class MyPrincipal implements Principal, Serializable {   private static final serialVersionUID = 1L;   private final String name;    public MyPrincipal(String name) {     this.name = name;   }    public String getName() {     return name;   }    // also implement equals() and hashCode(), you get the point }

The Group

This is a class that implements java.security.acl.Group:

public class MyGroup implements Group, Serializable {   private static final long serialVersionUID = 1L;   private final String name;   private final Set<Principal> users = new HashSet<Principal>();    public MyGroup(String name) {     this.name = name;   }    public boolean addMember(Principal user) {     return users.add(user);   }    public boolean removeMember(Principal user) {     return users.remove(user);   }    public boolean isMember(Principal member) {     return users.contains(member);   }    public Enumeration<? extends Principal> members() {     return Collections.enumeration(users);   }    public String getName() {     return name;   }    public boolean equals(Object o) {     // you are smart enough to write this; just compare the name   }    // yeah, write your own hashCode method too }

We’ll see how this group works next, with the LoginModule.

The LoginModule

I wrote my class by copying com.sun.jmx.remote.security.FileLoginModule. I won’t duplicate all of the code here because I can’t — it is proprietary. But here are the highlights (it is really easy, I promise):

public class MyLoginModule implements LoginModule {   private Subject subject;   private CallbackHandler callbackHandler;   private Map<String, ?> sharedState;   private Map<String, ?> options;    private boolean commitSucceeded = false;   private boolean loginSucceeded = false;    private String username;   private MyPrincipal user;   private MyPrincipal[] roles;    public void initialize(Subject subject, CallbackHandler callbackHandler,                          Map<String, ?> sharedState, Map<String, ?> options) {     this.subject = subject;     this.callbackHandler = callbackHandler;     this.sharedState = sharedState;     this.options = options;   }    public void login() throws LoginException {     ...     NameCallback nameCallback = new NameCallback("Username");     PasswordCallback passwordCallback = new PasswordCallback("Password", false);      Callback[] callbacks = new Callback[]{nameCallback, passwordCallback};     callbackHandler.handle(callbacks);      username = nameCallback.getName();     char[] password = passwordCallback.getPassword();     passwordCallback.clearPassword();      // you now have the username and password, so authenticate against your db      user = new MyPrincipal(username);     roles = new MyPrincipal[] {       new MyPrincipal("admin") // for example       ...fill in all of the roles from your database     };      // trust me - just do a "find usages" for classes implementing LoginModule   }    public boolean commit() throws LoginException {     ...     // this is the important part to work with JBoss:     subject.getPrincipals().add(user);     // jboss requires the name 'Roles'     MyGroup group = new MyGroup("Roles");     for (MyPrincipal role : roles) {       group.addMember(role);     }     subject.getPrincipals().add(group);      ...     return true;   }    ...other methods are trivial. just copy the general patterns from FileLoginModule }

Packaging

You need to compile those three classes and put them into a JAR file. We’ll call it myJaas.jar. Next, you’ll generate a .sar file. We can call this myapp.sar. This should also work in an EAR file, but mine is a SAR file. The structure of the SAR file is:

myapp.sar  |  +--myapp.war (the web application itself)  +--my-login-config.xml  +--myJaas.jar  +--META-INF       +--jboss-service.xml

The WAR file

This is a normal WAR file, but it does contain a JBoss-specific XML file. It looks like this:

myapp.war   |   +--WEB-INF        +--classes        +--lib        +--jboss-web.xml        +--web.xml

The jboss-web.xml file looks like this:

<jboss-web>   <security-domain>java:/jaas/myjaas</security-domain>   <depends>jboss.jca:service=DataSourceBinding,name=MyDS</depends> </jboss-web>

That “myjaas” is important because it ties your web app to the application policy we will define in my-login-config.xml. That data source dependency ensures the data source “MyDS” is deployed before the WAR file is deployed.

jboss-service.xml

This file looks like this:

<server>   <mbean code="org.jboss.security.auth.login.DynamicLoginConfig"     name="whatever:service=MyLogin">     <attribute name="AuthConfig">my-login-config.xml</attribute>     <depends optional-attribute-name="LoginConfigService">       jboss.security:service=XMLLoginConfig</depends>     <depends optional-attribute-name="SecurityManagerService">       jboss.security:service=JaasSecurityManager</depends>   </mbean> </server>

my-login-config.xml

Here goes:

<policy>   <application-policy name="myjaas">     <authentication>       <login-module code="com.acme.MyLoginModule"         flag="required">         <module-option name="myKey">myValue</module-option>       </login-module>     </authentication>   </application-policy> </policy>

That module-option is critical to my own proprietary implementation. It allowed me to pass along something called the “enterprise ID”, a proprietary piece of data we need for login functionality. You can add whatever options you need, they are available in your LoginModule class in the options map.

Final Steps

The last thing is to ensure your web application deployment descriptor (web.xml) declares the security constraint:

<security-constraint>   <web-resource-collection>     <web-resource-name>Secure Content</web-resource-name>     <url-pattern>/feed/*</url-pattern>   </web-resource-collection>   <auth-constraint>     <role-name>FeedAdmin</role-name>   </auth-constraint>   <user-data-constraint>     <transport-guarantee>NONE</transport-guarantee>   </user-data-constraint> </security-constraint>  <login-config>   <auth-method>BASIC</auth-method>   <realm-name>The Restricted Zone</realm-name> </login-config>  <security-role>   <description>The role required to access restricted content</description>   <role-name>FeedAdmin</role-name> </security-role>

In Summary

I hope this helps someone out there. While there are some JBoss-specific XML files, for the most part the APIs are generic JAAS. Furthermore, the web.xml is completely standard, with no dependencies on JBoss.

Leave a Reply

Your email address will not be published. Required fields are marked *