[
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)