Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

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.

Add CredentialSupplier hook to supply the credentials

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
macroId20e7a58e-c5b5-48ba-b24d-710cf2cd6a47
nameCredentialSupplier Concepts
pagePin2

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.

Observe authentication failure and update credentials on notification

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
macroId02769dd6-7b88-459a-9743-24dedeb72c4a
nameAuthentication failure notification with Listener
pagePin2

Thus,  for OAUTH2 based authentication, a special implementation of AuthenticationListener can be configured on the connection. When connection recovery fails with AuthenticationException, the listener gets invoked. The code can be added into the listener to get new access token and update it on a connection by invoking setPassword(String) method. The next failover attempt would use an updated token to establish connectivity.

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
languagejava
titleToken re-generation
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.