[ 
https://issues.apache.org/jira/browse/ARTEMIS-2952?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Luís Alves updated ARTEMIS-2952:
--------------------------------
    Description: 
To be able to implement OpenID Connect authN on Artemis I had to extend 
ActiveMQXAConnectionFactory to override the method:

{code:java}
protected synchronized ActiveMQConnection createConnectionInternal(final String 
username,
                                                                      final 
String password,
                                                                      final 
boolean isXA,
                                                                      final int 
type) throws JMSException
{code}

This allows me to send and access token instead of the user:password pair. In 
my implementation I leave username empty (like a flag) to tell the server that 
I wanna use a token that is on the password field.
This currently works fine but I had to do some reflection to modify the 
readOnly as it's private.

Can this flag be protected as well?

A possible alternative is to provide a way to pass the user and password using 
a Supplier, so I can define a method that gets a new access token if the 
current is expired. The token expiration also will cause some disconnections, 
as the server will detect the token expired and disconnect the client. I don't 
think there's any way I can update the connection credentials while the 
connection is established, correct? 


For reference here is the current code I'm using:

{code:java}
public class OAuth2ActiveMQXAConnectionFactory extends 
ActiveMQXAConnectionFactory {
    private static final Logger logger = 
LoggerFactory.getLogger(OAuth2ActiveMQXAConnectionFactory.class);

    private AtomicReference<String> accessTokenRef = new 
AtomicReference<>(null);
    private transient OIDCProviderMetadata providerMetadata;

    public OAuth2ActiveMQXAConnectionFactory() {
    }

    public OAuth2ActiveMQXAConnectionFactory(boolean ha, 
TransportConfiguration[] initialConnectors, String issuerURI) {
        super(ha, initialConnectors);

        if (!StringUtils.isEmpty(issuerURI)) {
            try {
                URL providerConfigurationURL = new URL(issuerURI + 
"/.well-known/openid-configuration");
                InputStream stream = providerConfigurationURL.openStream();
                // Read all data from URL
                String providerInfo = null;
                try (java.util.Scanner s = new java.util.Scanner(stream)) {
                    providerInfo = s.useDelimiter("\\A").hasNext() ? s.next() : 
"";
                }
                this.providerMetadata = 
OIDCProviderMetadata.parse(providerInfo);
            } catch (IOException | ParseException e) {
                throw new IllegalStateException("Cannot configure OIDC Provider 
Metadata.", e);
            }
        }
    }

    @Override
    @SuppressWarnings({"squid:S2095"}) //ClientSessionFactory - Resources 
should be closed
    protected synchronized ActiveMQConnection createConnectionInternal(final 
String username,
                                                                       final 
String password,
                                                                       final 
boolean isXA,
                                                                       final 
int type) throws JMSException {
        setSuperReadOnly();

        ClientSessionFactory factory;
        try {
            // we can't do a try with resource ad the factory cannot be closed
            // or it will close the ActiveMQConnection.
            factory = getServerLocator().createSessionFactory();
        } catch (Exception e) {
            JMSException jmse = new JMSException("Failed to create session 
factory");

            jmse.initCause(e);
            jmse.setLinkedException(e);

            throw jmse;
        }

        ActiveMQConnection connection = null;

        String accessToken;
        String user;
        if (nonNull(providerMetadata)) {
            accessToken = accessTokenRef.updateAndGet(
                previousToken -> getOrRefreshToken(previousToken, username, 
password));
            user = "";
        } else {
            //use normal authentication instead of token
            accessToken = password;
            user = username;
        }

        if (type == ActiveMQConnection.TYPE_GENERIC_CONNECTION ||
            type == ActiveMQConnection.TYPE_QUEUE_CONNECTION ||
            type == ActiveMQConnection.TYPE_TOPIC_CONNECTION) {
            connection = new ActiveMQXAConnection(this, user, accessToken, 
type, getClientID(),
                getDupsOKBatchSize(), getTransactionBatchSize(), 
isCacheDestinations(), false, factory);
        }

        if (connection == null) {
            throw new JMSException("Failed to create connection: invalid type " 
+ type);
        }
        connection.setReference(this);

        try {
            connection.authorize(!isEnableSharedClientID());
        } catch (JMSException e) {
            try {
                connection.close();
            } catch (JMSException me) {
                logger.debug("Failed to close connection", me);
            }
            throw e;
        }

        return connection;
    }

    private void setSuperReadOnly() {
        //XXX: workaround to avoid re-writing the whole connection factory 
code. (readOnly = true;)
        try {
            Field readOnly = 
ActiveMQConnectionFactory.class.getDeclaredField("readOnly");
            readOnly.setAccessible(true);
            readOnly.set(this, true);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            logger.info("failed to set read only to true");
        }
    }

    private String getOrRefreshToken(String previousToken, String username, 
String password) {
        if (previousToken == null) {
            return authenticateUserWithClientCredentialsGrant(username, 
password);
        }
        SignedJWT signedJWT;
        try {
            signedJWT = SignedJWT.parse(previousToken);
            Date exp = signedJWT.getJWTClaimsSet().getExpirationTime();

            Instant now = Instant.now().minus(2, ChronoUnit.SECONDS);
            if (exp.before(Date.from(now))) {
                //token is already expires let's refresh
                return authenticateUserWithClientCredentialsGrant(username, 
password);
            }
        } catch (java.text.ParseException e) {
            throw new IllegalStateException("Cannot fail parsing the token.", 
e);
        }
        return previousToken;
    }

    private String authenticateUserWithClientCredentialsGrant(String user, 
String password) {
        AccessToken accessToken = null;
        try {
            // Construct the client credentials grant
            final AuthorizationGrant clientGrant = new ClientCredentialsGrant();

            // The credentials to authenticate the client at the token endpoint
            final ClientID clientID = new ClientID(user);
            final  Secret clientSecret = new Secret(password);
            final ClientAuthentication clientAuth = new 
ClientSecretBasic(clientID, clientSecret);

            // The request scope for the token (may be optional)
            final Scope scope = new Scope("openid", "roles");

            // Make the token request
            final TokenRequest request = new 
TokenRequest(providerMetadata.getTokenEndpointURI(), clientAuth, clientGrant, 
scope);

            final TokenResponse response = 
TokenResponse.parse(request.toHTTPRequest().send());

            if (!response.indicatesSuccess()) {
                // We got an error response...
                final TokenErrorResponse errorResponse = 
response.toErrorResponse();
                logger.debug("Error response object {}", 
errorResponse.getErrorObject());
                return null;
            }

            final AccessTokenResponse successResponse = 
response.toSuccessResponse();

            //do we need refresh token? we have the client credentials
            accessToken = successResponse.getTokens().getAccessToken();
        } catch (ParseException | IOException e) {
            throw new IllegalStateException("Failed to do the client 
credentials grant.", e);
        }
        return accessToken.getValue();
    }

}
{code}

 

  was:
To be able to implement OpenID Connect authN on Artemis I had to extend 
ActiveMQXAConnectionFactory to override the method:

{code:java}
protected synchronized ActiveMQConnection createConnectionInternal(final String 
username,
                                                                      final 
String password,
                                                                      final 
boolean isXA,
                                                                      final int 
type) throws JMSException
{code}

This allows me to send and access token instead of the user:password pair. In 
my implementation I leave username empty (like a flag) to tell the server that 
I wanna use a token that is on the password field.
This currently works fine but I had to do some reflection to modify the 
readOnly as it's private.

Can this flag be protected as well?

A possible alternative is to provide a way to pass the user and password using 
a Supplier, so I can define a method that gets a new access token if the 
current is expired. The token expiration also will cause some disconnections, 
as the server will detect the token expired and disconnect the client. I don't 
think there's any way I can update the connection credentials while the 
connection is established, correct? 

 


> Extending ActiveMQXAConnectionFactory is limited by readOnly being private
> --------------------------------------------------------------------------
>
>                 Key: ARTEMIS-2952
>                 URL: https://issues.apache.org/jira/browse/ARTEMIS-2952
>             Project: ActiveMQ Artemis
>          Issue Type: Improvement
>    Affects Versions: 2.15.0
>            Reporter: Luís Alves
>            Priority: Major
>
> To be able to implement OpenID Connect authN on Artemis I had to extend 
> ActiveMQXAConnectionFactory to override the method:
> {code:java}
> protected synchronized ActiveMQConnection createConnectionInternal(final 
> String username,
>                                                                       final 
> String password,
>                                                                       final 
> boolean isXA,
>                                                                       final 
> int type) throws JMSException
> {code}
> This allows me to send and access token instead of the user:password pair. In 
> my implementation I leave username empty (like a flag) to tell the server 
> that I wanna use a token that is on the password field.
> This currently works fine but I had to do some reflection to modify the 
> readOnly as it's private.
> Can this flag be protected as well?
> A possible alternative is to provide a way to pass the user and password 
> using a Supplier, so I can define a method that gets a new access token if 
> the current is expired. The token expiration also will cause some 
> disconnections, as the server will detect the token expired and disconnect 
> the client. I don't think there's any way I can update the connection 
> credentials while the connection is established, correct? 
> For reference here is the current code I'm using:
> {code:java}
> public class OAuth2ActiveMQXAConnectionFactory extends 
> ActiveMQXAConnectionFactory {
>     private static final Logger logger = 
> LoggerFactory.getLogger(OAuth2ActiveMQXAConnectionFactory.class);
>     private AtomicReference<String> accessTokenRef = new 
> AtomicReference<>(null);
>     private transient OIDCProviderMetadata providerMetadata;
>     public OAuth2ActiveMQXAConnectionFactory() {
>     }
>     public OAuth2ActiveMQXAConnectionFactory(boolean ha, 
> TransportConfiguration[] initialConnectors, String issuerURI) {
>         super(ha, initialConnectors);
>         if (!StringUtils.isEmpty(issuerURI)) {
>             try {
>                 URL providerConfigurationURL = new URL(issuerURI + 
> "/.well-known/openid-configuration");
>                 InputStream stream = providerConfigurationURL.openStream();
>                 // Read all data from URL
>                 String providerInfo = null;
>                 try (java.util.Scanner s = new java.util.Scanner(stream)) {
>                     providerInfo = s.useDelimiter("\\A").hasNext() ? s.next() 
> : "";
>                 }
>                 this.providerMetadata = 
> OIDCProviderMetadata.parse(providerInfo);
>             } catch (IOException | ParseException e) {
>                 throw new IllegalStateException("Cannot configure OIDC 
> Provider Metadata.", e);
>             }
>         }
>     }
>     @Override
>     @SuppressWarnings({"squid:S2095"}) //ClientSessionFactory - Resources 
> should be closed
>     protected synchronized ActiveMQConnection createConnectionInternal(final 
> String username,
>                                                                        final 
> String password,
>                                                                        final 
> boolean isXA,
>                                                                        final 
> int type) throws JMSException {
>         setSuperReadOnly();
>         ClientSessionFactory factory;
>         try {
>             // we can't do a try with resource ad the factory cannot be closed
>             // or it will close the ActiveMQConnection.
>             factory = getServerLocator().createSessionFactory();
>         } catch (Exception e) {
>             JMSException jmse = new JMSException("Failed to create session 
> factory");
>             jmse.initCause(e);
>             jmse.setLinkedException(e);
>             throw jmse;
>         }
>         ActiveMQConnection connection = null;
>         String accessToken;
>         String user;
>         if (nonNull(providerMetadata)) {
>             accessToken = accessTokenRef.updateAndGet(
>                 previousToken -> getOrRefreshToken(previousToken, username, 
> password));
>             user = "";
>         } else {
>             //use normal authentication instead of token
>             accessToken = password;
>             user = username;
>         }
>         if (type == ActiveMQConnection.TYPE_GENERIC_CONNECTION ||
>             type == ActiveMQConnection.TYPE_QUEUE_CONNECTION ||
>             type == ActiveMQConnection.TYPE_TOPIC_CONNECTION) {
>             connection = new ActiveMQXAConnection(this, user, accessToken, 
> type, getClientID(),
>                 getDupsOKBatchSize(), getTransactionBatchSize(), 
> isCacheDestinations(), false, factory);
>         }
>         if (connection == null) {
>             throw new JMSException("Failed to create connection: invalid type 
> " + type);
>         }
>         connection.setReference(this);
>         try {
>             connection.authorize(!isEnableSharedClientID());
>         } catch (JMSException e) {
>             try {
>                 connection.close();
>             } catch (JMSException me) {
>                 logger.debug("Failed to close connection", me);
>             }
>             throw e;
>         }
>         return connection;
>     }
>     private void setSuperReadOnly() {
>         //XXX: workaround to avoid re-writing the whole connection factory 
> code. (readOnly = true;)
>         try {
>             Field readOnly = 
> ActiveMQConnectionFactory.class.getDeclaredField("readOnly");
>             readOnly.setAccessible(true);
>             readOnly.set(this, true);
>         } catch (NoSuchFieldException | IllegalAccessException e) {
>             logger.info("failed to set read only to true");
>         }
>     }
>     private String getOrRefreshToken(String previousToken, String username, 
> String password) {
>         if (previousToken == null) {
>             return authenticateUserWithClientCredentialsGrant(username, 
> password);
>         }
>         SignedJWT signedJWT;
>         try {
>             signedJWT = SignedJWT.parse(previousToken);
>             Date exp = signedJWT.getJWTClaimsSet().getExpirationTime();
>             Instant now = Instant.now().minus(2, ChronoUnit.SECONDS);
>             if (exp.before(Date.from(now))) {
>                 //token is already expires let's refresh
>                 return authenticateUserWithClientCredentialsGrant(username, 
> password);
>             }
>         } catch (java.text.ParseException e) {
>             throw new IllegalStateException("Cannot fail parsing the token.", 
> e);
>         }
>         return previousToken;
>     }
>     private String authenticateUserWithClientCredentialsGrant(String user, 
> String password) {
>         AccessToken accessToken = null;
>         try {
>             // Construct the client credentials grant
>             final AuthorizationGrant clientGrant = new 
> ClientCredentialsGrant();
>             // The credentials to authenticate the client at the token 
> endpoint
>             final ClientID clientID = new ClientID(user);
>             final  Secret clientSecret = new Secret(password);
>             final ClientAuthentication clientAuth = new 
> ClientSecretBasic(clientID, clientSecret);
>             // The request scope for the token (may be optional)
>             final Scope scope = new Scope("openid", "roles");
>             // Make the token request
>             final TokenRequest request = new 
> TokenRequest(providerMetadata.getTokenEndpointURI(), clientAuth, clientGrant, 
> scope);
>             final TokenResponse response = 
> TokenResponse.parse(request.toHTTPRequest().send());
>             if (!response.indicatesSuccess()) {
>                 // We got an error response...
>                 final TokenErrorResponse errorResponse = 
> response.toErrorResponse();
>                 logger.debug("Error response object {}", 
> errorResponse.getErrorObject());
>                 return null;
>             }
>             final AccessTokenResponse successResponse = 
> response.toSuccessResponse();
>             //do we need refresh token? we have the client credentials
>             accessToken = successResponse.getTokens().getAccessToken();
>         } catch (ParseException | IOException e) {
>             throw new IllegalStateException("Failed to do the client 
> credentials grant.", e);
>         }
>         return accessToken.getValue();
>     }
> }
> {code}
>  



--
This message was sent by Atlassian Jira
(v8.3.4#803005)

Reply via email to