Table of Contents |
---|
Problem statement
The Qpid JMS clients for AMQP 1.0 and AMQP 0-x support XOAUTH2 SASL mechanism to authenticate messaging connection against OAUTH2 based services. The XOAUTH2 SASL mechanism is described on Google Developers page OAuth 2.0 mechanism and involves sending a bearer token as part of initial client response as illustrated below:
...
The broker is supposed to validate the token and accept connections from the clients providing a valid token. Depending from OAUTH2 authentication provider the broker can perform some extra client validation by invoking OAUTH2 authentication provider specific REST API.
The XOAUTH2 SASL mechanism works perfectly for an initial connection establishment. However, the client implementations of XOAUTH2 SASL mechanism do not have a mechanism to update an expired access token. Thus, when failover is used when token expires and connectivity is lost after token being expired, the restoration of connectivity is not possible, as the same expired token would be used in every re-connection attempt. As result, the existing failover mechanism will not be able to re-establish connectivity with XOAUTH2 SASL. The only way the client application can work around this issue is by tracking the token expiration timeout, closing an existing connection after token expiration and re-opening a new connection with a new token. Thus, using XOAUTH2 SASL authentication in production environments requires users to write a complex logic around tracking of expiration time and re-opening connections which can interrupt a messaging flow and might cause disruption in underlying business use cases.
Possible solutions
There are several ways to address the limitation. Though, every of them would involve changes to the client code. This wiki describes some possible approaches for fixing the issue.
User controlled credential update
In JMS API the password based credentials can be passed either via connection URL or directly into API calls: ConnectionFactory#createConnection(String userName, String password), ConnectionFactory#createContext(String userName, String password). When credentials are set there is no way in JMS API to update the credentials. We need an extension to JMS API which would allow to pass into JMS Connection special objects capable of supplying credentials. Such objects can be used for an initial connection establishment and restoring the connectivity during failover.
A possible example of JMS connection extension
Code Block |
---|
ConnectionFactory connectionFactory =...
CredentialSupplier passwordCredentialSupplier = new PasswordCredentialSupplier(password);
CredentialSupplier userNameCredentialSupplier = new UserNameCredentialSupplier(userName);
Connection connection = connectionFactory.createConnection(userNameCredentialSupplier , passwordCredentialSupplier ) |
The API methods might look like below
Code Block |
---|
ConnectionFactory#createConnection(CredentialSupplier... credentialSuppliers)
ConnectionFactory#createContext(CredentialSupplier... credentialSuppliers) |
Gliffy Diagram | ||||||
---|---|---|---|---|---|---|
|
The end-user can implement a custom CredentialSupplier to supply credentials on runtime when connection is being established or re-established. It would be up to CredentialSupplier implementation to check the current credentials and renew them when they expire.
The SASL mechanism implementations would need to be changed to operate with CredentialSupplier interface and its derivatives (UserNameCredentialSupplier, PasswordCredentialSupplier) to fetch the required credentials. The new interfaces and implementations extending CredentialSupplier can be added for new SASL mechanisms if required.
Authentication Failure Observer
A special Listener can be introduced to call on authentication failures. Thus, when authentication fails due to an expired credentials the messaging application can be notified through this Listener, which can be set on JMS Connection or Context. The credentials set methods (setPassword(), setUserName) can be added into JMS Connection or Context implementations. That should allow to update the credentials if required.
The following class diagram illustrates the idea
Gliffy Diagram | ||||||
---|---|---|---|---|---|---|
|
Introduce a special failover mechanism
A special failover mechanism can be added with hooks to get the credentials. Both JMS implementations have different failover API. The failover mechanism supporting credential update would need to be different. Such failover hook needs to be configured via connection URL.
Generate token inside of the client
A special OAUTH2 hooks can be added to clients in order to generate the token. Every OAUTH2 provider API (google, microsoft, github, etc) would need to have a separate implementation to get the token. Though, we need to develop a common interface which would allow to inject and invoke the hooks.
Connection Pools
, the failover functionality would attempt to reconnect with an expired token. The token needs to be updated in order make sub-subsequent reconnect successful.
This wiki describes an approach which can be used for refreshing or regenerating the token.
Using Qpid JMS connection extension to supply OAUTH2 access token
The Qpid JMS clients for AMQP 1.0 allows to register an implementation of BiFunction<Connection, URI, Object>
as a password connection extension on JmsConnectionFactory
. If extension is set, on opening connection the registered function is invoked to provide a password. Such function can be used to check the access token expiration time and re-generate or refresh the token by invoking OAUTH2 provider API. The following sample code illustrates this approach for token regeneration
Code Block | ||||
---|---|---|---|---|
| ||||
final String brokerURI = "..."; // 1 broker connection URI
final TokenGenerator tokenGenerator = new TokenGenerator(...); // 2 helper object to generate access token for a specific OAUTH2 implementation
final BiFunction<Connection, URI, Object> tokenExtension = new BiFunction<Connection, URI, Object>()// 3 password extension for token renewal
{
private volatile String token;
private long currentTokenExpirationTime;
@Override
public Object apply(final Connection connection, final URI uri)
{
long currentTime = System.currentTimeMillis();
if (currentTime > currentTokenExpirationTime) // 4 check token expiration
{
this.token = tokenGenerator.generateAccessToken(); // 5 get new token
this.currentTokenExpirationTime = tokenGenerator.getTokenExpirationTime(token); // 6 preserve token expiration time
}
return this.token;
}
};
final JmsConnectionFactory factory = new JmsConnectionFactory(brokerURI); // 7 create connection factory
factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), tokenExtension); // 8 register password extension for token regeneration
final Connection connection = factory.createConnection(); // 9 open connection |
In the snippet above an implementation of BiFunction<Connection, URI, Object>
is created at (3) for access token provisioning. The function implementation checks the token expiration at (4) and regenerate the token at (5) using a helper object (2) implementing calls to OAUTH2 specific API. The token expiration time is preserved at (6) for the following reconnects attempts. An instance of JmsConnectionFactory
is created at (7) for a given brokerURI (1). A password extension is registered at (8). JMS connection is open at (9). The example uses a hypothetical class TokenGenerator
invoking underlying OAUTH2 API to generate/renew access token and get token expiration time.
Access token regeneration with legacy JMS Client for AMPQ 0-x.
The legacy JMS client does not provide an extension similar to new JMS Client for AMQP 1.0. The client application needs to track the token expiration timeout, close the connection after token expiration and re-opening a new connection with a new tokenThe Pool connection factories (like Pooled-JMS or PooledConnectionFactory from JMS for AMQP-0-x ) have the same issue with token expiration. The expired token needs to be updated in some ways on cached connections or on connection creation. Though, an introduction of a special failover mechanism ( configurable via connection URL) or generation of the token inside of the client by means of special hooks would solve this problem.