com.ibm.cloud.objectstorage.oauth.DefaultTokenManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ibm-cos-java-sdk-bundle Show documentation
Show all versions of ibm-cos-java-sdk-bundle Show documentation
A single bundled dependency that includes all service and dependent JARs with third-party libraries relocated to different namespaces.
/*
* Copyright 2017 IBM Corp. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.ibm.cloud.objectstorage.oauth;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.http.HttpStatus;
import org.apache.http.impl.client.HttpClientBuilder;
import com.ibm.cloud.objectstorage.ClientConfiguration;
import com.ibm.cloud.objectstorage.SDKGlobalConfiguration;
import com.ibm.cloud.objectstorage.http.apache.SdkProxyRoutePlanner;
import com.ibm.cloud.objectstorage.http.apache.utils.ApacheUtils;
import com.ibm.cloud.objectstorage.http.settings.HttpClientSettings;
import com.ibm.cloud.objectstorage.log.InternalLogApi;
import com.ibm.cloud.objectstorage.log.InternalLogFactory;
import com.ibm.cloud.objectstorage.oauth.DefaultTokenProvider;
import com.ibm.cloud.objectstorage.oauth.OAuthServiceException;
import com.ibm.cloud.objectstorage.oauth.Token;
import com.ibm.cloud.objectstorage.oauth.TokenManager;
import com.ibm.cloud.objectstorage.oauth.TokenProvider;
import com.ibm.cloud.objectstorage.oauth.DefaultTokenManager;
/**
* Default implementation of Token Manager. This design will allow the class to
* be created with multiple constructors, yet only one instance can be created.
* The DefaultTokenProvider is used to retrieve the token from the IAM Service
* which uses the api key method. A client can also use their own implementation
* of TokenHandler
*
*
*/
public class DefaultTokenManager implements TokenManager {
protected static final InternalLogApi log = InternalLogFactory.getLog(DefaultTokenManager.class);
private TokenProvider provider;
private volatile Token token;
// flag to signify if an async refresh process has already started
private volatile boolean asyncInProgress = false;
/** variable to overwrite the global SDKGlobalConfiguration.IAM_ENDPOINT **/
private String iamEndpoint = SDKGlobalConfiguration.IAM_ENDPOINT;
/**
* variable to overwrite the global SDKGlobalConfiguration.IAM_MAX_RETRY
**/
private int iamMaxRetry = SDKGlobalConfiguration.IAM_MAX_RETRY;
/**
* variable to overwrite the global SDKGlobalConfiguration.IAM_MAX_RETRY
**/
private double iamRefreshOffset = SDKGlobalConfiguration.IAM_REFRESH_OFFSET;
/** The client configuration */
private ClientConfiguration clientConfiguration;
/** The client http setting */
private HttpClientSettings httpClientSettings;
// Executor service for token refresh
private final ExecutorService executor = Executors.newSingleThreadExecutor();
/**
* Set of HTTP response codes that should attempt retry.
*/
private static final Set NON_RETRYABLE_STATUS_CODES = new HashSet(4);
static {
NON_RETRYABLE_STATUS_CODES.add(HttpStatus.SC_BAD_REQUEST);
NON_RETRYABLE_STATUS_CODES.add(HttpStatus.SC_UNAUTHORIZED);
NON_RETRYABLE_STATUS_CODES.add(HttpStatus.SC_FORBIDDEN);
NON_RETRYABLE_STATUS_CODES.add(HttpStatus.SC_NOT_FOUND);
}
/**
* Default constructor using the apiKey. This instance will use the
* DefaultTokenProvider to retrieve the access token.
*
* @param apiKey
* The IBM API Key
*/
public DefaultTokenManager(String apiKey) {
log.debug("DefaultTokenManager api key constructor");
this.provider = new DefaultTokenProvider(apiKey);
}
/**
* Constructor which will allow a custom TokenHandler to retrieve the token
* from the IAM Service
*
* @param provider
* The Token Provider which will retrieve the Token Object from
* the IAM service
*/
public DefaultTokenManager(TokenProvider provider) {
this.provider = provider;
}
/**
* Overwrite the default IAM endpoint. This should only be done in a
* development or staging environment
*
* @param iamEndpoint
* The http endpoint to retrieve the token
*/
public void setIamEndpoint(String iamEndpoint) {
this.iamEndpoint = iamEndpoint;
/* The TokenManager's variable does nothing, so if it is set, pass it down */
if (getProvider() instanceof DefaultTokenProvider) {
DefaultTokenProvider defaultProvider = (DefaultTokenProvider)getProvider();
defaultProvider.setIamEndpoint(iamEndpoint);
}
}
/**
* Over write the default IAM refresh offset. This should only be done in a
* development or staging environment
*
* @param offset
* The percentage of token life that token should be refreshed
* before expiration.
*/
public void setIamRefreshOffset(double offset) {
this.iamRefreshOffset = offset;
}
/**
* Over write the default IAM max retry count. This should only be done in a
* development or staging environment
*
* @param offset
* The max number of times to attempt IAM token retrieval.
*/
public void setIamMaxRetry(int count) {
this.iamMaxRetry = count;
}
/**
* Return the TokenProvider used by the TokenManager
*
*/
public TokenProvider getProvider() {
return provider;
}
/**
* Retrieve the access token String from the OAuth2 token object
*
*/
@Override
public String getToken() {
log.debug("DefaultTokenManager getToken()");
if (!checkCache()) {
retrieveToken();
}
// retrieve from cache
if (token == null) {
token = retrieveTokenFromCache();
}
// check if expired
if (hasTokenExpired(token)) {
token = retrieveTokenFromCache();
}
// check if token should be refreshed. If a refreshtoken is not present, the token manager will call upon the original tokenprovider retrieve a fresh token
if (isTokenExpiring(token) && !isAsyncInProgress()) {
if (null != token.getRefresh_token() && "not_supported" != token.getRefresh_token()) {
this.asyncInProgress = true;
submitRefreshTask();
} else {
retrieveToken();
token = retrieveTokenFromCache();
}
}
if (token.getAccess_token() != null && !token.getAccess_token().isEmpty()) {
return token.getAccess_token();
} else if (token.getDelegated_refresh_token()!= null && !token.getDelegated_refresh_token().isEmpty()) {
return token.getDelegated_refresh_token();
} else if (token.getIms_token() != null && !token.getIms_token().isEmpty()) {
return token.getIms_token();
} else {
return token.getUaa_token();
}
}
/**
* Check if cache has a Token object stored
*
* @return boolean Indicates if the Token has been cached
*/
protected boolean checkCache() {
log.debug("OAuthTokenManager.checkCache()");
boolean tokenExists = getCachedToken() == null ? false : true;
return tokenExists;
}
/**
* Add the Token object to in-memory cache
*
* @param token
* The IAM Token object
*/
protected synchronized void cacheToken(final Token token) {
log.debug("OAuthTokenManager.cacheToken");
// Parse token expires in seconds.
int tokenExpiresInSecs;
try {
tokenExpiresInSecs = Integer.parseInt(token.getExpires_in());
} catch (NumberFormatException exception) {
tokenExpiresInSecs = 0;
}
// Parse token expiration time
long tokenExpirationTime;
try {
tokenExpirationTime = Long.parseLong(token.getExpiration());
} catch (NumberFormatException exception) {
tokenExpirationTime = 0;
}
// Calculate token refresh time based on lifespan percentage offset.
long refreshBeforeExpirySecs = (long) (tokenExpiresInSecs * this.iamRefreshOffset);
long tokenRefreshTime = tokenExpirationTime - refreshBeforeExpirySecs;
token.setRefreshTime(tokenRefreshTime);
token.setExpirationTime(tokenExpirationTime);
setTokenCache(token);
}
/**
* Retrieve the IAM token from cache storage
*
* @return Token The IAM Token object
*/
protected Token retrieveTokenFromCache() {
log.debug("OAuthTokenManager.retrieveTokenFromCache");
return getCachedToken();
}
/**
* Check if the current cached token has expired. If it has a synchronous
* http call is made to the IAM service to retrieve & store a new token
*
* @param token
* The IAM Token object
*
* @return boolean Indicates if the currently cached token has expired
*/
protected boolean hasTokenExpired(final Token token) {
log.debug("OAuthTokenManager.hasTokenExpired");
final long currentTime = System.currentTimeMillis() / 1000L;
if (Long.valueOf(token.getExpiration()) < currentTime) {
retrieveToken();
return true;
}
return false;
}
/**
* Check if the current cached token is expiring in less than the given
* offset. If it is, an asynchronous call is made to the IAM service to
* update the cache.
*
* @param token
* The IAM Token object
*
* @return boolean Indicates if the currently cached token is due refresh
*/
protected boolean isTokenExpiring(final Token token) {
log.debug("OAuthTokenManager.isTokenExpiring");
final long currentTime = System.currentTimeMillis() / 1000L;
if (currentTime > token.getRefreshTime()) {
log.debug("Token is expiring");
return true;
} else {
log.debug("Token is not expiring." + token.getRefreshTime() + " > " + currentTime);
return false;
}
}
/**
*
* @return boolean currently cached Token object
*/
protected Token getCachedToken() {
return this.token;
}
/**
*
* @param token
* Sets the Token object in cache
*/
protected void setTokenCache(Token token) {
this.token = token;
}
/**
* retrieve token from provider. Ensures each thread checks the token is
* null prior to making the callout to IAM
*
*/
protected synchronized void retrieveToken() {
log.debug("OAuthTokenManager.retrieveToken");
// If we have no token or if the token we have has expired, get a new token
if (token == null || (Long.valueOf(token.getExpiration()) < System.currentTimeMillis() / 1000L)) {
log.debug("Token needs to be refreshed, retrieving from provider");
for (int attempt = 1; attempt <= this.iamMaxRetry; attempt++) {
try {
token = provider.retrieveToken();
break; // We received a token; no exceptions were thrown
} catch (OAuthServiceException exception) {
log.debug("Exception retrieving IAM token on attempt " + attempt
+ ". Returned status code " + exception.getStatusCode()
+ ". Error Message: " + exception.getErrorMessage()
+ ". Status Message: " + exception.getStatusMessage());
// Check if we've run out of retries and need to rethrow this exception
if (attempt >= this.iamMaxRetry) {
throw exception;
}
}
}
// Retrieving tokens should always return a non-null value,
// so even if token was originally null, it should not be now.
if (token == null) {
throw new OAuthServiceException("TokenProvider.retrieveToken() " +
" returned null and instead of throwing an exception. This is a bug." +
" Custom TokenProvider classes should return a token or thrown exceptions.");
}
cacheToken(token);
}
}
/**
* Submits a token refresh task
*
* @return void
*/
protected void submitRefreshTask() {
TokenRefreshTask tokenRefreshTask = new TokenRefreshTask(this);
executor.execute(tokenRefreshTask);
log.debug("Submitted token refresh task");
}
/**
* boolean value to signal if the async refresh method is already in use
*
* @return boolean
*/
protected boolean isAsyncInProgress() {
log.debug("Aysnchrnonous job in progress : " + asyncInProgress);
return asyncInProgress;
}
/**
* Client config to customise the IAM Client
*
* @return
*/
public ClientConfiguration getClientConfiguration() {
return clientConfiguration;
}
/**
* Set the client config that is been used on the s3client
*
* @param clientConfiguration
*/
public void setClientConfiguration(ClientConfiguration clientConfiguration) {
this.clientConfiguration = clientConfiguration;
if (clientConfiguration != null) {
this.httpClientSettings = HttpClientSettings.adapt(clientConfiguration);
if (getProvider() instanceof DefaultTokenProvider) {
DefaultTokenProvider defaultProvider = (DefaultTokenProvider)getProvider();
defaultProvider.setHttpClientSettings(httpClientSettings);
}
if (getProvider() instanceof DelegateTokenProvider) {
DelegateTokenProvider delegateProvider = (DelegateTokenProvider)getProvider();
delegateProvider.setHttpClientSettings(httpClientSettings);
}
}
}
private boolean shouldRetry(int statusCode) {
if (NON_RETRYABLE_STATUS_CODES.contains(statusCode)) {
return false;
}
else {
return true;
}
}
@Override
protected void finalize() throws Throwable {
try {
executor.shutdown();
} catch (Throwable t) {
throw t;
} finally {
super.finalize();
}
}
class TokenRefreshTask implements Runnable {
private DefaultTokenManager tokenManager;
private Token refreshedToken = null;
TokenRefreshTask(DefaultTokenManager tokenManager) {
this.tokenManager = tokenManager;
}
@Override
public void run() {
try {
if (!(tokenManager.getProvider() instanceof DefaultTokenProvider)) {
log.info("OAuthTokenManager.TokenRefreshTask: Token Provider is not of type DefaultTokenProvider, so using the refresh token is not supported.");
return;
}
// Try to get a new token as long as it has not yet expired
while ((this.refreshedToken == null)
&& (System.currentTimeMillis() / 1000 < tokenManager.token.getExpirationTime())) {
try {
log.info("OAuthTokenManager.TokenRefreshTask: Attempting to retrieve refresh token");
refreshedToken = ((DefaultTokenProvider)tokenManager.getProvider()).retrieveTokenWithRefresh(tokenManager.token.getRefresh_token());
break; // We received a token; no exceptions were thrown
} catch (OAuthServiceException exception) {
log.info("OAuthTokenManager.TokenRefreshTask: Exception retrieving IAM token"
+ ". Returned status code " + exception.getStatusCode()
+ ". Error Message: " + exception.getErrorMessage()
+ ". Status Message: " + exception.getStatusMessage());
// Wait before retrying
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
log.info("Token refresh task interrupted: " + e.getMessage());
}
if (System.currentTimeMillis() / 1000 >= tokenManager.token.getExpirationTime()) {
// The token has expired, so we want to rethrow this exception as the failure reason
throw exception;
}
}
}
if (refreshedToken != null) {
tokenManager.cacheToken(refreshedToken);
log.info("OAuthTokenManager.TokenRefreshTask: Token refreshed");
} else if (System.currentTimeMillis() / 1000 < tokenManager.token.getExpirationTime()) {
// Retrieving tokens should always return a non-null value, so if we are here,
// we broke the loop because of the 'break', not because the token expired.
log.info("OAuthTokenManager.TokenRefreshTask: Refresh aborted before token expiration");
throw new OAuthServiceException("OAuthTokenManager.TokenRefreshTask: "
+ " TokenProvider.retrieveTokenWithRefresh()"
+ " returned null and instead of throwing an exception. This is a bug."
+ " Custom TokenProvider classes should return a token or thrown exceptions.");
} else {
log.info("OAuthTokenManager.TokenRefreshTask: Failed to refresh token");
}
} finally {
tokenManager.asyncInProgress = false;
}
}
}
/**
* Add a proxy to the http request if it has been set within settings
*
* @param builder
* Builder used to create the http client
* @param settings
* Settings which contain any proxy configuration details
*/
public static void addProxyConfig(HttpClientBuilder builder,
HttpClientSettings settings) {
if (settings.isProxyEnabled()) {
log.info("Configuring Proxy. Proxy Host: " + settings.getProxyHost() + " " +
"Proxy Port: " + settings.getProxyPort() + " " +
"Proxy Protocol: " + settings.getProxyProtocol());
builder.setRoutePlanner(new SdkProxyRoutePlanner(
settings.getProxyHost(), settings.getProxyPort(), settings.getProxyProtocol(), settings.getNonProxyHosts()));
if (settings.isAuthenticatedProxy()) {
builder.setDefaultCredentialsProvider(ApacheUtils.newProxyCredentialsProvider(settings));
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy