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 { @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { ... } @Override public boolean login() throws LoginException { ... } @Override public boolean commit() throws LoginException { ... } @Override public boolean abort() throws LoginException { ... } @Override 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 ORM 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:
@Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> 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:
@Override 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.
Karaf Configuration
The JAAS module is deployed to Karaf through a regular blueprint context:
<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’:
<beans ...> <broker xmlns="http://activemq.apache.org/schema/core" ... <plugins> <jaasAuthenticationPlugin configuration="activemq-jaas" /> </plugins> ... </broker> </beans>
Injecting 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:
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 can be written in terms of the PrincipalManager:
public class MongoDbLoginModule implements LoginModule { private PrincipalManager principalManager; ... }
Karaf gives access to the Blueprint bundle context as part of the parameters provided to the JAAS module’s initialize() call. So the call becomes:
private <T> T getService(BundleContext bundleContext, Class<T> clazz) { ServiceReference<T> ref = bundleContext.getServiceReference(clazz); T service = bundleContext.getService(ref); return service; } public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> 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.