Over a million developers have joined DZone.

A MongoDB-based JAAS plugin for Karaf and ActiveMQ

· Database Zone

Sign up for the Couchbase Community Newsletter to stay ahead of the curve on the latest NoSQL news, events, and webinars. Brought to you in partnership with Coucbase.

ActiveMQ supports pluggable JAAS modules that handle the authentication of incoming requests. ActiveMQ comes preloaded with two JAAS modules: a module that reads authentication details from a properties file and one where data is stored in LDAP. When MongoDB is the repository of authentication data, then integration options include either synchronizing the MongoDB repository with LDAP or developing a JAAS module for MongoDB. In this article, a MongoDB JAAS module for a simple data schema is described. In addition instructions are provided for deploying this as a JAAS realm in Karaf with Blueprint.

The schema

A very simple schema is assumed:
  • A username
  • A password in encoded (SHA/SSHA) or raw form

The JAAS module

There’s nothing ActiveMQ-specific in the JAAS login module. In fact the module is exported as a JAAS realm in Karaf and can be used by any JAAS-aware subsystem such as the Karaf container itself. In addition the JAAS realm can happily coexist with other JAAS realms. The module must implement the javax.security.auth.spi.LoginModule interface:

public class MongoDbLoginModule implements LoginModule {
    public void initialize(Subject subject, CallbackHandler callbackHandler,
        Map sharedState, Map options) {
        ...
    }
    public boolean login() throws LoginException {
        ...
    }
    public boolean commit() throws LoginException {
        ...
    }
    public boolean abort() throws LoginException {
        ...
    }
    public boolean logout() throws LoginException {
        ...
    }
}
  • The login() method will authenticate the incoming request.
  • commit() is called when authentication succeeds and typically sets the groups/roles that the logged in principal is a member of. Group/role membership is used to validate authorization rules at a later stage. E.g. ActiveMQ’s authorization modules rely on the JAAS groups/roles that are setup at this stage.
  • abort() is called when authentication fails.
  • The logout() call provides an option to clean up the state setup during the login & commit phases.
  • initialize() includes settings provided in the JAAS configuration file (shown later on).

MongoDB

MongoDB interaction is directly through the MongoDB Java driver. A mapping library such as Spring Data, Morphia or Jongo is not employed not only to avoid the additional dependencies but simply because the schema is too trivial to use an ODM layer.

The JAAS module expects the following configuration options:
  • The host & port where MongoDB runs.
  • The MongoDB database and collection which holds the authentication details.
  • The name of the attribute in the MongoDB collection that maps to the principal’s username.
  • The name of the attribute in the MongoDB collection that maps to the principal’s password.

Initialization

initialize() will save the above settings:

public void initialize(Subject subject, CallbackHandler callbackHandler,
    Map sharedState, Map options) {
    this.subject = subject;
    this.handler = callbackHandler;
 
    mongoDbHost = getString(options, MONGODB_HOST);
    mongoDbPort = getInteger(options, MONGODB_PORT);
    mongoDbDatabase = getString(options, MONGODB_DB);
    mongoDbCollection = getString(options, MONGODB_COLLECTION);
    mongoDbAttrUser = getString(options, MONGODB_ATTR_USER);
    mongoDbAttrPassword = getString(options, MONGODB_ATTR_PASSWORD);
}

Authentication

login() will fetch the username/password pair and authenticate the credentials:

public boolean login() throws LoginException {
    Callback[] callbacks = new Callback[2];
    callbacks[0] = new NameCallback("User name");
    callbacks[1] = new PasswordCallback("Password", false);
 
    try {
        handler.handle(callbacks);
    } catch (IOException ioe) {
        throw (LoginException) new LoginException().initCause(ioe);
    } catch (UnsupportedCallbackException uce) {
        throw (LoginException) new LoginException().initCause(uce);
    }
 
    username = ((NameCallback) callbacks[0]).getName();
    if (username == null)
        return false;
 
    String password = "";
    if (((PasswordCallback) callbacks[1]).getPassword() != null)
        password = new String(((PasswordCallback) callbacks[1]).getPassword());
 
    authenticate(username, password);
    return true;
}

The authentication method will lookup the user and compare the provided (plain text) password with the password stored in the database which can be either plain text, encoded with SHA or salted SHA (SSHA).

