Apache ActiveMQ is a well known and very flexible message broker. As such, it fully embraced the AAA model (Authentication, Authorization, Accountability) with built-in plugins.
For example, by default, it comes with
- Simple Authentication Plugin: it handles user authentication based on the `activemq.xml` defined list of users. Or, as an alternative, you can load users from properties. This is very useful for tests or to quickly bootstrap a project but does not target real-life deployments.
- JAAS Authentication Plugin: JAAS stands for Java Authentication and Authorization Service and is quite well known in the Java EE space (or Jakarta EE space). It allows ActiveMQ to use Login Modules to support properties, LDAP, or X509 SSL Certificates.
The JAAS Authentication Plugin provides a first approach to production environments. Still, it’s a bit of old technology, and it’s not always easy to find the right Login Module implementation outside the well known LDAP ones.
In some cases, we just need to apply IP filtering, and sometimes we need other mechanisms, such as JWT. We will use the latter in this blog to see how we can extend default built-in mechanisms.
JWT based authentication and authorization
JWT is very common and used in HTTP because they are meaningful tokens, and they can be signed or encrypted. This allows the server to validate the signature with a set of authorized keys and make sure the user is the one he pretends to be. They are different from blind tokens such as jsessionid in the HTTP context because they are JSON key/value pairs serialized as Base64 encoding.
The idea is to use the same JWT mechanism to authenticate ActiveMQ clients, so it’s possible to enforce some policies (create queue/topic, subscribe to a queue/topic, or simply connect to the server).
The implementation is based on
JwtAuthenticationPlugin: which allows to configure how to verify and authenticate a user using JWT. It does not embed any logic but everything configuration related. It is responsible for adding the BrokerFilter in the invocation chain.
public class JwtAuthenticationPlugin implements BrokerPlugin {
public static final String JWT_ISSUER = "https://server.example.com";
public static final Claims JWT_GROUPS_CLAIM = Claims.groups;
public static final String JWT_SIGNING_KEY_LOCATION = "/privateKey.pem";
public static final String JWT_VALIDATING_PUBLIC_KEY = "";
public static final JWSAlgorithm JWT_SIGNING_ALGO = JWSAlgorithm.RS256;
public JwtAuthenticationPlugin() {
}
public Broker installPlugin(final Broker next) {
// the public key is hard coded here for simplicity
return new JwtAuthenticationBroker(next, JWT_ISSUER, JWT_GROUPS_CLAIM, JWT_VALIDATING_PUBLIC_KEY);
}
}
JwtAuthenticationBroker is the BrokerFilter implementation. It is created by the JwtAuthenticationPlugin with the required logic to read, and validate JWT. At the end, it will create a SecurityContext instance and feed it with user principal and roles.
/**
* Handles authenticating a users based on a JWT token
*/
public class JwtAuthenticationBroker extends AbstractAuthenticationBroker {
private final String jwtIssuer;
private final Claims jwtGroupsClaim;
private final String jwtValidatingPublicKey;
public JwtAuthenticationBroker(
final Broker next,
final String jwtIssuer,
final Claims jwtGroupsClaim,
final String jwtValidatingPublicKey) {
super(next);
this.jwtIssuer = jwtIssuer;
this.jwtGroupsClaim = jwtGroupsClaim;
this.jwtValidatingPublicKey = jwtValidatingPublicKey;
}
@Override
public void addConnection(final ConnectionContext context, final ConnectionInfo info) throws Exception {
SecurityContext securityContext = context.getSecurityContext();
if (securityContext == null) {
securityContext = authenticate(info.getUserName(), info.getPassword(), null);
context.setSecurityContext(securityContext);
securityContexts.add(securityContext);
}
try {
super.addConnection(context, info);
} catch (Exception e) {
securityContexts.remove(securityContext);
context.setSecurityContext(null);
throw e;
}
}
@Override
public SecurityContext authenticate(final String username, final String password, final X509Certificate[] certificates) throws SecurityException {
SecurityContext securityContext = null;
if (!StringUtils.isEmpty(username)) {
// parse the JWT token, and check signature, validity, nbf
}
// login as anonymous or deny
return securityContext;
}
}
On the authorization side, there isn’t much to create, it’s only a matter of using the default SimpleAuthenticationPlugin. You need to define your policies and it will test the permissions against the SecurityContext created by the JwtAuthenticationBroker.
The entire project with all classes and logic is available on Github https://github.com/tomitribe/activemq-jwt-authn-authz
Conclusion
ActiveMQ internal architecture is very flexible and allows the user to easily plugin in any authentication/authorization mechanism. The JWT based authentication architecture could be improved to use client_id and client_secret only to generate a JWT token used to authenticate the client and authorize it to subscribe or not to queues and topics. For message-level security, we could create another JWT using a user profile (username/password as opposed to client_id/client_secret) and pass the token in a message header.