/**
* Copyright (C) 2015 Zalando SE (http://tech.zalando.com)
*
* 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 org.zalando.stups.tokens;
import static org.zalando.stups.tokens.EndsWithFilenameFilter.forSuffix;
import static org.zalando.stups.tokens.FileSupplier.getCredentialsDir;
import static org.zalando.stups.tokens.util.Objects.notNull;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.zalando.stups.tokens.fs.FilesystemSecretRefresher;
import org.zalando.stups.tokens.fs.FilesystemSecretsRefresherConfiguration;
import org.zalando.stups.tokens.mcb.MCBConfig;
/**
* Use the <i>AccessTokensBuilder</i> obtained via
* {@link Tokens#createAccessTokensWithUri(URI)} to build your configuration for
* obtaining an {@link AccessToken} for different scopes / services via the
* {@link AccessTokens#getAccessToken(Object)} or
* {@link AccessTokens#get(Object)} methods on the instance returned after
* invoking {@link AccessTokensBuilder#start()}.
*
* This class offers a Fluent Interface type of Builder. You can invoke any of
* the methods on the initially retrieved instance until you call
* {@link AccessTokensBuilder#start()}. Invoking any at any later point of time
* will throw an {@link IllegalStateException}.
*
*/
public class AccessTokensBuilder implements TokenRefresherConfiguration {
private final URI accessTokenUri;
private URI tokenInfoUri;
private ClientCredentialsProvider clientCredentialsProvider = null;
private UserCredentialsProvider userCredentialsProvider = null;
private int refreshPercentLeft = 40;
private int warnPercentLeft = 20;
private final HttpConfig httpConfig = new HttpConfig();
private final Set<AccessTokenConfiguration> accessTokenConfigurations = new HashSet<>();
private boolean locked = false;
private HttpProviderFactory httpProviderFactory;
private int schedulingPeriod = 5;
private TimeUnit schedulingTimeUnit = TimeUnit.SECONDS;
private ScheduledExecutorService executorService;
private int tokenVerifierSchedulingPeriod = 5;
private TimeUnit tokenVerifierSchedulingTimeUnit = TimeUnit.MINUTES;
private TokenVerifierProvider tokenVerifierProvider;
private MCBConfig tokenRefresherMcbConfig = new MCBConfig.Builder().build();
private MCBConfig tokenVerifierMcbConfig = new MCBConfig.Builder().withErrorThreshold(3).withMaxMulti(4)
.withTimeout(10).withTimeUnit(TimeUnit.MINUTES).build();
private MetricsListener metricsListener = new DebugLogMetricsListener();
private final FilesystemSecretsRefresherConfiguration filesystemSecretsRefresherConfiguration;
AccessTokensBuilder(final URI accessTokenUri) {
this.accessTokenUri = notNull("accessTokenUri", accessTokenUri);
this.filesystemSecretsRefresherConfiguration = new FilesystemSecretsRefresherConfiguration(this);
}
private void checkLock() {
if (locked) {
throw new IllegalStateException("configuration already done");
}
}
/**
* Use the supplied implementation of {@link ClientCredentialsProvider} to
* create the {@link ClientCredentials} which will be used to authenticate
* the client when requesting a new access token.
*
* See https://tools.ietf.org/html/rfc6749#section-1.3.4 for further
* information on the meaning of client credentials in the context of
* OAuth2.0
*
* @param clientCredentialsProvider
* Your implementation of the {@link ClientCredentialsProvider}
* interface to use. See
* {@link JsonFileBackedClientCredentialsProvider} for a
* potential implementation
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied
* {@link ClientCredentialsProvider} set.
*/
public AccessTokensBuilder usingClientCredentialsProvider(
final ClientCredentialsProvider clientCredentialsProvider) {
checkLock();
this.clientCredentialsProvider = notNull("clientCredentialsProvider", clientCredentialsProvider);
return this;
}
/**
* Use the supplied implementation of {@link UserCredentialsProvider} to
* create the {@link UserCredentials} which will be used to authenticate the
* user when requesting a new access token.
*
* See https://tools.ietf.org/html/rfc6749#section-1.3.3 for further
* information on the meaning of user credentials and how they can be used.
*
* @param userCredentialsProvider
* Your implementation of the {@link UserCredentialsProvider}
* interface to use. See
* {@link JsonFileBackedUserCredentialsProvider} for a potential
* implementation
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied
* {@link UserCredentialsProvider} set.
*/
public AccessTokensBuilder usingUserCredentialsProvider(final UserCredentialsProvider userCredentialsProvider) {
checkLock();
this.userCredentialsProvider = notNull("userCredentialsProvider", userCredentialsProvider);
return this;
}
/**
* Use the supplied implementation of {@link HttpProviderFactory} to create
* the {@link HttpProvider} which will be used for requesting new access
* tokens.
*
* @param factory
* Your implementation of the {@link HttpProviderFactory} to use.
* See {@link ClosableHttpProviderFactory} for a potential
* implementation.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link HttpProviderFactory}
* set.
*/
public AccessTokensBuilder usingHttpProviderFactory(final HttpProviderFactory factory) {
checkLock();
this.httpProviderFactory = notNull("httpProviderFactory", factory);
return this;
}
/**
* Use the supplied implementation of {@link TokenVerifierProvider} to
* create the {@link TokenVerifier} to use for verifying access and refresh
* tokens
*
* @param tokenVerifierProvider
* Your implementation of the {@link TokenVerifierProvider} to
* user. See {@link CloseableTokenVerifierProvider} for a
* potential implementation.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link TokenVerifierProvider}
* set.
*/
public AccessTokensBuilder usingTokenVerifierProvider(TokenVerifierProvider tokenVerifierProvider) {
checkLock();
this.tokenVerifierProvider = notNull("tokenVerifierProvider", tokenVerifierProvider);
return this;
}
/**
* Change the socket timeout in milliseconds to be used for HTTP
* connections. Default value is 2000.
*
* @param socketTimeout
* Your desired socket timeout in milliseconds
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>socket timeout</i> set.
*/
public AccessTokensBuilder socketTimeout(final int socketTimeout) {
checkLock();
this.httpConfig.setSocketTimeout(socketTimeout);
return this;
}
/**
* Use the supplied existing {@link ScheduledExecutorService} for all
* scheduled operations done by the created {@link AccessTokens}
* implementation instead of creating a new one.
*
* If none is supplied a new one will be created using
* {@link Executors#newSingleThreadScheduledExecutor(java.util.concurrent.ThreadFactory)} upon start.
*
* @param executorService
* Your version of the {@link ScheduledExecutorService} to be
* used.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied
* {@link ScheduledExecutorService} set.
*
* @see TokenRefresherThreadFactory
* @see Executors#newSingleThreadExecutor(java.util.concurrent.ThreadFactory)
*/
public AccessTokensBuilder existingExecutorService(final ScheduledExecutorService executorService) {
checkLock();
this.executorService = notNull("executorService", executorService);
return this;
}
/**
* Change the connect timeout in milliseconds to be used for HTTP
* connections. Default value is 1000.
*
* @param connectTimeout
* Your desired connect timeout in milliseconds
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>connect timeout</i> set.
*/
public AccessTokensBuilder connectTimeout(final int connectTimeout) {
checkLock();
this.httpConfig.setConnectTimeout(connectTimeout);
return this;
}
/**
* Change the connection request timeout in milliseconds to be used for
* requesting new HTTP connections from the connection manager. Default
* value is 500.
*
* @param connectionRequestTimeout
* Your desired connection request timeout in milliseconds
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>connection request
* timeout</i> set.
*/
public AccessTokensBuilder connectionRequestTimeout(final int connectionRequestTimeout) {
checkLock();
this.httpConfig.setConnectionRequestTimeout(connectionRequestTimeout);
return this;
}
/**
* Change whether the stale connection check should be enabled or disabled
* when requesting a HTTP connection from the connection manager. Default
* behaviour is enabled.
*
* @param staleConnectionCheckEnabled
* True in case the stale connection check should be enabled
* false otherwise
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>stale connection check
* value</i> set.
*/
public AccessTokensBuilder staleConnectionCheckEnabled(final boolean staleConnectionCheckEnabled) {
checkLock();
this.httpConfig.setStaleConnectionCheckEnabled(staleConnectionCheckEnabled);
return this;
}
/**
* Set the threshold of the validity time left before the service tries to
* refresh an access token with the authorization server. Default value is
* 40.
*
* @param refreshPercentLeft
* The percentage of validity time left that triggers refreshing
* an access token
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>refresh percentage left</i>
* set.
*/
public AccessTokensBuilder refreshPercentLeft(final int refreshPercentLeft) {
checkLock();
this.refreshPercentLeft = refreshPercentLeft;
return this;
}
/**
* Set the threshold of the validity time left before the service issues a
* warning. This value can be used to detect possible access token
* refreshing problems e.g. Default value is 20.
*
* @param warnPercentLeft
* The percentage of validity time left that triggers a warning
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>warn percentage left</i>
* set.
*/
public AccessTokensBuilder warnPercentLeft(final int warnPercentLeft) {
checkLock();
this.warnPercentLeft = warnPercentLeft;
return this;
}
/**
* Configure a new access token configuration that should be managed by the
* returned {@link AccessTokens} implementation. You can manage multiple
* different tokens by invoking this method multiple times with different
* values for <i>tokenId</i>.
*
* You can e.g. manage a read only token as well as a write token. It is
* considered best practice to use as limited scopes as reasonable to
* mitigate the security implications that arise by leaked access tokens.
*
* @param tokenId
* A unique id for this specific access token configuration. The
* supplied object must implement {@link Object#equals(Object)}
* in a way that it identifies the same value correctly. A
* straight forward version would be using a {@link String}
* value.
* @return An instance of {@link AccessTokenConfiguration} which offers a
* fluent interface type of configuration for a single
* <i>tokenId</i>. Use the returned object to configure e.g the
* scopes for that specific token.
*/
public AccessTokenConfiguration manageToken(final Object tokenId) {
checkLock();
notNull("tokenId", tokenId);
final AccessTokenConfiguration config = new AccessTokenConfiguration(tokenId, this);
accessTokenConfigurations.add(config);
return config;
}
/**
* Configure the amount of time between two runs of checking which existing
* access tokens should be refreshed. This method must be used together with
* {@link AccessTokensBuilder#schedulingTimeUnit(TimeUnit)} to define the
* scheduling. The meaning of the value supplied here depends on the setting
* for the {@link TimeUnit}. Default value is set to 5.
*
* @param schedulingPeriod
* The value for the <i>scheduling period</i> to use.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>scheduling period</i> set.
*/
public AccessTokensBuilder schedulingPeriod(final int schedulingPeriod) {
checkLock();
this.schedulingPeriod = schedulingPeriod;
return this;
}
/**
* Configure the {@link TimeUnit} used together with the configured
* <i>scheduling period</i> to use for checking which access tokens should
* be refreshed. Default value is {@link TimeUnit#SECONDS}
*
* @param timeUnit
* The {@link TimeUnit} to use together with the configured
* <i>scheduling period</i>
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link TimeUnit} set.
*/
public AccessTokensBuilder schedulingTimeUnit(TimeUnit timeUnit) {
checkLock();
this.schedulingTimeUnit = notNull("schedulingTimeUnit", timeUnit);
return this;
}
/**
* Configure the amount of time that should pass between two run of the
* {@link TokenVerifier}. The exact meaning of this value depends on the
* value set by
* {@link AccessTokensBuilder#tokenVerifierSchedulingTimeUnit(TimeUnit)}.
* Default value is 5.
*
* @param tokenVerifierSchedulingPeriod
* The value for the <i>token verification scheduling period</i>
* to use.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>token verification
* scheduling period</i> set.
*/
public AccessTokensBuilder tokenVerifierSchedulingPeriod(int tokenVerifierSchedulingPeriod) {
checkLock();
this.tokenVerifierSchedulingPeriod = tokenVerifierSchedulingPeriod;
return this;
}
/**
* Configure the {@link TimeUnit} used together with the configured <i>token
* verification scheduling period</i> to use for verifying existing access
* tokens. Default value is {@link TimeUnit#SECONDS}
*
* @param timeUnit
* The {@link TimeUnit} to use together with the configured
* <i>token verification scheduling period</i>
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link TimeUnit} set.
*/
public AccessTokensBuilder tokenVerifierSchedulingTimeUnit(TimeUnit timeUnit) {
checkLock();
this.tokenVerifierSchedulingTimeUnit = notNull("tokenVerifierSchedulingTimeUnit", timeUnit);
return this;
}
/**
* Configure the <i>token info URI</i> to be used by the
* {@link TokenVerifier} to verify existing access tokens.
*
* @param tokenInfoUri
* The <i>token info URI</i> to be used by the
* {@link TokenVerifier}
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied <i>token info URI</i> set.
*/
public AccessTokensBuilder tokenInfoUri(URI tokenInfoUri) {
checkLock();
this.tokenInfoUri = notNull("tokenInfoUri", tokenInfoUri);
return this;
}
/**
* Configure the configuration for the circuit breaker to be used for
* refreshing access tokens.
*
* @param config
* The {@link MCBConfig} that should be used for refreshing
* access tokens.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link MCBConfig} set.
*/
public AccessTokensBuilder tokenRefresherMcbConfig(MCBConfig config) {
checkLock();
this.tokenRefresherMcbConfig = notNull("tokenRefresherMcbConfig", config);
return this;
}
/**
* Configure the configuration for the circuit breaker to be used for
* verifying access tokens.
*
* @param config
* The {@link MCBConfig} that should be used for verifying access
* tokens.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link MCBConfig} set.
*/
public AccessTokensBuilder tokenVerifierMcbConfig(MCBConfig config) {
checkLock();
this.tokenVerifierMcbConfig = notNull("tokenVerifierMcbConfig", config);
return this;
}
/**
* Configure the {@link MetricsListener} to be used by the created
* {@link AccessTokens} implementation for reporting execution times.
*
* @param metricsListener
* The {@link MetricsListener} implementation to be used. See
* {@link DebugLogMetricsListener} for a potential implementation
* of this interface.
* @return The same {@link AccessTokensBuilder} instance this method has
* been called upon with the supplied {@link MetricsListener} set.
*/
public AccessTokensBuilder metricsListener(MetricsListener metricsListener) {
checkLock();
this.metricsListener = notNull("metricsListener", metricsListener);
return this;
}
public FilesystemSecretsRefresherConfiguration whenUsingFilesystemSecrets(){
return this.filesystemSecretsRefresherConfiguration;
}
@Override
public int getSchedulingPeriod() {
return schedulingPeriod;
}
@Override
public URI getAccessTokenUri() {
return accessTokenUri;
}
@Override
public URI getTokenInfoUri() {
return tokenInfoUri;
}
@Override
public HttpProviderFactory getHttpProviderFactory() {
return this.httpProviderFactory;
}
@Override
public ClientCredentialsProvider getClientCredentialsProvider() {
return clientCredentialsProvider;
}
@Override
public UserCredentialsProvider getUserCredentialsProvider() {
return userCredentialsProvider;
}
@Override
public int getRefreshPercentLeft() {
return refreshPercentLeft;
}
@Override
public ScheduledExecutorService getExecutorService() {
if (executorService == null) {
return Executors.newSingleThreadScheduledExecutor(new TokenRefresherThreadFactory());
} else {
return executorService;
}
}
@Override
public TokenVerifierProvider getTokenVerifierProvider() {
if (tokenVerifierProvider == null) {
return new CloseableTokenVerifierProvider();
} else {
return tokenVerifierProvider;
}
}
@Override
public int getWarnPercentLeft() {
return warnPercentLeft;
}
@Override
public Set<AccessTokenConfiguration> getAccessTokenConfigurations() {
return Collections.unmodifiableSet(accessTokenConfigurations);
}
/**
* Create the {@link AccessTokens} instance along with any required
* additional components. After this method has been invoked this
* {@link AccessTokensBuilder} is in locked state and all further
* invocations of any of the configuration methods will throw an
* {@link IllegalStateException}.
*
* Invoking this method will create and start instances of
* {@link AccessTokenRefresher} as well as {@link AccessTokenRefresher} as
* configured previously using the {@link ScheduledExecutorService} as
* configured.
*
* In case no {@link ClientCredentialsProvider} has been configured an
* instance of {@link JsonFileBackedClientCredentialsProvider} is used.
*
* In case no {@link UserCredentialsProvider} has been configured an
* instance of {@link JsonFileBackedUserCredentialsProvider} is used.
*
* In case no {@link HttpProviderFactory} has been configured an instance of
* {@link ClosableHttpProviderFactory} is used.
*
* @return The {@link AccessTokens} instance built from the previously
* supplied configuration. Use it to e.g. get an {@link AccessToken}
* for any of your configured <i>tokenIds</i>.
*/
public AccessTokens start() {
if (accessTokenConfigurations.size() == 0) {
throw new IllegalArgumentException("no scopes defined");
}
locked = true;
if (clientCredentialsProvider == null) {
// use default
clientCredentialsProvider = new JsonFileBackedClientCredentialsProvider();
}
if (userCredentialsProvider == null) {
// use default
userCredentialsProvider = new JsonFileBackedUserCredentialsProvider();
}
if (httpProviderFactory == null) {
this.httpProviderFactory = new ClosableHttpProviderFactory();
}
final AbstractAccessTokenRefresher refresher = getAccessTokenRefresher();
refresher.start();
return refresher;
}
@Override
public HttpConfig getHttpConfig() {
return httpConfig;
}
protected AbstractAccessTokenRefresher getAccessTokenRefresher() {
if(isFilesystemSecretsLayout()){
return new FilesystemSecretRefresher(this);
}
return new AccessTokenRefresher(this);
}
private boolean isFilesystemSecretsLayout() {
try {
return getCredentialsDir().list(forSuffix("-token-secret")).length > 0;
} catch (Exception e) {
return false;
}
}
@Override
public int getTokenVerifierSchedulingPeriod() {
return tokenVerifierSchedulingPeriod;
}
@Override
public MCBConfig getTokenRefresherMcbConfig() {
return tokenRefresherMcbConfig;
}
@Override
public MCBConfig getTokenVerifierMcbConfig() {
return tokenVerifierMcbConfig;
}
@Override
public MetricsListener getMetricsListener() {
return metricsListener;
}
@Override
public TimeUnit getSchedulingTimeUnit() {
return schedulingTimeUnit;
}
@Override
public TimeUnit getTokenVerifierSchedulingTimeUnit() {
return tokenVerifierSchedulingTimeUnit;
}
@Override
public FilesystemSecretsRefresherConfiguration getFilesystemSecretsRefresherConfiguration() {
return filesystemSecretsRefresherConfiguration;
}
}