private void authenticate(String username, String password)
    throws LoginException {
    DBObject dbObject = findUser(username);
    if (dbObject == null) {
        throw new FailedLoginException(String.format(
            "Wrong username or password for user %s.", username));
    }
 
    if (!dbObject.containsField(mongoDbAttrPassword)) {
        throw new FailedLoginException(String.format(
            "Wrong username or password for user %s.", username));
    }
 
    String encPassword = (String) dbObject.get(mongoDbAttrPassword);
    if (StringUtils.isEmpty(encPassword)) {
        throw new FailedLoginException(String.format(
            "Wrong username or password for user %s.", username));
    }
 
    verifyPassword(encPassword, password);
}
 
private void verifyPassword(String encPassword, String rawPassword)
    throws FailedLoginException {
    boolean result = getPasswordEncoder().isPasswordValid(encPassword, rawPassword, null);     
    if (result)
        return;
         
    throw new FailedLoginException(String.format(
        "Wrong username or password for user %s.", username));
}

The password encoder is cut-down version of Spring Security’s LDAP password encoder which supports both raw and SHA/SSHA hashed passwords.

Karaf Setup

The JAAS module is deployed to Karaf through a regular blueprint context:

blueprint.xml
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
    xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.0.0"
    xmlns:jaas="http://karaf.apache.org/xmlns/jaas/v1.1.0">
     
    <cm:property-placeholder persistent-id="io.modio.blog.security.activemq"/>
 
    <jaas:config name="activemq-jaas">
        <jaas:module className="io.modio.blog.security.activemq.jaas.mongodb.MongoDbLoginModule"
            flags="required">
            mongodb.host=${io.modio.blog.security.activemq.jaas.mongodb.host}
            mongodb.port=${io.modio.blog.security.activemq.jaas.mongodb.port}
            mongodb.db=${io.modio.blog.security.activemq.jaas.mongodb.db}
            mongodb.collection=${io.modio.blog.security.activemq.jaas.mongodb.collection}
            mongodb.attribute.user=${io.modio.blog.security.activemq.jaas.mongodb.attribute.user}
            mongodb.attribute.password=${io.modio.blog.security.activemq.jaas.mongodb.attribute.password}
        </jaas:module>
    </jaas:config>
 
</blueprint>

ActiveMQ Configuration

ActiveMQ can be configured through activemq.xml to use the JAAS realm named activemq-jaas:

activemq.xml

<beans ...>
    <broker xmlns="http://activemq.apache.org/schema/core"
        ...
        <plugins>
            <jaasAuthenticationPlugin configuration="activemq-jaas" />
        </plugins>
        ...
    </broker>
</beans>

Injecting OSGi Services to the JAAS Realm

The MongoDB-based JAAS module described above binds the particular backend storage with the authentication functionality. Ideally the backend authentication repository should be abstracted behind a generic management interface such as:

PrincipalManager.java
public interface PrincipalManager {
    Principal add(Principal principal) throws AuthException;
     
    void deleteByAccessKey(String accessKey) throws AuthException;
     
    void addToGroupByAccessKey(String accessKey, String groupKey) throws AuthException;
     
    void deleteFromGroupByAccessKey(String accessKey, String groupKey) throws AuthException;
     
    Principal findByAccessKey(String accessKey) throws AuthException;
     
    boolean authenticate(String accessKey, String secretKey) throws AuthException;
}

Then the JAAS module would be written in terms of the PrincipalManager:

PrincipalManager.java
public class MongoDbLoginModule implements LoginModule {
    private PrincipalManager principalManager;
    ...
}

Fortunately, Karaf gives access to the Blueprint bundle context as part of the parameters provided to the JAAS module’s initialize() call. So accessing an OSGi service becomes possible:

MongoDbLoginModule.java
private  T getService(BundleContext bundleContext, Class clazz) {
    ServiceReference ref = bundleContext.getServiceReference(clazz);
    T service = bundleContext.getService(ref);
    return service;
}
 
public void initialize(Subject subject, CallbackHandler callbackHandler,
    Map sharedState, Map options) {
    BundleContext bundleContext = (BundleContext)options.get(BundleContext.class.getName());
    this.subject = subject;
    this.handler = callbackHandler;
    principalManager = getService(bundleContext, PrincipalManager.class);
}

Including references to the PrincipalManager service in the Blueprint definition will ensure that the service is available before the JAAS realm is activated.

The Getting Started with NoSQL Guide will get you hands-on with NoSQL in minutes with no coding needed. Brought to you in partnership with Couchbase.

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}