All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.athenz.zts.ZTSClient Maven / Gradle / Ivy

/*
 * Copyright 2016 Yahoo Inc.
 *
 * 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.yahoo.athenz.zts;

import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import athenz.shade.zts.athenz.shade.zts.javax.ws.rs.client.Client;
import athenz.shade.zts.athenz.shade.zts.javax.ws.rs.client.ClientBuilder;

import athenz.shade.zts.athenz.shade.zts.com.fasterxml.jackson.databind.DeserializationFeature;
import com.oath.auth.KeyRefresher;
import com.oath.auth.KeyRefresherException;
import com.oath.auth.KeyRefresherListener;
import com.oath.auth.Utils;
import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver;
import athenz.shade.zts.athenz.shade.zts.org.apache.http.config.Registry;
import athenz.shade.zts.athenz.shade.zts.org.apache.http.config.RegistryBuilder;
import athenz.shade.zts.athenz.shade.zts.org.apache.http.conn.socket.ConnectionSocketFactory;
import athenz.shade.zts.athenz.shade.zts.org.apache.http.conn.socket.PlainConnectionSocketFactory;
import athenz.shade.zts.athenz.shade.zts.org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import athenz.shade.zts.athenz.shade.zts.org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.operator.OperatorCreationException;
import org.ehcache.Cache;
import athenz.shade.zts.athenz.shade.zts.org.glassfish.jersey.apache.connector.ApacheClientProperties;
import athenz.shade.zts.athenz.shade.zts.org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import athenz.shade.zts.athenz.shade.zts.org.glassfish.jersey.client.ClientConfig;
import athenz.shade.zts.athenz.shade.zts.org.glassfish.jersey.client.ClientProperties;
import athenz.shade.zts.athenz.shade.zts.org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import athenz.shade.zts.athenz.shade.zts.org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.securitytoken.model.AssumeRoleRequest;
import com.amazonaws.services.securitytoken.model.Credentials;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
import athenz.shade.zts.athenz.shade.zts.com.fasterxml.jackson.core.JsonProcessingException;
import athenz.shade.zts.athenz.shade.zts.com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.athenz.auth.AuthorityConsts;
import com.yahoo.athenz.auth.Principal;
import com.yahoo.athenz.auth.PrivateKeyStore;
import com.yahoo.athenz.auth.ServiceIdentityProvider;
import com.yahoo.athenz.auth.impl.RoleAuthority;
import com.yahoo.athenz.auth.util.Crypto;
import com.yahoo.athenz.auth.util.CryptoException;
import com.yahoo.athenz.common.config.AthenzConfig;
import com.yahoo.athenz.common.utils.SSLUtils;
import com.yahoo.athenz.common.utils.SSLUtils.ClientSSLContextBuilder;
import com.yahoo.rdl.JSON;

public class ZTSClient implements Closeable {

    private static final Logger LOG = LoggerFactory.getLogger(ZTSClient.class);

    private String ztsUrl = null;
    private String proxyUrl = null;
    private String domain = null;
    private String service = null;
    private SSLContext sslContext = null;
    private ZTSClientNotificationSender notificationSender = null;

    ZTSRDLGeneratedClient ztsClient = null;
    ServiceIdentityProvider siaProvider = null;
    Principal principal = null;
    ZTSClientCache ztsClientCache = ZTSClientCache.getInstance();

    // configurable fields
    //
    static private boolean cacheDisabled = false;
    static private int tokenMinExpiryTime = 900;
    static private int tokenMaxExpiryOffset = 300;
    static private long prefetchInterval = 60; // seconds
    static private boolean prefetchAutoEnable = true;
    static private String x509CsrDn = null;
    static private String x509CsrDomain = null;
    static private int reqReadTimeout = 30000;
    static private int reqConnectTimeout = 30000;
    static private String x509CertDNSName = null;
    static private String confZtsUrl = null;
    static private JwtsSigningKeyResolver resolver = null;
    
    private boolean enablePrefetch = true;
    private boolean ztsClientOverride = false;

    @SuppressWarnings("unused")
    static private boolean initialized = initConfigValues();
    
    // system properties

    public static final String ZTS_CLIENT_PROP_ATHENZ_CONF               = "athenz.athenz_conf";

    public static final String ZTS_CLIENT_PROP_TOKEN_MIN_EXPIRY_TIME     = "athenz.zts.client.token_min_expiry_time";
    public static final String ZTS_CLIENT_PROP_READ_TIMEOUT              = "athenz.zts.client.read_timeout";
    public static final String ZTS_CLIENT_PROP_CONNECT_TIMEOUT           = "athenz.zts.client.connect_timeout";
    public static final String ZTS_CLIENT_PROP_PREFETCH_SLEEP_INTERVAL   = "athenz.zts.client.prefetch_sleep_interval";
    public static final String ZTS_CLIENT_PROP_PREFETCH_AUTO_ENABLE      = "athenz.zts.client.prefetch_auto_enable";
    public static final String ZTS_CLIENT_PROP_X509CERT_DNS_NAME         = "athenz.zts.client.x509cert_dns_name";
    public static final String ZTS_CLIENT_PROP_X509CSR_DN                = "athenz.zts.client.x509csr_dn";
    public static final String ZTS_CLIENT_PROP_X509CSR_DOMAIN            = "athenz.zts.client.x509csr_domain";
    public static final String ZTS_CLIENT_PROP_DISABLE_CACHE             = "athenz.zts.client.disable_cache";

    public static final String ZTS_CLIENT_PROP_CERT_ALIAS                       = "athenz.zts.client.cert_alias";
    
    public static final String ZTS_CLIENT_PROP_KEYSTORE_PATH                    = "athenz.zts.client.keystore_path";
    public static final String ZTS_CLIENT_PROP_KEYSTORE_TYPE                    = "athenz.zts.client.keystore_type";
    public static final String ZTS_CLIENT_PROP_KEYSTORE_PASSWORD                = "athenz.zts.client.keystore_password";
    public static final String ZTS_CLIENT_PROP_KEYSTORE_PWD_APP_NAME            = "athenz.zts.client.keystore_pwd_app_name";
    
    public static final String ZTS_CLIENT_PROP_KEY_MANAGER_PASSWORD             = "athenz.zts.client.keymanager_password";
    public static final String ZTS_CLIENT_PROP_KEY_MANAGER_PWD_APP_NAME         = "athenz.zts.client.keymanager_pwd_app_name";
    
    public static final String ZTS_CLIENT_PROP_TRUSTSTORE_PATH                  = "athenz.zts.client.truststore_path";
    public static final String ZTS_CLIENT_PROP_TRUSTSTORE_TYPE                  = "athenz.zts.client.truststore_type";
    public static final String ZTS_CLIENT_PROP_TRUSTSTORE_PASSWORD              = "athenz.zts.client.truststore_password";
    public static final String ZTS_CLIENT_PROP_TRUSTSTORE_PWD_APP_NAME          = "athenz.zts.client.truststore_pwd_app_name";

    public static final String ZTS_CLIENT_PROP_POOL_MAX_PER_ROUTE               = "athenz.zts.client.http_pool_max_per_route";
    public static final String ZTS_CLIENT_PROP_POOL_MAX_TOTAL                   = "athenz.zts.client.http_pool_max_total";

    public static final String ZTS_CLIENT_PROP_PRIVATE_KEY_STORE_FACTORY_CLASS  = "athenz.zts.client.private_keystore_factory_class";
    public static final String ZTS_CLIENT_PROP_CLIENT_PROTOCOL                  = "athenz.zts.client.client_ssl_protocol";
    public static final String ZTS_CLIENT_PKEY_STORE_FACTORY_CLASS              = "com.yahoo.athenz.auth.impl.FilePrivateKeyStoreFactory";
    public static final String ZTS_CLIENT_DEFAULT_CLIENT_SSL_PROTOCOL           = "TLSv1.2";

    public static final String ROLE_TOKEN_HEADER = System.getProperty(RoleAuthority.ATHENZ_PROP_ROLE_HEADER,
            RoleAuthority.HTTP_HEADER);

    final static ConcurrentHashMap ROLE_TOKEN_CACHE = new ConcurrentHashMap<>();
    final static ConcurrentHashMap ACCESS_TOKEN_CACHE = new ConcurrentHashMap<>();
    final static ConcurrentHashMap AWS_CREDS_CACHE = new ConcurrentHashMap<>();

    private static final Queue PREFETCH_SCHEDULED_ITEMS = new ConcurrentLinkedQueue<>();
    private static Timer FETCH_TIMER;
    private static final Object TIMER_LOCK = new Object();
    static AtomicLong FETCHER_LAST_RUN_AT = new AtomicLong(-1);
    static final ClientKeyRefresherListener KEY_REFRESHER_LISTENER = new ClientKeyRefresherListener();

    // allows outside implementations to get role tokens for special environments - ex. hadoop
    
    private static ServiceLoader ztsTokenProviders;
    private static AtomicReference> svcLoaderCacheKeys;
    private static PrivateKeyStore PRIVATE_KEY_STORE = loadServicePrivateKey();
    private static ZTSAccessTokenFileLoader ztsAccessTokenFileLoader;

    enum TokenType {
        ROLE,
        ACCESS,
        AWS,
        SVC_ROLE
    }

    static boolean initConfigValues() {

        // load our service providers tokens
        
        loadSvcProviderTokens();
        
        // set the token min expiry time

        setTokenMinExpiryTime(Integer.parseInt(System.getProperty(ZTS_CLIENT_PROP_TOKEN_MIN_EXPIRY_TIME, "900")));

        // set the prefetch interval
        
        setPrefetchInterval(Integer.parseInt(System.getProperty(ZTS_CLIENT_PROP_PREFETCH_SLEEP_INTERVAL, "60")));

        // set the prefetch support
        
        setPrefetchAutoEnable(Boolean.parseBoolean(System.getProperty(ZTS_CLIENT_PROP_PREFETCH_AUTO_ENABLE, "true")));
        
        // disable the cache if configured
        
        setCacheDisable(Boolean.parseBoolean(System.getProperty(ZTS_CLIENT_PROP_DISABLE_CACHE, "false")));

        // set x509 csr details
        
        setX509CsrDetails(System.getProperty(ZTS_CLIENT_PROP_X509CSR_DN),
                System.getProperty(ZTS_CLIENT_PROP_X509CSR_DOMAIN));
        
        // set connection timeouts
        
        setConnectionTimeouts(Integer.parseInt(System.getProperty(ZTS_CLIENT_PROP_CONNECT_TIMEOUT, "30000")),
                Integer.parseInt(System.getProperty(ZTS_CLIENT_PROP_READ_TIMEOUT, "30000")));
        
        // set our server certificate dns name
        
        setX509CertDnsName(System.getProperty(ZTS_CLIENT_PROP_X509CERT_DNS_NAME));
        
        // finally retrieve our configuration ZTS url from our config file
        
        lookupZTSUrl();

        // init zts file utility

        initZTSAccessTokenFileLoader();
        
        return true;
    }
    
    /**
     * Set the X509 Cert DNS Name in case ZTS Server is running with
     * a certificate not matching its hostname
     * @param dnsName name of the ZTS Servers X.509 Cert dns value
     */
    public static void setX509CertDnsName(final String dnsName) {
        x509CertDNSName = dnsName;
    }
    
    /**
     * Set request connection and read timeout
     * @param connectTimeout timeout for initial connection in milliseconds
     * @param readTimeout timeout for read response in milliseconds
     */
    public static void setConnectionTimeouts(int connectTimeout, int readTimeout) {
         reqConnectTimeout = connectTimeout;
         reqReadTimeout = readTimeout;
    }
    
    /**
     * Set X509 CSR Details - DN and domain name. These values can be specified
     * in the generate csr function as well in which case these will be ignored.
     * @param csrDn string identifying the dn for the csr without the cn component
     * @param csrDomain string identifying the dns domain for generating SAN fields
     */
    public static void setX509CsrDetails(final String csrDn, final String csrDomain) {
        x509CsrDn = csrDn;
        x509CsrDomain = csrDomain;
    }
    
    /**
     * Disable the cache of role tokens if configured.
     * @param cacheState false to disable the cache
     */
    public static void setCacheDisable(boolean cacheState) {
        cacheDisabled = cacheState;
    }
    
    /**
     * Enable prefetch of role tokens
     * @param fetchState state of prefetch
     */
    public static void setPrefetchAutoEnable(boolean fetchState) {
        prefetchAutoEnable = fetchState;
    }
    
    /**
     * Set the prefetch interval. if the prefetch interval is longer than
     * our token min expiry time, then we'll default back to 60 seconds
     * @param interval time in seconds
     */
    public static void setPrefetchInterval(int interval) {
        prefetchInterval = interval;
        if (prefetchInterval >= tokenMinExpiryTime) {
            prefetchInterval = 60;
        }
    }
    /**
     * Set the minimum token expiry time. The server will not give out tokens
     * less than configured expiry time
     * @param minExpiryTime expiry time in seconds
     */
    public static void setTokenMinExpiryTime(int minExpiryTime) {
        
        // The minimum token expiry time by default is 15 minutes (900). By default the
        // server gives out role tokens for 2 hours and with this setting we'll be able
        // to cache tokens for 1hr45mins before requesting a new one from ZTS

        tokenMinExpiryTime = minExpiryTime;
        if (tokenMinExpiryTime < 0) {
            tokenMinExpiryTime = 900;
        }
    }

    public static void lookupZTSUrl() {
        
        String rootDir = System.getenv("ROOT");
        if (rootDir == null) {
            rootDir = "/home/athenz";
        }
        
        String confFileName = System.getProperty(ZTS_CLIENT_PROP_ATHENZ_CONF,
                rootDir + "/conf/athenz/athenz.conf");

        try {
            Path path = Paths.get(confFileName);
            AthenzConfig conf = JSON.fromBytes(Files.readAllBytes(path), AthenzConfig.class);
            confZtsUrl = conf.getZtsUrl();
        } catch (Exception ex) {
            // if we have a zts client service specified and we have keys
            // in our service loader cache then we're running within
            // some managed framework (e.g. hadoop) so we're going to
            // report this exception as a warning rather than an error
            // and default to localhost as the url to avoid further
            // warnings from our generated client

            LOG.warn("Unable to extract ZTS Url from conf file {}, exc: {}",
                    confFileName, ex.getMessage());
            
            if (!svcLoaderCacheKeys.get().isEmpty()) {
                confZtsUrl = "https://localhost:4443/";
            }
        }
    }

    public static void initZTSAccessTokenFileLoader() {
        if (resolver == null) {
            resolver = new JwtsSigningKeyResolver(null, null);
        }
        ztsAccessTokenFileLoader = new ZTSAccessTokenFileLoader(resolver);
        ztsAccessTokenFileLoader.preload();
    }

    public static void setAccessTokenSignKeyResolver(JwtsSigningKeyResolver jwtsSigningKeyResolver) {
        resolver = jwtsSigningKeyResolver;
    }

    /**
     * Constructs a new ZTSClient object with default settings.
     * The url for ZTS Server is automatically retrieved from the athenz
     * configuration file (ztsUrl field). The client can only be used
     * to retrieve objects from ZTS that do not require any authentication
     * otherwise addCredentials method must be used to set the principal identity.
     * Default read and connect timeout values are 30000ms (30sec).
     * The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in
     * milliseconds.
     */
    public ZTSClient() {
        initClient(null, null, null, null, null);
        enablePrefetch = false; // can't use this domain and service for prefetch
    }
    
    /**
     * Constructs a new ZTSClient object with the given ZTS Server Url.
     * If the specified zts url is null, then it is automatically
     * retrieved from athenz.conf configuration file (ztsUrl field).
     * Default read and connect timeout values are 30000ms (30sec).
     * The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in
     * milliseconds. This client object can only be used for API calls
     * that require no authentication or setting the principal using
     * addCredentials method before calling any other authentication
     * protected API.
     * @param ztsUrl ZTS Server's URL (optional)
     */
    public ZTSClient(String ztsUrl) {
        initClient(ztsUrl, null, null, null, null);
        enablePrefetch = false; // can't use this domain and service for prefetch
    }
    
    /**
     * Constructs a new ZTSClient object with the given principal identity.
     * The url for ZTS Server is automatically retrieved from the athenz
     * configuration file (ztsUrl field). Default read and connect timeout values
     * are 30000ms (30sec). The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in milliseconds.
     * @param identity Principal identity for authenticating requests
     */
    public ZTSClient(Principal identity) {
        this(null, identity);
    }
    
    /**
     * Constructs a new ZTSClient object with the given principal identity
     * and ZTS Server Url. Default read and connect timeout values are
     * 30000ms (30sec). The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in milliseconds.
     * @param ztsUrl ZTS Server's URL (optional)
     * @param identity Principal identity for authenticating requests
     */
    public ZTSClient(String ztsUrl, Principal identity) {
        
        // verify we have a valid principal and authority
        
        if (identity == null) {
            throw new IllegalArgumentException("Principal object must be specified");
        }
        if (identity.getAuthority() == null) {
            throw new IllegalArgumentException("Principal Authority cannot be null");
        }
        initClient(ztsUrl, identity, null, null, null);
        enablePrefetch = false; // can't use this domain and service for prefetch
    }
    
    /**
     * Constructs a new ZTSClient object with the given SSLContext object
     * and ZTS Server Url. Default read and connect timeout values are
     * 30000ms (30sec). The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in milliseconds.
     * @param ztsUrl ZTS Server's URL (optional)
     * @param sslContext SSLContext that includes service's private key and x.509 certificate
     * for authenticating requests
     */
    public ZTSClient(String ztsUrl, SSLContext sslContext) {
        this(ztsUrl, null, sslContext);
    }

    /**
     * Constructs a new ZTSClient object with the given SSLContext object
     * and ZTS Server Url through the specified Proxy URL. Default read
     * and connect timeout values are 30000ms (30sec). The application can
     * change these values by using the athenz.zts.client.read_timeout and
     * athenz.zts.client.connect_timeout system properties. The values
     * specified for timeouts must be in milliseconds.
     * @param ztsUrl ZTS Server's URL
     * @param proxyUrl Proxy Server's URL
     * @param sslContext SSLContext that includes service's private key and x.509 certificate
     * for authenticating requests
     */
    public ZTSClient(String ztsUrl, String proxyUrl, SSLContext sslContext) {

        // verify we have a valid ssl context specified

        if (sslContext == null) {
            throw new IllegalArgumentException("SSLContext object must be specified");
        }
        this.sslContext = sslContext;
        this.proxyUrl = proxyUrl;
        initClient(ztsUrl, null, null, null, null);
    }
    
    /**
     * Constructs a new ZTSClient object with the given service details
     * identity provider (which will provide the ntoken for the service)
     * The ZTS Server url is automatically retrieved from athenz.conf configuration
     * file (ztsUrl field). Default read and connect timeout values are
     * 30000ms (30sec). The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in milliseconds.
     * @param domainName name of the domain
     * @param serviceName name of the service
     * @param siaProvider service identity provider for the client to request principals
     */
    public ZTSClient(String domainName, String serviceName, ServiceIdentityProvider siaProvider) {
        this(null, domainName, serviceName, siaProvider);
    }

    /**
     * Constructs a new ZTSClient object with the given service details
     * identity provider (which will provide the ntoken for the service)
     * and ZTS Server Url. If the specified zts url is null, then it is
     * automatically retrieved from athenz.conf configuration file
     * (ztsUrl field). Default read and connect timeout values are
     * 30000ms (30sec). The application can change these values by using the
     * athenz.zts.client.read_timeout and athenz.zts.client.connect_timeout
     * system properties. The values specified for timeouts must be in milliseconds.
     * @param ztsUrl ZTS Server's URL (optional)
     * @param domainName name of the domain
     * @param serviceName name of the service
     * @param siaProvider service identity provider for the client to request principals
     */
    public ZTSClient(String ztsUrl, String domainName, String serviceName,
                     ServiceIdentityProvider siaProvider) {
        if (isEmpty(domainName)) {
            throw new IllegalArgumentException("Domain name must be specified");
        }
        if (isEmpty(serviceName)) {
            throw new IllegalArgumentException("Service name must be specified");
        }
        if (siaProvider == null) {
            throw new IllegalArgumentException("Service Identity Provider must be specified");
        }
        initClient(ztsUrl, null, domainName, serviceName, siaProvider);
    }
    
    /**
     * Close the ZTSClient object and release any allocated resources.
     */
    @Override
    public void close() {
        ztsClient.close();
    }

    /**
     * Call to enable/disable prefetch for the current ZTSClient.
     * @param state whether prefetch is enabled or not
     */
    public void setEnablePrefetch(boolean state) {
        enablePrefetch = state;
    }

    /**
     * Set new ZTS Client configuration property. This method calls
     * internal athenz.shade.zts.athenz.shade.zts.javax.ws.rs.client.Client client's property method.
     * If already set, the existing value of the property will be updated.
     * Setting a null value into a property effectively removes the property
     * from the property bag.
     * @param name property name.
     * @param value property value. null value removes the property with the given name.
     */
    public void setProperty(String name, Object value) {
        if (ztsClient != null) {
            ztsClient.setProperty(name, value);
        }
    }

    /**
     * Sets a notificationSender that will listen to notifications and send them (usually to domain admins)
     * @param notificationSender notification sender
     */
    public void setNotificationSender(ZTSClientNotificationSender notificationSender) {
        this.notificationSender = notificationSender;
    }

    /**
     * Cancel the Prefetch Timer. This removes all the prefetch
     * items from the list, purges and cancels the fetch timer.
     * This should be called before application shutdown.
     */
    public static void cancelPrefetch() {
        PREFETCH_SCHEDULED_ITEMS.clear();
        if (FETCH_TIMER != null) {
            FETCH_TIMER.purge();
            FETCH_TIMER.cancel();
            FETCH_TIMER = null;
        }
    }
    
    /**
     * Returns the locally configured ZTS Server's URL value
     * @return ZTS Server URL
     */
    public String getZTSUrl() {
        return ztsUrl;
    }
    
    public void setZTSRDLGeneratedClient(ZTSRDLGeneratedClient client) {
        this.ztsClient = client;
        ztsClientOverride = true;
    }

    public void setZTSClientCache(ZTSClientCache ztsClientCache) {
        this.ztsClientCache = ztsClientCache;
    }

    /**
     * Generate the SSLContext object based on give key/cert and truststore
     * files. If configured, the method will monitor any changes in the given
     * key/cert files and automatically update the ssl context.
     * @param trustStorePath path to the trust-store
     * @param trustStorePassword trust store password
     * @param publicCertFile path to the certificate file
     * @param privateKeyFile path to the private key file
     * @param monitorKeyCertUpdates boolean flag whether or not monitor file updates
     * @return SSLContext object
     * @throws InterruptedException interrupt exceptions
     * @throws KeyRefresherException any exceptions from the cert refresher
     * @throws IOException io exceptions
     */
    public SSLContext createSSLContext(final String trustStorePath, final char[] trustStorePassword,
                 final String publicCertFile, final String privateKeyFile, boolean monitorKeyCertUpdates)
            throws InterruptedException, KeyRefresherException, IOException {

        // Create our SSL Context object based on our private key and
        // certificate and jdk truststore

        KeyRefresher keyRefresher = Utils.generateKeyRefresher(trustStorePath, trustStorePassword,
                publicCertFile, privateKeyFile, KEY_REFRESHER_LISTENER);
        SSLContext sslContext = Utils.buildSSLContext(keyRefresher.getKeyManagerProxy(),
                keyRefresher.getTrustManagerProxy());
        if (monitorKeyCertUpdates) {
            keyRefresher.startup();
        }
        return sslContext;
    }

    private SSLContext createSSLContext() {
        
        // to create the SSL context we must have the keystore path
        // specified. If it's not specified, then we are not going
        // to create our ssl context
        
        String keyStorePath = System.getProperty(ZTS_CLIENT_PROP_KEYSTORE_PATH);
        if (isEmpty(keyStorePath)) {
            return null;
        }
        String keyStoreType = System.getProperty(ZTS_CLIENT_PROP_KEYSTORE_TYPE);
        String keyStorePwd = System.getProperty(ZTS_CLIENT_PROP_KEYSTORE_PASSWORD);
        char[] keyStorePassword = null;
        if (!isEmpty(keyStorePwd)) {
            keyStorePassword = keyStorePwd.toCharArray();
        }
        String keyStorePasswordAppName = System.getProperty(ZTS_CLIENT_PROP_KEYSTORE_PWD_APP_NAME);
        char[] keyManagerPassword = null;
        String keyManagerPwd = System.getProperty(ZTS_CLIENT_PROP_KEY_MANAGER_PASSWORD);
        if (!isEmpty(keyManagerPwd)) {
            keyManagerPassword = keyManagerPwd.toCharArray();
        }
        String keyManagerPasswordAppName = System.getProperty(ZTS_CLIENT_PROP_KEY_MANAGER_PWD_APP_NAME);
        
        // truststore
        String trustStorePath = System.getProperty(ZTS_CLIENT_PROP_TRUSTSTORE_PATH);
        String trustStoreType = System.getProperty(ZTS_CLIENT_PROP_TRUSTSTORE_TYPE);
        String trustStorePwd = System.getProperty(ZTS_CLIENT_PROP_TRUSTSTORE_PASSWORD);
        char[] trustStorePassword = null;
        if (!isEmpty(trustStorePwd)) {
            trustStorePassword = trustStorePwd.toCharArray();
        }
        String trustStorePasswordAppName = System.getProperty(ZTS_CLIENT_PROP_TRUSTSTORE_PWD_APP_NAME);
        
        // alias and protocol details
        String certAlias = System.getProperty(ZTS_CLIENT_PROP_CERT_ALIAS);
        String clientProtocol = System.getProperty(ZTS_CLIENT_PROP_CLIENT_PROTOCOL,
                ZTS_CLIENT_DEFAULT_CLIENT_SSL_PROTOCOL);

        ClientSSLContextBuilder builder = new SSLUtils.ClientSSLContextBuilder(clientProtocol)
                .privateKeyStore(PRIVATE_KEY_STORE).keyStorePath(keyStorePath);
        
        if (!isEmpty(certAlias)) {
            builder.certAlias(certAlias);
        }
        if (!isEmpty(keyStoreType)) {
            builder.keyStoreType(keyStoreType);
        }
        if (null != keyStorePassword) {
            builder.keyStorePassword(keyStorePassword);
        }
        if (null != keyStorePasswordAppName) {
            builder.keyStorePasswordAppName(keyStorePasswordAppName);
        }
        if (null != keyManagerPassword) {
            builder.keyManagerPassword(keyManagerPassword);
        }
        if (null != keyManagerPasswordAppName) {
            builder.keyManagerPasswordAppName(keyManagerPasswordAppName);
        }
        if (!isEmpty(trustStorePath)) {
            builder.trustStorePath(trustStorePath);
        }
        if (!isEmpty(trustStoreType)) {
            builder.trustStoreType(trustStoreType);
        }
        if (null != trustStorePassword) {
            builder.trustStorePassword(trustStorePassword);
        }
        if (null != trustStorePasswordAppName) {
            builder.trustStorePasswordAppName(trustStorePasswordAppName);
        }

        return builder.build();
    }
    
    static PrivateKeyStore loadServicePrivateKey() {
        String pkeyFactoryClass = System.getProperty(ZTS_CLIENT_PROP_PRIVATE_KEY_STORE_FACTORY_CLASS,
                ZTS_CLIENT_PKEY_STORE_FACTORY_CLASS);
        return SSLUtils.loadServicePrivateKey(pkeyFactoryClass);
    }

    ClientBuilder getClientBuilder() {
        return ClientBuilder.newBuilder();
    }

    private void initClient(final String serverUrl, Principal identity,
            final String domainName, final String serviceName,
            final ServiceIdentityProvider siaProvider) {
        
        ztsUrl = (serverUrl == null) ? confZtsUrl : serverUrl;
        
        // verify if the url is ending with /zts/v1 and if it's
        // not we'll automatically append it
        
        if (!isEmpty(ztsUrl)) {
            if (!ztsUrl.endsWith("/zts/v1")) {
                if (ztsUrl.charAt(ztsUrl.length() - 1) != '/') {
                    ztsUrl += '/';
                }
                ztsUrl += "zts/v1";
            }
        }

        // determine to see if we need a host verifier for our ssl connections
        
        HostnameVerifier hostnameVerifier = null;
        if (!isEmpty(x509CertDNSName)) {
            hostnameVerifier = new AWSHostNameVerifier(x509CertDNSName);
        }
        
        // if we don't have a ssl context specified, check the system
        // properties to see if we need to create one

        if (sslContext == null) {
            sslContext = createSSLContext();
        }

        // setup our client config object with timeouts

        final JacksonJsonProvider jacksonJsonProvider = new JacksonJaxbJsonProvider()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        final ClientConfig config = new ClientConfig(jacksonJsonProvider);

        PoolingHttpClientConnectionManager connManager = createConnectionManager(sslContext, hostnameVerifier);
        if (connManager != null) {
            config.property(ApacheClientProperties.CONNECTION_MANAGER, connManager);
        }
        config.connectorProvider(new ApacheConnectorProvider());

        // if we're asked to use a proxy for our request
        // we're going to set the property that is supported
        // by the apache connector and use that
        
        if (proxyUrl != null) {
            config.property(ClientProperties.PROXY_URI, proxyUrl);
        }
        
        ClientBuilder builder = getClientBuilder();
        if (sslContext != null) {
            builder = builder.sslContext(sslContext);
            enablePrefetch = true;
        }

        // JerseyClientBuilder::withConfig() replaces the existing config with the new client
        // config. Hence the client config should be added to the builder before the timeouts.
        // Otherwise the timeout settings would be overridden.
        Client rsClient = builder.withConfig(config)
                .hostnameVerifier(hostnameVerifier)
                .readTimeout(reqReadTimeout, TimeUnit.MILLISECONDS)
                .connectTimeout(reqConnectTimeout, TimeUnit.MILLISECONDS)
                .build();

        ztsClient = new ZTSRDLGeneratedClient(ztsUrl, rsClient);
        principal = identity;
        domain = domainName;
        service = serviceName;
        this.siaProvider = siaProvider;
        
        // if we are given a principal object then we need
        // to update the domain/service settings
        
        if (principal != null) {
            domain  = principal.getDomain();
            service = principal.getName();
            ztsClient.addCredentials(identity.getAuthority().getHeader(), identity.getCredentials());
        }
    }

    PoolingHttpClientConnectionManager createConnectionManager(SSLContext sslContext, HostnameVerifier hostnameVerifier) {

        if (sslContext == null) {
            return null;
        }

        SSLConnectionSocketFactory sslSocketFactory;
        if (hostnameVerifier == null) {
            sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
        } else {
            sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
        }
        Registry registry = RegistryBuilder
                .create()
                .register("https", sslSocketFactory)
                .register("http", new PlainConnectionSocketFactory())
                .build();
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);

        // we'll use the default values from apache http connector - max 20 and per route 2

        int maxPerRoute = Integer.parseInt(System.getProperty(ZTS_CLIENT_PROP_POOL_MAX_PER_ROUTE, "2"));
        int maxTotal = Integer.parseInt(System.getProperty(ZTS_CLIENT_PROP_POOL_MAX_TOTAL, "20"));

        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
        poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
        return poolingHttpClientConnectionManager;
    }

    void setPrefetchInterval(long interval) {
        prefetchInterval = interval;
    }
    
    long getPrefetchInterval() {
        return prefetchInterval;
    }
    
    /**
     * Returns the header name that the client needs to use to pass
     * the received RoleToken to the Athenz protected service.
     * @return HTTP header name
     */
    public static String getHeader() {
        return ROLE_TOKEN_HEADER;
    }

    /**
     * Set client credentials based on the given principal.
     * @param identity Principal identity for authenticating requests
     * @return self ZTSClient object
     */
    public ZTSClient addCredentials(Principal identity) {
        return addPrincipalCredentials(identity, true);
    }
    
    /**
     * Set the client credentials using the specified header and token.
     * @param credHeader authentication header name
     * @param credToken authentication credentials
     */
    public void addCredentials(String credHeader, String credToken) {
        ztsClient.addCredentials(credHeader, credToken);
    }
    
    /**
     * Clear the principal identity set for the client. Unless a new principal is set
     * using the addCredentials method, the client can only be used to requests data
     * from the ZTS Server that doesn't require any authentication.
     * @return self ZTSClient object
     */
    public ZTSClient clearCredentials() {
        
        if (principal != null) {
            ztsClient.addCredentials(principal.getAuthority().getHeader(), null);
            principal = null;
        }
        return this;
    }
    
    ZTSClient addPrincipalCredentials(Principal identity, boolean resetServiceDetails) {
        
        if (identity != null && identity.getAuthority() != null) {
            ztsClient.addCredentials(identity.getAuthority().getHeader(), identity.getCredentials());
        }

        // if the client is adding new principal identity then we have to 
        // clear out the sia provider object reference so that we don't try
        // to get a service token since we already have one given to us

        if (resetServiceDetails) {
            siaProvider = null;
        }
        
        principal = identity;
        return this;
    }

    boolean sameCredentialsAsBefore(Principal svcPrincipal) {
        
        // if we don't have a principal or no credentials
        // then the principal has changed
        
        if (principal == null) {
            return false;
        }
        
        final String creds = principal.getCredentials();
        if (creds == null) {
            return false;
        }
        
        return creds.equals(svcPrincipal.getCredentials());
    }
    
    boolean updateServicePrincipal() {
        
        // if we have a service principal then we need to keep updating
        // our PrincipalToken otherwise it might expire.
        
        if (siaProvider == null) {
            return false;
        }
        
        Principal svcPrincipal = siaProvider.getIdentity(domain, service);
        
        // if we get no principal from our sia provider, then we
        // should log and throw an IllegalArgumentException otherwise the
        // client doesn't know that something bad has happened - in this
        // case illegal domain/service was passed to the constructor
        // and the ZTS Server just rejects the request with 401
        
        if (svcPrincipal == null) {
            final String msg = "UpdateServicePrincipal: Unable to get PrincipalToken "
                    + "from SIA Provider for " + domain + "." + service;
            LOG.error(msg);
            throw new IllegalArgumentException(msg);
        }
        
        // if the principal has the same credentials as before
        // then we don't need to update anything
        
        if (sameCredentialsAsBefore(svcPrincipal)) {
            return false;
        }
            
        addPrincipalCredentials(svcPrincipal, false);
        return true;
    }
    
    /**
     * Retrieve list of services that have been configured to run on the specified host
     * @param host name of the host
     * @return list of service names on success. ZTSClientException will be thrown in case of failure
     */
    public HostServices getHostServices(String host) {
        updateServicePrincipal();
        try {
            return ztsClient.getHostServices(host);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve list of ZTS Server public keys in Json WEB Key (JWK) format
     * @param rfcCurveNames EC curve names - use values defined in RFC only
     * @return list of public keys (JWKs) on success. ZTSClientException will be thrown in case of failure
     */
    public JWKList getJWKList(boolean rfcCurveNames) {
        updateServicePrincipal();
        try {
            return ztsClient.getJWKList(rfcCurveNames);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve list of ZTS Server public keys in Json WEB Key (JWK) format
     * @return list of public keys (JWKs) on success. ZTSClientException will be thrown in case of failure
     */
    public JWKList getJWKList() {
        return getJWKList(false);
    }

    /**
     * For the specified requester(user/service) return the corresponding Role Token that
     * includes the list of roles that the principal has access to in the specified domain.
     * The client will automatically fulfill the request from the cache, if possible.
     * The default minimum expiry time is 900 secs (15 mins).
     * @param domainName name of the domain
     * @return ZTS generated Role Token. ZTSClientException will be thrown in case of failure
     */
    public RoleToken getRoleToken(String domainName) {
        return getRoleToken(domainName, null, null, null, false, null);
    }
    
    /**
     * For the specified requester(user/service) return the corresponding Role Token that
     * includes the list of roles that the principal has access to in the specified domain
     * and filtered to include only those that end with the specified suffix.
     * The client will automatically fulfill the request from the cache, if possible.
     * The default minimum expiry time is 900 secs (15 mins).
     * @param domainName name of the domain
     * @param roleNames only interested in roles with these names, comma separated list of roles
     * @return ZTS generated Role Token. ZTSClientException will be thrown in case of failure
     */
    public RoleToken getRoleToken(String domainName, String roleNames) {
        if (isEmpty(roleNames)) {
            throw new IllegalArgumentException("RoleNames cannot be null or empty");
        }
        return getRoleToken(domainName, roleNames, null, null, false, null);
    }
    
    /**
     * For the specified requester(user/service) return the corresponding Role Token that
     * includes the list of roles that the principal has access to in the specified domain
     * @param domainName name of the domain
     * @param roleNames only interested in roles with these names, comma separated list of roles
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @param ignoreCache ignore the cache and retrieve the token from ZTS Server
     * @return ZTS generated Role Token. ZTSClientException will be thrown in case of failure
     */
    public RoleToken getRoleToken(String domainName, String roleNames, Integer minExpiryTime,
            Integer maxExpiryTime, boolean ignoreCache) {
        return getRoleToken(domainName, roleNames, minExpiryTime, maxExpiryTime,
                ignoreCache, null);
    }
    
    /**
     * For the specified requester(user/service) return the corresponding Role Token that
     * includes the list of roles that the principal has access to in the specified domain
     * @param domainName name of the domain
     * @param roleNames only interested in roles with these names, comma separated list of roles
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @param ignoreCache ignore the cache and retrieve the token from ZTS Server
     * @param proxyForPrincipal (optional) this request is proxy for this principal
     * @return ZTS generated Role Token. ZTSClientException will be thrown in case of failure
     */
    public RoleToken getRoleToken(String domainName, String roleNames, Integer minExpiryTime,
            Integer maxExpiryTime, boolean ignoreCache, String proxyForPrincipal) {
        
        RoleToken roleToken;

        // first lookup in our cache to see if it can be satisfied
        // only if we're not asked to ignore the cache

        String cacheKey = null;
        if (!cacheDisabled) {
            cacheKey = getRoleTokenCacheKey(domainName, roleNames, proxyForPrincipal);
            if (cacheKey != null && !ignoreCache) {
                roleToken = lookupRoleTokenInCache(cacheKey, minExpiryTime, maxExpiryTime, tokenMinExpiryTime);
                if (roleToken != null) {
                    return roleToken;
                }
                // start prefetch for this token if prefetch is enabled
                if (enablePrefetch && prefetchAutoEnable) {
                    if (prefetchRoleToken(domainName, roleNames, minExpiryTime, maxExpiryTime,
                            proxyForPrincipal)) {
                        roleToken = lookupRoleTokenInCache(cacheKey, minExpiryTime, maxExpiryTime, tokenMinExpiryTime);
                    }
                    if (roleToken != null) {
                        return roleToken;
                    }
                    LOG.error("GetRoleToken: cache prefetch and lookup error");
                }
            }
        }
        
        // 2nd look in service providers
        //
        for (ZTSClientService provider: ztsTokenProviders) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("getRoleToken: found service provider={}", provider);
            }
            
            // provider needs to know who the client is so we'll be passing
            // the client's domain and service names as the first two fields
            
            roleToken = provider.fetchToken(domain, service, domainName, roleNames,
                    minExpiryTime, maxExpiryTime, proxyForPrincipal);
            if (roleToken != null) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("getRoleToken: service provider={} returns token", provider);
                }
                return roleToken;
            }
        }
        
        // if no hit then we need to request a new token from ZTS
        
        updateServicePrincipal();
        try {
            roleToken = ztsClient.getRoleToken(domainName, roleNames,
                    minExpiryTime, maxExpiryTime, proxyForPrincipal);
        } catch (ResourceException ex) {

            // if we have an entry in our cache then we'll return that
            // instead of returning failure

            if (cacheKey != null && !ignoreCache) {
                roleToken = lookupRoleTokenInCache(cacheKey, null, null, 1);
                if (roleToken != null) {
                    return roleToken;
                }
            }

            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {

            // if we have an entry in our cache then we'll return that
            // instead of returning failure

            if (cacheKey != null && !ignoreCache) {
                roleToken = lookupRoleTokenInCache(cacheKey, null, null, 1);
                if (roleToken != null) {
                    return roleToken;
                }
            }

            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
        
        // need to add the token to our cache. If our principal was
        // updated then we need to retrieve a new cache key
        
        if (!cacheDisabled) {
            if (cacheKey == null) {
                cacheKey = getRoleTokenCacheKey(domainName, roleNames, proxyForPrincipal);
            }
            if (cacheKey != null) {
                ROLE_TOKEN_CACHE.put(cacheKey, roleToken);
            }
        }
        return roleToken;
    }

    /**
     * For the specified requester(user/service) return the corresponding Access Token that
     * includes the list of roles that the principal has access to in the specified domain
     * @param domainName name of the domain
     * @param roleNames (optional) only interested in roles with these names, comma separated list of roles
     * @param expiryTime (optional) specifies that the returned Access must be
     *          at least valid for specified number of seconds. Pass 0 to use
     *          server default timeout.
     * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
     */
    public AccessTokenResponse getAccessToken(String domainName, List roleNames, long expiryTime) {
        return getAccessToken(domainName, roleNames, null, null, null, null, expiryTime, false);
    }

    /**
     * For the specified requester(user/service) return the corresponding Access Token that
     * includes the given role with specified authorization details included as claims.
     * The principal must have access to the role in the specified domain, and the domain
     * administrator must have configured the request authorization details type and fields
     * for the requested role.
     * @param domainName name of the domain
     * @param roleName name of the role
     * @param authorizationDetails additional authorization details to be added as a claim in the access token
     * @param expiryTime (optional) specifies that the returned Access must be
     *          at least valid for specified number of seconds. Pass 0 to use
     *          server default timeout.
     * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
     */
    public AccessTokenResponse getAccessToken(String domainName, String roleName, String authorizationDetails, long expiryTime) {
        return getAccessToken(domainName, Collections.singletonList(roleName), null, null,
                authorizationDetails, null, expiryTime, false);
    }

    /**
     * For the specified requester(user/service) return the corresponding Access Token that
     * includes the list of roles that the principal has access to in the specified domain
     * @param domainName name of the domain
     * @param roleNames (optional) only interested in roles with these names, comma separated list of roles
     * @param idTokenServiceName (optional) as part of the response return an id token whose audience
     *          is the specified service (only service name e.g. api) in the
     *          domainName domain.
     * @param expiryTime (optional) specifies that the returned Access must be
     *          at least valid for specified number of seconds. Pass 0 to use
     *          server default timeout.
     * @param ignoreCache ignore the cache and retrieve the token from ZTS Server
     * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
     */
    public AccessTokenResponse getAccessToken(String domainName, List roleNames,
            String idTokenServiceName, long expiryTime, boolean ignoreCache) {
        return getAccessToken(domainName, roleNames, idTokenServiceName, null, null, null, expiryTime, ignoreCache);
    }

    /**
     * For the specified requester(user/service) return the corresponding Access Token that
     * includes the list of roles that the principal has access to in the specified domain
     * @param domainName name of the domain
     * @param roleNames (optional) only interested in roles with these names, comma separated list of roles
     * @param idTokenServiceName (optional) as part of the response return an id token whose audience
     *          is the specified service (only service name e.g. api) in the
     *          domainName domain.
     * @param proxyForPrincipal (optional) this request is proxy for this principal
     * @param authorizationDetails (optional) rich authorization request details
     * @param expiryTime (optional) specifies that the returned Access must be
     *          at least valid for specified number of seconds. Pass 0 to use
     *          server default timeout.
     * @param ignoreCache ignore the cache and retrieve the token from ZTS Server
     * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
     */
    public AccessTokenResponse getAccessToken(String domainName, List roleNames, String idTokenServiceName,
            String proxyForPrincipal, String authorizationDetails, long expiryTime, boolean ignoreCache) {
        return getAccessToken(domainName, roleNames, idTokenServiceName, proxyForPrincipal, authorizationDetails,
                null, expiryTime, ignoreCache);
    }

    /**
     * For the specified requester(user/service) return the corresponding Access Token that
     * includes the list of roles that the principal has access to in the specified domain
     * @param domainName name of the domain
     * @param roleNames (optional) only interested in roles with these names, comma separated list of roles
     * @param idTokenServiceName (optional) as part of the response return an id token whose audience
     *          is the specified service (only service name e.g. api) in the
     *          domainName domain.
     * @param proxyForPrincipal (optional) this request is proxy for this principal
     * @param authorizationDetails (optional) rich authorization request details
     * @param proxyPrincipalSpiffeUris (optional) comma separated list of spiffe uris of proxy
     *          principals that this token will be routed through
     * @param expiryTime (optional) specifies that the returned Access must be
     *          at least valid for specified number of seconds. Pass 0 to use
     *          server default timeout.
     * @param ignoreCache ignore the cache and retrieve the token from ZTS Server
     * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
     */
    public AccessTokenResponse getAccessToken(String domainName, List roleNames, String idTokenServiceName,
            String proxyForPrincipal, String authorizationDetails, String proxyPrincipalSpiffeUris, long expiryTime,
            boolean ignoreCache) {

        AccessTokenResponse accessTokenResponse = null;

        // first lookup in our cache to see if it can be satisfied
        // only if we're not asked to ignore the cache

        String cacheKey = null;
        if (!cacheDisabled) {
            cacheKey = getAccessTokenCacheKey(domainName, roleNames, idTokenServiceName,
                    proxyForPrincipal, authorizationDetails, proxyPrincipalSpiffeUris);
            if (cacheKey != null && !ignoreCache) {
                accessTokenResponse = lookupAccessTokenResponseInCache(cacheKey, expiryTime);
                if (accessTokenResponse != null) {
                    return accessTokenResponse;
                }
                // start prefetch for this token if prefetch is enabled
                if (enablePrefetch && prefetchAutoEnable) {
                    if (prefetchAccessToken(domainName, roleNames, idTokenServiceName,
                            proxyForPrincipal, authorizationDetails, proxyPrincipalSpiffeUris, expiryTime)) {
                        accessTokenResponse = lookupAccessTokenResponseInCache(cacheKey, expiryTime);
                    }
                    if (accessTokenResponse != null) {
                        return accessTokenResponse;
                    }
                    LOG.error("GetAccessToken: cache prefetch and lookup error");
                }
            }
        }

        // if no hit then we need to look up in disk
        try {
            accessTokenResponse = ztsAccessTokenFileLoader.lookupAccessTokenFromDisk(domainName, roleNames);
        } catch (IOException ex) {
            LOG.error("GetAccessToken: failed to load access token from disk {}", ex.getMessage());
        }

        // if no hit then we need to request a new token from ZTS

        if (accessTokenResponse == null) {
            updateServicePrincipal();
            try {
                final String requestBody = generateAccessTokenRequestBody(domainName, roleNames,
                        idTokenServiceName, proxyForPrincipal, authorizationDetails,
                        proxyPrincipalSpiffeUris, expiryTime);
                accessTokenResponse = ztsClient.postAccessTokenRequest(requestBody);
            } catch (ResourceException ex) {
                if (cacheKey != null && !ignoreCache) {
                    accessTokenResponse = lookupAccessTokenResponseInCache(cacheKey, -1);
                    if (accessTokenResponse != null) {
                        return accessTokenResponse;
                    }
                }
                throw new ZTSClientException(ex.getCode(), ex.getData());
            } catch (Exception ex) {
                if (cacheKey != null && !ignoreCache) {
                    accessTokenResponse = lookupAccessTokenResponseInCache(cacheKey, -1);
                    if (accessTokenResponse != null) {
                        return accessTokenResponse;
                    }
                }
                throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
            }
        }


        // need to add the token to our cache. If our principal was
        // updated then we need to retrieve a new cache key

        if (!cacheDisabled) {
            if (cacheKey == null) {
                cacheKey = getAccessTokenCacheKey(domainName, roleNames, idTokenServiceName,
                        proxyForPrincipal, authorizationDetails, proxyPrincipalSpiffeUris);
            }
            if (cacheKey != null) {
                ACCESS_TOKEN_CACHE.put(cacheKey, new AccessTokenResponseCacheEntry(accessTokenResponse));
            }
        }

        return accessTokenResponse;
    }

    String generateAccessTokenRequestBody(String domainName, List roleNames, String idTokenServiceName,
            String proxyForPrincipal, String authorizationDetails, String proxyPrincipalSpiffeUris,
            long expiryTime) throws UnsupportedEncodingException {

        StringBuilder body = new StringBuilder(256);
        body.append("grant_type=client_credentials");
        if (expiryTime > 0) {
            body.append("&expires_in=").append(expiryTime);
        }

        StringBuilder scope = new StringBuilder(256);
        if (isEmpty(roleNames)) {
            scope.append(domainName).append(":domain");
        } else {
            for (String role : roleNames) {
                if (scope.length() != 0) {
                    scope.append(' ');
                }
                scope.append(domainName).append(AuthorityConsts.ROLE_SEP).append(role);
            }
        }
        if (!isEmpty(idTokenServiceName)) {
            scope.append(" openid ").append(domainName).append(":service.").append(idTokenServiceName);
        }
        final String scopeStr = scope.toString();
        body.append("&scope=").append(URLEncoder.encode(scopeStr, "UTF-8"));

        if (!isEmpty(proxyForPrincipal)) {
            body.append("&proxy_for_principal=").append(URLEncoder.encode(proxyForPrincipal, "UTF-8"));
        }

        if (!isEmpty(authorizationDetails)) {
            body.append("&authorization_details=").append(URLEncoder.encode(authorizationDetails, "UTF-8"));
        }

        if (!isEmpty(proxyPrincipalSpiffeUris)) {
            body.append("&proxy_principal_spiffe_uris=").append(URLEncoder.encode(proxyPrincipalSpiffeUris, "UTF-8"));
        }

        return body.toString();
    }

    /**
     * For the specified requester(user/service) return the corresponding Role Certificate
     * @param domainName name of the domain
     * @param roleName name of the role
     * @param req Role Certificate Request (csr)
     * @return RoleToken that includes client x509 role certificate
     */
    @Deprecated
    public RoleToken postRoleCertificateRequest(String domainName, String roleName,
            RoleCertificateRequest req) {
        
        updateServicePrincipal();
        try {
            return ztsClient.postRoleCertificateRequest(domainName, roleName, req);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getMessage());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * For the specified requester(user/service) return the corresponding Role Certificate
     * @param req Role Certificate Request (csr)
     * @return RoleCertificate that includes client x509 role certificate
     */
    public RoleCertificate postRoleCertificateRequest(RoleCertificateRequest req) {

        updateServicePrincipal();
        try {
            return ztsClient.postRoleCertificateRequestExt(req);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getMessage());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Generate a Role Certificate request that could be sent to ZTS
     * to obtain a X509 Certificate for the requested role.
     * @param principalDomain name of the principal's domain
     * @param principalService name of the principal's service
     * @param roleDomainName name of the domain where role is defined
     * @param roleName name of the role to get a certificate request for
     * @param privateKey private key for the service identity for the caller
     * @param csrDn string identifying the dn for the csr without the cn component
     * @param csrDomain string identifying the dns domain for generating SAN fields
     * @param expiryTime number of seconds to request certificate to be valid for
     * @return RoleCertificateRequest object
     */
    public static RoleCertificateRequest generateRoleCertificateRequest(final String principalDomain,
            final String principalService, final String roleDomainName, final String roleName,
            PrivateKey privateKey, final String csrDn, final String csrDomain, int expiryTime) {
        
        if (principalDomain == null || principalService == null) {
            throw new IllegalArgumentException("Principal's Domain and Service must be specified");
        }
        
        if (roleDomainName == null || roleName == null) {
            throw new IllegalArgumentException("Role DomainName and Name must be specified");
        }
        
        if (csrDomain == null) {
            throw new IllegalArgumentException("X509 CSR Domain must be specified");
        }
        
        // Athenz uses lower case for all elements, so let's
        // generate our dn which will be our role resource value
        
        final String domain = principalDomain.toLowerCase();
        final String service = principalService.toLowerCase();
        
        String dn = "cn=" + roleDomainName.toLowerCase() + AuthorityConsts.ROLE_SEP + roleName.toLowerCase();
        if (csrDn != null) {
            dn = dn.concat(",").concat(csrDn);
        }
        
        // now let's generate our dsnName and email fields which will based on
        // our principal's details

        final String hostName = service + '.' + domain.replace('.', '-') + '.' + csrDomain;
        final String email = domain + "." + service + "@" + csrDomain;
        
        GeneralName[] sanArray = new GeneralName[2];
        sanArray[0] = new GeneralName(GeneralName.dNSName, new DERIA5String(hostName));
        sanArray[1] = new GeneralName(GeneralName.rfc822Name, new DERIA5String(email));
        
        String csr;
        try {
            csr = Crypto.generateX509CSR(privateKey, dn, sanArray);
        } catch (OperatorCreationException | IOException ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }

        return new RoleCertificateRequest().setCsr(csr).setExpiryTime(expiryTime);
    }
    
    /**
     * Generate a Role Certificate request that could be sent to ZTS
     * to obtain a X509 Certificate for the requested role.
     * @param principalDomain name of the principal's domain
     * @param principalService name of the principal's service
     * @param roleDomainName name of the domain where role is defined
     * @param roleName name of the role to get a certificate request for
     * @param privateKey private key for the service identity for the caller
     * @param cloud string identifying the environment, e.g. aws
     * @param expiryTime number of seconds to request certificate to be valid for
     * @return RoleCertificateRequest object
     */
    public static RoleCertificateRequest generateRoleCertificateRequest(final String principalDomain,
            final String principalService, final String roleDomainName, final String roleName,
            final PrivateKey privateKey, final String cloud, int expiryTime) {
        
        if (cloud == null) {
            throw new IllegalArgumentException("Cloud Environment must be specified");
        }
        
        String csrDomain;
        if (x509CsrDomain != null) {
            csrDomain = cloud + "." + x509CsrDomain;
        } else {
            csrDomain = cloud;
        }

        return generateRoleCertificateRequest(principalDomain, principalService,
                roleDomainName, roleName, privateKey, x509CsrDn, csrDomain,
                expiryTime);
    }
    
    /**
     * Generate a Instance Refresh request that could be sent to ZTS to
     * request a TLS certificate for a service.
     * @param principalDomain name of the principal's domain
     * @param principalService name of the principal's service
     * @param privateKey private key for the service identity for the caller
     * @param csrDn string identifying the dn for the csr without the cn component
     * @param csrDomain string identifying the dns domain for generating SAN fields
     * @param expiryTime number of seconds to request certificate to be valid for
     * @return InstanceRefreshRequest object
     */
    public static InstanceRefreshRequest generateInstanceRefreshRequest(final String principalDomain,
            final String principalService, PrivateKey privateKey, final String csrDn,
            final String csrDomain, int expiryTime) {
        
        if (principalDomain == null || principalService == null) {
            throw new IllegalArgumentException("Principal's Domain and Service must be specified");
        }
        
        if (csrDomain == null) {
            throw new IllegalArgumentException("X509 CSR Domain must be specified");
        }
        
        // Athenz uses lower case for all elements, so let's
        // generate our dn which will be based on our service name
        
        final String domain = principalDomain.toLowerCase();
        final String service = principalService.toLowerCase();
        final String cn = domain + "." + service;
        
        String dn = "cn=" + cn;
        if (csrDn != null) {
            dn = dn.concat(",").concat(csrDn);
        }
        
        // now let's generate our dsnName field based on our principal's details

        GeneralName[] sanArray = new GeneralName[2];

        final String hostName = service + '.' + domain.replace('.', '-') + '.' + csrDomain;
        sanArray[0] = new GeneralName(GeneralName.dNSName, new DERIA5String(hostName));

        final String spiffeUri = "spiffe://" + domain + "/sa/" + service;
        sanArray[1] = new GeneralName(GeneralName.uniformResourceIdentifier, new DERIA5String(spiffeUri));

        String csr;
        try {
            csr = Crypto.generateX509CSR(privateKey, dn, sanArray);
        } catch (OperatorCreationException | IOException ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }

        return new InstanceRefreshRequest().setCsr(csr).setExpiryTime(expiryTime);
    }
    
    /**
     * Generate a Instance Refresh request that could be sent to ZTS to
     * request a TLS certificate for a service.
     * @param principalDomain name of the principal's domain
     * @param principalService name of the principal's service
     * @param privateKey private key for the service identity for the caller
     * @param cloud string identifying the environment, e.g. aws
     * @param expiryTime number of seconds to request certificate to be valid for
     * @return InstanceRefreshRequest object
     */
    public static InstanceRefreshRequest generateInstanceRefreshRequest(String principalDomain,
            String principalService, PrivateKey privateKey, String cloud, int expiryTime) {
        
        if (cloud == null) {
            throw new IllegalArgumentException("Cloud Environment must be specified");
        }
        
        String csrDomain;
        if (x509CsrDomain != null) {
            csrDomain = cloud + "." + x509CsrDomain;
        } else {
            csrDomain = cloud;
        }
        
        return generateInstanceRefreshRequest(principalDomain, principalService, privateKey,
                x509CsrDn, csrDomain, expiryTime);
    }
    
    static class TokenPrefetchTask extends TimerTask {
        
        ZTSClient getZTSClient(PrefetchTokenScheduledItem item) {
            
            ZTSClient client;
            if (item.sslContext != null) {
                client = new ZTSClient(item.providedZTSUrl, item.proxyUrl, item.sslContext);
            } else {
                client = new ZTSClient(item.providedZTSUrl, item.identityDomain,
                        item.identityName, item.siaProvider);
            }
            return client;
        }

        boolean shouldRefresh(TokenType tokenType, long currentTime, long lastFetchTime,
                long lastFailTime, long expiryTime) {

            // if the ssl context has been modified since the fetch time
            // we are going to refresh all access tokens since they are cert bound

            if (tokenType == TokenType.ACCESS && lastFetchTime < KEY_REFRESHER_LISTENER.getLastCertRefreshTime()) {
                return true;
            }

            // if we are still half way before the token expires then
            // there is no need to refresh it

            if ((expiryTime - lastFetchTime) / 2 + lastFetchTime > currentTime) {
                return false;
            }

            // if we have no failures then we haven't refreshed so
            // we should refresh it now

            if (lastFailTime == 0) {
                return true;
            }

            // otherwise we're going to do the same check to make sure
            // we're half way since the last failed time so we'll
            // progressively try to refresh frequently until we
            // a successful response from ZTS Server

            return (expiryTime - lastFailTime) / 2 + lastFailTime <= currentTime;
        }

        @Override
        public void run() {

            long currentTime = System.currentTimeMillis() / 1000;
            FETCHER_LAST_RUN_AT.set(currentTime);
            
            if (LOG.isDebugEnabled()) {
                LOG.debug("PrefetchTask: Fetching tokens from the scheduled queue. Size={}",
                        PREFETCH_SCHEDULED_ITEMS.size());
            }
            if (PREFETCH_SCHEDULED_ITEMS.isEmpty()) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("PrefetchTask: No items to fetch. Queue is empty");
                }
                return;
            }
            
            List toFetch = new ArrayList<>();

            // if this item is to be fetched now, add it to collection
            // special handling for service tokens so we'll keep track
            // of if we have any expired service tokens

            boolean svcTokenRefresh = false;
            for (PrefetchTokenScheduledItem item : PREFETCH_SCHEDULED_ITEMS) {

                // see if item requires refresh

                if (LOG.isDebugEnabled()) {
                    final String itemName = item.sslContext == null ?
                            item.identityDomain + "." + item.identityName : item.sslContext.toString();
                    LOG.debug("PrefetchTask: item={} type={} domain={} roleName={} fetch/fail/expire times {}/{}/{}",
                            itemName, item.tokenType, item.domainName, item.roleName, item.fetchTime,
                            item.lastFailTime, item.expiresAtUTC);
                }
                if (shouldRefresh(item.tokenType, currentTime, item.fetchTime, item.lastFailTime, item.expiresAtUTC)) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("PrefetchTask: domain={} roleName={}. Refresh this item.",
                                item.domainName, item.roleName);
                    }
                    toFetch.add(item);
                    if (item.tokenType == TokenType.SVC_ROLE) {
                        svcTokenRefresh = true;
                    }
                }
            }
            
            // if toFetch is not empty, fetch those tokens, and add refreshed
            // scheduled items back to the queue
            
            if (toFetch.isEmpty()) {
                return;
            }

            // if we have any service tokens then we need to fetch
            // the new set from the provider

            Set svcLoaderCache = null;
            if (svcTokenRefresh) {
                try {
                    svcLoaderCache = loadSvcProviderTokens();
                } catch (Exception ex) {
                    LOG.error("Unable to load service provider tokens", ex);
                }
            }

            // fetch items

            for (PrefetchTokenScheduledItem item : toFetch) {

                // create ZTS Client for this particular item

                try (ZTSClient itemZtsClient = getZTSClient(item)) {
                    processPrefetchTask(item, itemZtsClient, svcLoaderCache, currentTime);
                }
            }

            // clean our temporary list

            toFetch.clear();
        }
    }

    static void processPrefetchTask(PrefetchTokenScheduledItem item, ZTSClient itemZtsClient,
            Set svcLoaderCache, long currentTime) {

        // use the zts client if one was given however we need
        // reset back to the original client so we don't close
        // our given client

        ZTSRDLGeneratedClient savedZtsClient = itemZtsClient.ztsClient;
        if (item.ztsClient != null) {
            itemZtsClient.ztsClient = item.ztsClient;
        }

        try {
            switch (item.tokenType) {

                case ROLE:

                    RoleToken token = itemZtsClient.getRoleToken(item.domainName, item.roleName,
                            item.minDuration, item.maxDuration, true, item.proxyForPrincipal);

                    // update the expiry time

                    item.setExpiresAtUTC(token.getExpiryTime());
                    break;

                case ACCESS:

                    AccessTokenResponse response = itemZtsClient.getAccessToken(item.domainName, item.roleNames,
                            item.idTokenServiceName, item.proxyForPrincipal, item.authorizationDetails, item.maxDuration, true);

                    // update the expiry time

                    item.setExpiresAtUTC(System.currentTimeMillis() / 1000 + response.getExpires_in());
                    break;

                case AWS:

                    AWSTemporaryCredentials awsCred = itemZtsClient.getAWSTemporaryCredentials(item.domainName,
                            item.roleName, item.externalId, item.minDuration, item.maxDuration, true);

                    // update the expiry time

                    item.setExpiresAtUTC(awsCred.getExpiration().millis() / 1000);
                    break;

                case SVC_ROLE:

                    // check if the key is in the new key set
                    // if not, mark the item as invalid and
                    // remove it from the fetch list

                    if (svcLoaderCache != null && !svcLoaderCache.contains(item.cacheKey)) {
                        item.setIsInvalid(true);
                        PREFETCH_SCHEDULED_ITEMS.remove(item);
                    }
                    break;
            }

            // update the fetch/fail times

            item.setFetchTime(currentTime);
            item.setLastFailTime(0);

        } catch (ZTSClientException ex) {

            LOG.error("PrefetchTask: Error while trying to prefetch token", ex);

            // if we get either invalid credential or the request
            // is forbidden then there is no point of retrying
            // so we'll mark the item as invalid otherwise we'll
            // keep the item in the queue and retry later

            int code = ex.getCode();
            if (code == ResourceException.UNAUTHORIZED || code == ResourceException.FORBIDDEN) {

                // we need to mark it as invalid and then remove it from
                // our list so that we don't match other active requests
                // that might have been already added to the queue

                item.setIsInvalid(true);
                item.setLastFailTime(currentTime);
                PREFETCH_SCHEDULED_ITEMS.remove(item);
            } else {
                item.setLastFailTime(currentTime);
            }

        } catch (Exception ex) {

            // any other exception should remove this item from fetch queue
            // we need to mark it as invalid and then remove it from
            // our list so that we don't match other active requests
            // that might have been already added to the queue

            item.setLastFailTime(currentTime);
            item.setIsInvalid(true);
            PREFETCH_SCHEDULED_ITEMS.remove(item);
            LOG.error("PrefetchTask: Error while trying to prefetch token", ex);
        }

        if (item.shouldSendNotification()) {
            ZTSClientNotification ztsClientNotification = new ZTSClientNotification(
                    itemZtsClient.getZTSUrl(),
                    item.roleName,
                    item.tokenType.toString(),
                    item.expiresAtUTC,
                    item.isInvalid,
                    item.domainName);
            item.lastNotificationTime = currentTime;
            item.notificationSender.sendNotification(ztsClientNotification);
        }

        // don't forget to restore the original client if case
        // we had overridden with the caller specified client

        itemZtsClient.ztsClient = savedZtsClient;
    }

    // method useful for test purposes only
    int getScheduledItemsSize() {
        return PREFETCH_SCHEDULED_ITEMS.size();
    }
    
    /**
     * Pre-fetches role tokens so that the client does not take the hit of
     * contacting ZTS Server for its first request (avg ~75ms). The client
     * library will automatically try to keep the cache up to date such
     * that the tokens are never expired and regular getRoleToken requests
     * are fulfilled from the cache instead of contacting ZTS Server.
     * @param domainName name of the domain
     * @param roleName (optional) only interested in roles with this name
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @return true if all is well, else false
     */
    boolean prefetchRoleToken(String domainName, String roleName,
            Integer minExpiryTime, Integer maxExpiryTime) {
        
        return prefetchRoleToken(domainName, roleName, minExpiryTime, maxExpiryTime, null);
    }
    
    /**
     * Pre-fetches role tokens so that the client does not take the hit of
     * contacting ZTS Server for its first request (avg ~75ms). The client
     * library will automatically try to keep the cache up to date such
     * that the tokens are never expired and regular getRoleToken requests
     * are fulfilled from the cache instead of contacting ZTS Server.
     * @param domainName name of the domain
     * @param roleName (optional) only interested in roles with this name
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @param proxyForPrincipal (optional) request is proxy for this principal
     * @return true if all is well, else false
     */
    boolean prefetchRoleToken(String domainName, String roleName,
            Integer minExpiryTime, Integer maxExpiryTime, String proxyForPrincipal) {
        
        if (domainName == null || domainName.trim().isEmpty()) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, "Domain Name cannot be empty");
        }
        
        RoleToken token = getRoleToken(domainName, roleName, minExpiryTime, maxExpiryTime,
                true, proxyForPrincipal);
        if (token == null) {
            LOG.error("PrefetchToken: No token fetchable using domain={}, roleSuffix={}",
                        domainName, roleName);
            return false;
        }
        long expiryTimeUTC = token.getExpiryTime();
        
        return prefetchToken(domainName, roleName, null, minExpiryTime, maxExpiryTime,
                proxyForPrincipal, null, null, null, expiryTimeUTC, TokenType.ROLE);
    }
    
    boolean prefetchAwsCreds(String domainName, String roleName, String externalId,
            Integer minExpiryTime, Integer maxExpiryTime) {
        
        if (domainName == null || domainName.trim().isEmpty()) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, "Domain Name cannot be empty");
        }

        AWSTemporaryCredentials awsCred = getAWSTemporaryCredentials(domainName, roleName,
                externalId, minExpiryTime, maxExpiryTime, true);
        if (awsCred == null) {
            LOG.error("PrefetchToken: No aws credential fetchable using domain={}, roleName={}",
                        domainName, roleName);
            return false;
        }
        long expiryTimeUTC = awsCred.getExpiration().millis() / 1000;

        return prefetchToken(domainName, roleName, null, minExpiryTime, maxExpiryTime, null,
                externalId, null, null, expiryTimeUTC, TokenType.AWS);
    }

    public boolean prefetchAccessToken(String domainName, List roleNames,
            String idTokenServiceName, String proxyForPrincipal, String authorizationDetails,
            String proxyPrincipalSpiffeUris, long expiryTime) {

        if (domainName == null || domainName.trim().isEmpty()) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, "Domain Name cannot be empty");
        }

        AccessTokenResponse tokenResponse = getAccessToken(domainName, roleNames, idTokenServiceName,
                proxyForPrincipal, authorizationDetails, proxyPrincipalSpiffeUris, expiryTime, true);
        if (tokenResponse == null) {
            LOG.error("PrefetchToken: No access token fetchable using domain={}", domainName);
            return false;
        }
        long expiryTimeUTC = System.currentTimeMillis() / 1000 + tokenResponse.getExpires_in();

        return prefetchToken(domainName, null, roleNames, null, (int) expiryTime,
                proxyForPrincipal, null, idTokenServiceName, authorizationDetails,
                expiryTimeUTC, TokenType.ACCESS);
    }

    boolean prefetchToken(String domainName, String roleName, List roleNames,
            Integer minExpiryTime, Integer maxExpiryTime, String proxyForPrincipal,
            String externalId, String idTokenServiceName, String authorizationDetails,
            long expiryTimeUTC, TokenType tokenType) {
        
        // if we're given a ssl context then we don't have domain/service
        // settings configured otherwise those are required
        
        if (sslContext == null) {
            if (isEmpty(domain) || isEmpty(service)) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn("PrefetchToken: setup failure. Both domain({}) and service({}) are required",
                            domain, service);
                }
                return false;
            }
        }

        PrefetchTokenScheduledItem item = new PrefetchTokenScheduledItem()
                .setTokenType(tokenType)
                .setFetchTime(System.currentTimeMillis() / 1000)
                .setDomainName(domainName)
                .setRoleName(roleName)
                .setRoleNames(roleNames)
                .setProxyForPrincipal(proxyForPrincipal)
                .setExternalId(externalId)
                .setMinDuration(minExpiryTime)
                .setMaxDuration(maxExpiryTime)
                .setExpiresAtUTC(expiryTimeUTC)
                .setIdTokenServiceName(idTokenServiceName)
                .setAuthorizationDetails(authorizationDetails)
                .setIdentityDomain(domain)
                .setIdentityName(service)
                .setTokenMinExpiryTime(ZTSClient.tokenMinExpiryTime)
                .setProvidedZTSUrl(this.ztsUrl)
                .setSiaIdentityProvider(siaProvider)
                .setSslContext(sslContext)
                .setProxyUrl(proxyUrl)
                .setNotificationSender(notificationSender);
        
        // include our zts client only if it was overridden by
        // the caller (most likely for unit test mock)
        
        if (ztsClientOverride) {
             item.setZtsClient(this.ztsClient);
        }

        // we need to make sure we don't have duplicates in
        // our prefetch list so since we got a brand new
        // token now we're going to remove any others we have
        // in the list and add this one. Our items equals
        // method defines what attributes we're looking for
        // when comparing two items.

        PREFETCH_SCHEDULED_ITEMS.remove(item);
        PREFETCH_SCHEDULED_ITEMS.add(item);

        startPrefetch();
        return true;
    }

    String getAccessTokenCacheKey(String domainName, List roleNames, String idTokenServiceName,
            String proxyForPrincipal, String authorizationDetails, String proxyPrincipalSpiffeUris) {

        // if we don't have a tenant domain specified but we have a ssl context
        // then we're going to use the hash code for our sslcontext as the
        // value for our tenant

        String tenantDomain = domain;
        if (domain == null && sslContext != null) {
            tenantDomain = sslContext.toString();
        }
        return getAccessTokenCacheKey(tenantDomain, service, domainName, roleNames,
                idTokenServiceName, proxyForPrincipal, authorizationDetails, proxyPrincipalSpiffeUris);
    }

    String getAccessTokenCacheKey(String tenantDomain, String tenantService, String domainName,
            List roleNames, String idTokenServiceName, String proxyForPrincipal,
            String authorizationDetails, String proxyPrincipalSpiffeUris) {

        // before we generate a cache key we need to have a valid domain

        if (tenantDomain == null) {
            return null;
        }

        StringBuilder cacheKey = new StringBuilder(256);
        cacheKey.append("p=");
        cacheKey.append(tenantDomain);
        if (tenantService != null) {
            cacheKey.append(".").append(tenantService);
        }

        cacheKey.append(";d=");
        cacheKey.append(domainName);

        if (!isEmpty(roleNames)) {
            cacheKey.append(";r=");
            cacheKey.append(ZTSClient.multipleRoleKey(roleNames));
        }

        if (!isEmpty(idTokenServiceName)) {
            cacheKey.append(";o=");
            cacheKey.append(idTokenServiceName);
        }

        if (!isEmpty(proxyForPrincipal)) {
            cacheKey.append(";u=");
            cacheKey.append(proxyForPrincipal);
        }

        if (!isEmpty(authorizationDetails)) {
            cacheKey.append(";z=");
            cacheKey.append(Base64.getUrlEncoder().withoutPadding().encodeToString(Crypto.sha256(authorizationDetails)));
        }

        if (!isEmpty(proxyPrincipalSpiffeUris)) {
            cacheKey.append(";s=");
            cacheKey.append(proxyPrincipalSpiffeUris);
        }

        return cacheKey.toString();
    }

    String getRoleTokenCacheKey(String domainName, String roleName, String proxyForPrincipal) {

        // if we don't have a tenant domain specified but we have a ssl context
        // then we're going to use the hash code for our sslcontext as the
        // value for our tenant
        
        String tenantDomain = domain;
        if (domain == null && sslContext != null) {
            tenantDomain = sslContext.toString();
        }
        return getRoleTokenCacheKey(tenantDomain, service, domainName, roleName, proxyForPrincipal);
    }
    
    static String getRoleTokenCacheKey(String tenantDomain, String tenantService, String domainName,
            String roleName, String proxyForPrincipal) {

        // before we generate a cache key we need to have a valid domain

        if (tenantDomain == null) {
            return null;
        }
        
        StringBuilder cacheKey = new StringBuilder(256);
        cacheKey.append("p=");
        cacheKey.append(tenantDomain);
        if (tenantService != null) {
            cacheKey.append(".").append(tenantService);
        }

        cacheKey.append(";d=");
        cacheKey.append(domainName);

        if (roleName != null && !roleName.isEmpty()) {
            cacheKey.append(";r=");
            
            // check to see if we have multiple roles in the values
            // in which case we need to sort the values
            
            if (roleName.indexOf(',') == -1) {
                cacheKey.append(roleName);
            } else {
                List roles = Arrays.asList(roleName.split(","));
                cacheKey.append(ZTSClient.multipleRoleKey(roles));
            }
        }
        
        if (proxyForPrincipal != null && !proxyForPrincipal.isEmpty()) {
            cacheKey.append(";u=");
            cacheKey.append(proxyForPrincipal);
        }
        
        return cacheKey.toString();
    }
    
    static boolean isExpiredToken(long expiryTime, Integer minExpiryTime, Integer maxExpiryTime,
            int tokenMinExpiryTime) {

        // we'll first make sure if we're given both min and max expiry
        // times then both conditions are satisfied

        if (minExpiryTime != null && expiryTime < minExpiryTime) {
            return true;
        }

        // it's possible the time on the client side is not in sync
        // so we'll allow up to 5 minute offset

        if (maxExpiryTime != null && expiryTime > maxExpiryTime + tokenMaxExpiryOffset) {
            return true;
        }

        // if both limits were null then we need to make sure
        // that our token is valid for based on our min configured value

        return minExpiryTime == null && maxExpiryTime == null && expiryTime < tokenMinExpiryTime;
    }
    
    RoleToken lookupRoleTokenInCache(String cacheKey, Integer minExpiryTime, Integer maxExpiryTime, int serverMinExpiryTime) {

        RoleToken roleToken = ROLE_TOKEN_CACHE.get(cacheKey);
        if (roleToken == null) {
            if (LOG.isInfoEnabled()) {
                LOG.info("LookupRoleTokenInCache: cache-lookup key: {} result: not found", cacheKey);
            }
            return null;
        }
        
        // before returning our cache hit we need to make sure it
        // satisfies the time requirements as specified by the client
        
        long expiryTime = roleToken.getExpiryTime() - (System.currentTimeMillis() / 1000);
        
        if (isExpiredToken(expiryTime, minExpiryTime, maxExpiryTime, serverMinExpiryTime)) {
            
            if (LOG.isInfoEnabled()) {
                LOG.info("LookupRoleTokenInCache: role-cache-lookup key: {} token-expiry: {}"
                        + " req-min-expiry: {} req-max-expiry: {} client-min-expiry: {} result: expired",
                        cacheKey, expiryTime, minExpiryTime, maxExpiryTime, serverMinExpiryTime);
            }

            // if the token is completely expired then we'll remove it from the cache

            if (expiryTime < 1) {
                ROLE_TOKEN_CACHE.remove(cacheKey);
            }

            return null;
        }
        
        return roleToken;
    }

    AccessTokenResponse lookupAccessTokenResponseInCache(String cacheKey, long expiryTime) {

        AccessTokenResponseCacheEntry accessTokenResponseCacheEntry = ACCESS_TOKEN_CACHE.get(cacheKey);
        if (accessTokenResponseCacheEntry == null) {
            if (LOG.isInfoEnabled()) {
                LOG.info("LookupAccessTokenResponseInCache: cache-lookup key: {} result: not found", cacheKey);
            }
            return null;
        }

        // before returning our cache hit we need to make sure it
        // it was at least 1/4th time left before the token expires
        // if the expiryTime is -1 then we return the token as
        // long as its not expired

        if (accessTokenResponseCacheEntry.isExpired(expiryTime)) {
            if (accessTokenResponseCacheEntry.isExpired(-1)) {
                ACCESS_TOKEN_CACHE.remove(cacheKey);
            }
            return null;
        }

        return accessTokenResponseCacheEntry.accessTokenResponse();
    }

    AWSTemporaryCredentials lookupAwsCredInCache(String cacheKey, Integer minExpiryTime,
            Integer maxExpiryTime) {

        AWSTemporaryCredentials awsCred = AWS_CREDS_CACHE.get(cacheKey);
        if (awsCred == null) {
            if (LOG.isInfoEnabled()) {
                LOG.info("LookupAwsCredInCache: aws-cache-lookup key: {} result: not found", cacheKey);
            }
            return null;
        }
        
        // before returning our cache hit we need to make sure it
        // satisfies the time requirements as specified by the client
        
        long expiryTime = awsCred.getExpiration().millis() - System.currentTimeMillis();
        expiryTime /= 1000;  // expiry time is in seconds
        
        if (isExpiredToken(expiryTime, minExpiryTime, maxExpiryTime, tokenMinExpiryTime)) {
            
            if (LOG.isInfoEnabled()) {
                LOG.info("LookupAwsCredInCache: aws-cache-lookup key: {} token-expiry: {}"
                        + " req-min-expiry: {} req-max-expiry: {} client-min-expiry: {} result: expired",
                        cacheKey, expiryTime, minExpiryTime, maxExpiryTime, tokenMinExpiryTime);
            }

            // if the token is completely expired then we'll remove it from the cache

            if (expiryTime < 1) {
                AWS_CREDS_CACHE.remove(cacheKey);
            }
            return null;
        }
        
        return awsCred;
    }

    /**
     * Retrieve the list of roles that the given principal has access to in the domain
     * @param domainName name of the domain
     * @param principal name of the principal
     * @return RoleAccess object on success. ZTSClientException will be thrown in case of failure
     */
    public RoleAccess getRoleAccess(String domainName, String principal) {
        updateServicePrincipal();

        // Try to fetch from cache.
        ZTSClientCache.DomainAndPrincipal cacheKey = null;
        Cache cache = ztsClientCache.getRoleAccessCache();
        if (cache != null) {
            cacheKey = new ZTSClientCache.DomainAndPrincipal(domainName, principal);
            RoleAccess cachedValue = cache.get(cacheKey);
            if (cachedValue != null) {
                return cachedValue;
            }
        }

        try {
            RoleAccess roleAccess = ztsClient.getRoleAccess(domainName, principal);
            if (cache != null) {
                cache.put(cacheKey, roleAccess);
            }
            return roleAccess;
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getMessage());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Retrieve the specified service object from a domain
     * @param domainName name of the domain
     * @param serviceName name of the service to be retrieved
     * @return ServiceIdentity object on success. ZTSClientException will be thrown in case of failure
     */
    public ServiceIdentity getServiceIdentity(String domainName, String serviceName) {
        updateServicePrincipal();
        try {
            return ztsClient.getServiceIdentity(domainName, serviceName);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve the specified public key from the given service object
     * @param domainName name of the domain
     * @param serviceName name of the service
     * @param keyId the identifier of the public key to be retrieved
     * @return PublicKeyEntry object or ZTSClientException will be thrown in case of failure
     */
    public PublicKeyEntry getPublicKeyEntry(String domainName, String serviceName, String keyId) {
        try {
            return ztsClient.getPublicKeyEntry(domainName, serviceName, keyId);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Retrieve the full list of services defined in a domain
     * @param domainName name of the domain
     * @return list of all service names on success. ZTSClientException will be thrown in case of failure
     */
    public ServiceIdentityList getServiceIdentityList(String domainName) {
        updateServicePrincipal();
        try {
            return ztsClient.getServiceIdentityList(domainName);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * For a given provider domain get a list of tenant domain names that the user is a member of
     * @param providerDomainName name of the provider domain
     * @param userName is the name of the user to search for in the tenant domains of the provider
     * @param roleName is the name of the role to filter on when searching through the list of tenants with
     *        the specified role name.
     * @param serviceName is the name of the service to filter on that the tenant has on-boarded to
     * @return TenantDomains object which contains a list of tenant domain names for a given provider 
     *         domain, that the user is a member of
     */
    public TenantDomains getTenantDomains(String providerDomainName, String userName,
            String roleName, String serviceName) {
        updateServicePrincipal();
        try {
            return ztsClient.getTenantDomains(providerDomainName, userName, roleName, serviceName);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Request by a service to refresh its NToken. The original NToken must have been
     * obtained by an authorized service by calling the postInstanceTenantRequest
     * method.
     * @param domain Name of the domain
     * @param service Name of the service
     * @param req InstanceRefreshRequest object for th request
     * @return Identity object that includes a refreshed NToken for the service
     */
    public Identity postInstanceRefreshRequest(String domain, String service, InstanceRefreshRequest req) {
        updateServicePrincipal();
        try {
            return ztsClient.postInstanceRefreshRequest(domain, service, req);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * For AWS Lambda functions generate a new private key, request a
     * x.509 certificate based on the requested CSR and return both to
     * the client in order to establish tls connections with other
     * Athenz enabled services.
     * @param domainName name of the domain
     * @param serviceName name of the service
     * @param account AWS account name that the function runs in
     * @param provider name of the provider service for AWS Lambda
     * @return AWSLambdaIdentity with private key and certificate
     */
    public AWSLambdaIdentity getAWSLambdaServiceCertificate(String domainName,
            String serviceName, String account, String provider) {
        
        if (domainName == null || serviceName == null) {
            throw new IllegalArgumentException("Domain and Service must be specified");
        }
        
        if (account == null || provider == null) {
            throw new IllegalArgumentException("AWS Account and Provider must be specified");
        }
        
        if (x509CsrDomain == null) {
            throw new IllegalArgumentException("X509 CSR Domain must be specified");
        }
        
        // first we're going to generate a private key for the request
        
        AWSLambdaIdentity lambdaIdentity = new AWSLambdaIdentity();
        try {
            lambdaIdentity.setPrivateKey(Crypto.generateRSAPrivateKey(2048));
        } catch (CryptoException ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }

        // we need to generate an csr with an instance register object
        
        InstanceRegisterInformation info = new InstanceRegisterInformation();
        info.setDomain(domainName.toLowerCase());
        info.setService(serviceName.toLowerCase());
        info.setProvider(provider.toLowerCase());

        final String athenzService = info.getDomain() + "." + info.getService();
        
        // generate our dn which will be based on our service name

        StringBuilder dnBuilder = new StringBuilder(128);
        dnBuilder.append("cn=");
        dnBuilder.append(athenzService);
        if (x509CsrDn != null) {
            dnBuilder.append(',');
            dnBuilder.append(x509CsrDn);
        }
        
        // now let's generate our dsnName field based on our principal's details

        GeneralName[] sanArray = new GeneralName[3];
        final String hostBuilder = info.getService() + '.' + info.getDomain().replace('.', '-') +
                '.' + x509CsrDomain;
        sanArray[0] = new GeneralName(GeneralName.dNSName, new DERIA5String(hostBuilder));

        final String instanceHostBuilder = "lambda-" + account + '-' + info.getService() +
                ".instanceid.athenz." + x509CsrDomain;
        sanArray[1] = new GeneralName(GeneralName.dNSName, new DERIA5String(instanceHostBuilder));

        final String spiffeUri = "spiffe://" + info.getDomain() + "/sa/" + info.getService();
        sanArray[2] = new GeneralName(GeneralName.uniformResourceIdentifier, new DERIA5String(spiffeUri));

        // next generate the csr based on our private key and data
        
        try {
            info.setCsr(Crypto.generateX509CSR(lambdaIdentity.getPrivateKey(),
                    dnBuilder.toString(), sanArray));
        } catch (OperatorCreationException | IOException ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
        
        // finally obtain attestation data for lambda
        
        info.setAttestationData(getAWSLambdaAttestationData(athenzService, account));
        
        // request the x.509 certificate from zts server
        
        Map> responseHeaders = new HashMap<>();
        InstanceIdentity identity = postInstanceRegisterInformation(info, responseHeaders);
        
        try {
            lambdaIdentity.setX509Certificate(Crypto.loadX509Certificate(identity.getX509Certificate()));
        } catch (CryptoException ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }

        lambdaIdentity.setCaCertificates(identity.getX509CertificateSigner());
        return lambdaIdentity;
    }
    
    String getAWSLambdaAttestationData(final String athenzService, final String account) {
        
        AWSAttestationData data = new AWSAttestationData();
        data.setRole(athenzService);
        
        Credentials awsCreds = assumeAWSRole(account, athenzService);
        data.setAccess(awsCreds.getAccessKeyId());
        data.setSecret(awsCreds.getSecretAccessKey());
        data.setToken(awsCreds.getSessionToken());
        
        ObjectMapper mapper = new ObjectMapper();
        String jsonData = null;
        try {
            jsonData = mapper.writeValueAsString(data);
        } catch (JsonProcessingException ex) {
            LOG.error("Unable to generate attestation json data: {}", ex.getMessage());
        }
        
        return jsonData;
    }
    
    AssumeRoleRequest getAssumeRoleRequest(String account, String roleName) {
        
        // assume the target role to get the credentials for the client
        // aws format is arn:aws:iam:::role/
        
        final String arn = "arn:aws:iam::" + account + ":role/" + roleName;
        
        AssumeRoleRequest req = new AssumeRoleRequest();
        req.setRoleArn(arn);
        req.setRoleSessionName(roleName);
        
        return req;
    }
    
    Credentials assumeAWSRole(String account, String roleName) {
        
        try {
            AssumeRoleRequest req = getAssumeRoleRequest(account, roleName);
            return AWSSecurityTokenServiceClientBuilder.defaultClient().assumeRole(req).getCredentials();
        } catch (Exception ex) {
            LOG.error("assumeAWSRole - unable to assume role: {}", ex.getMessage());
            return null;
        }
    }
    
    /**
     * AWSCredential Provider provides AWS Credentials which the caller can
     * use to authorize an AWS request. It automatically refreshes the credentials
     * when the current credentials become invalid.
     * It uses ZTS client to refresh the AWS Credentials. So the ZTS Client must
     * not be closed while the credential provider is being used.
     * The caller should close the client when the provider is no longer required.
     * For a given domain and role return AWS temporary credential provider
     * @param domainName name of the domain
     * @param roleName is the name of the role
     * @return AWSCredentialsProvider AWS credential provider
     */
    public AWSCredentialsProvider getAWSCredentialProvider(String domainName, String roleName) {
        return new AWSCredentialsProviderImpl(this, domainName, roleName);
    }

    /**
     * AWSCredential Provider provides AWS Credentials which the caller can
     * use to authorize an AWS request. It automatically refreshes the credentials
     * when the current credentials become invalid.
     * It uses ZTS client to refresh the AWS Credentials. So the ZTS Client must
     * not be closed while the credential provider is being used.
     * The caller should close the client when the provider is no longer required.
     * For a given domain and role return AWS temporary credential provider
     * @param domainName name of the domain
     * @param roleName is the name of the role
     * @param externalId (optional) external id to satisfy configured assume role condition
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @return AWSCredentialsProvider AWS credential provider
     */
    public AWSCredentialsProvider getAWSCredentialProvider(String domainName, String roleName,
            String externalId, Integer minExpiryTime, Integer maxExpiryTime) {
        return new AWSCredentialsProviderImpl(this, domainName, roleName, externalId,
                minExpiryTime, maxExpiryTime);
    }

    /**
     * For a given domain and role return AWS temporary credentials. If the token
     * is present in the local cache and not expired, it will be returned.
     *
     * @param domainName name of the domain
     * @param roleName is the name of the role
     * @return AWSTemporaryCredentials AWS credentials
     */
    public AWSTemporaryCredentials getAWSTemporaryCredentials(String domainName, String roleName) {
        return getAWSTemporaryCredentials(domainName, roleName, null, null, null, false);
    }

    /**
     * For a given domain and role return AWS temporary credentials. If ignoreCache
     * argument is true then the request will be carried against Athenz ZTS Server
     * and ignore any possibly valid credentials from the local cache.
     *
     * @param domainName name of the domain
     * @param roleName is the name of the role
     * @param ignoreCache whether or not ignore the cache for this request
     * @return AWSTemporaryCredentials AWS credentials
     */
    public AWSTemporaryCredentials getAWSTemporaryCredentials(String domainName, String roleName,
            boolean ignoreCache) {
        return getAWSTemporaryCredentials(domainName, roleName, null, null, null, ignoreCache);
    }

    /**
     * For a given domain and role return AWS temporary credentials. If the token
     * is present in the local cache and not expired, it will be returned.
     *
     * @param domainName name of the domain
     * @param roleName is the name of the role
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @param externalId (optional) external id to satisfy configured assume role condition
     * @return AWSTemporaryCredentials AWS credentials
     */
    public AWSTemporaryCredentials getAWSTemporaryCredentials(String domainName, String roleName,
            String externalId, Integer minExpiryTime, Integer maxExpiryTime) {
        return getAWSTemporaryCredentials(domainName, roleName, externalId,
                minExpiryTime, maxExpiryTime, false);
    }

    /**
     * For a given domain and role return AWS temporary credentials. If ignoreCache
     * argument is true then the request will be carried against Athenz ZTS Server
     * and ignore any possibly valid credentials from the local cache.
     *
     * @param domainName name of the domain
     * @param roleName is the name of the role
     * @param minExpiryTime (optional) specifies that the returned RoleToken must be
     *          at least valid (min/lower bound) for specified number of seconds,
     * @param maxExpiryTime (optional) specifies that the returned RoleToken must be
     *          at most valid (max/upper bound) for specified number of seconds.
     * @param externalId (optional) external id to satisfy configured assume role condition
     * @param ignoreCache whether or not ignore the cache for this request
     * @return AWSTemporaryCredentials AWS credentials
     */
    public AWSTemporaryCredentials getAWSTemporaryCredentials(String domainName, String roleName,
            String externalId, Integer minExpiryTime, Integer maxExpiryTime, boolean ignoreCache) {

        // since our aws role name can contain the path element thus /'s
        // we need to encode the value and use that instead
        
        try {
            roleName = URLEncoder.encode(roleName, "UTF-8");
        } catch (UnsupportedEncodingException ex) {
            LOG.error("Unable to encode {} - error {}", roleName, ex.getMessage());
        }
        
        // first lookup in our cache to see if it can be satisfied
        // only if we're not asked to ignore the cache
        
        AWSTemporaryCredentials awsCred;
        String cacheKey = getRoleTokenCacheKey(domainName, roleName, null);
        if (cacheKey != null && !ignoreCache) {
            awsCred = lookupAwsCredInCache(cacheKey, minExpiryTime, maxExpiryTime);
            if (awsCred != null) {
                return awsCred;
            }

            // start prefetch for this token if prefetch is enabled

            if (enablePrefetch && prefetchAutoEnable) {
                if (prefetchAwsCreds(domainName, roleName, externalId, minExpiryTime, maxExpiryTime)) {
                    awsCred = lookupAwsCredInCache(cacheKey, minExpiryTime, maxExpiryTime);
                }
                if (awsCred != null) {
                    return awsCred;
                }
                LOG.error("GetAWSTemporaryCredentials: cache prefetch and lookup error");
            }
        }
        
        // if no hit then we need to request a new token from ZTS
        
        updateServicePrincipal();

        try {
            awsCred = ztsClient.getAWSTemporaryCredentials(domainName, roleName,
                    maxExpiryTime, externalId);
        } catch (ResourceException ex) {

            // if we have an entry in our cache then we'll return that
            // instead of returning failure

            if (cacheKey != null && !ignoreCache) {
                awsCred = lookupAwsCredInCache(cacheKey, null, null);
                if (awsCred != null) {
                    return awsCred;
                }
            }

            throw new ZTSClientException(ex.getCode(), ex.getData());

        } catch (Exception ex) {

            // if we have an entry in our cache then we'll return that
            // instead of returning failure

            if (cacheKey != null && !ignoreCache) {
                awsCred = lookupAwsCredInCache(cacheKey, null, null);
                if (awsCred != null) {
                    return awsCred;
                }
            }

            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
        
        // need to add the token to our cache. If our principal was
        // updated then we need to retrieve a new cache key

        if (awsCred != null) {
            if (cacheKey == null) {
                cacheKey = getRoleTokenCacheKey(domainName, roleName, null);
            }
            if (cacheKey != null) {
                AWS_CREDS_CACHE.put(cacheKey, awsCred);
            }
        }
        return awsCred;
    }
    
    /**
     * Retrieve the list of all policies (not just names) from the ZTS Server that
     * is signed with both ZTS's and ZMS's private keys. It will pass an option matchingTag
     * so that ZTS can skip returning signed policies if no changes have taken
     * place since that tag was issued.
     * @param domainName name of the domain
     * @param matchingTag name of the tag issued with last request
     * @param responseHeaders contains the "tag" returned for modification
     *   time of the policies, map key = "tag", List should contain a single value
     * @return list of policies signed by ZTS Server. ZTSClientException will be thrown in case of failure
     */
    public DomainSignedPolicyData getDomainSignedPolicyData(String domainName, String matchingTag,
            Map> responseHeaders) {
        try {
            return ztsClient.getDomainSignedPolicyData(domainName, matchingTag, responseHeaders);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Verify if the given principal has access to the specified role in the
     * domain or not.
     * @param domainName name of the domain
     * @param roleName name of the role
     * @param principal name of the principal to check for
     * @return Access object with grant true/false response. ZTSClientException will be thrown in case of failure
     */
    public Access getAccess(String domainName, String roleName, String principal) {
        updateServicePrincipal();
        try {
            return ztsClient.getAccess(domainName, roleName, principal);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Requests the ZTS to indicate whether or not the specific request for the
     * specified resource with authentication details will be granted or not.
     * @param action value of the action to be carried out (e.g. "UPDATE", "DELETE")
     * @param resource resource YRN. YRN is defined as {ServiceName})?:({LocationName})?:)?{ResourceName}"
     * @param trustDomain (optional) if the access checks involves cross domain check only
     *        check the specified trusted domain and ignore all others
     * @param principal (optional) carry out the access check for specified principal
     * @return ResourceAccess object indicating whether or not the request will be granted or not
     */
    public ResourceAccess getResourceAccess(String action, String resource, String trustDomain, String principal) {
        updateServicePrincipal();
        try {
            return ztsClient.getResourceAccess(action, resource, trustDomain, principal);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getMessage());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Requests the ZTS to indicate whether or not the specific request for the
     * specified resource with authentication details will be granted or not.
     * @param action value of the action to be carried out (e.g. "UPDATE", "DELETE")
     * @param resource resource YRN. YRN is defined as {ServiceName})?:({LocationName})?:)?{ResourceName}"
     * @param trustDomain (optional) if the access checks involves cross domain check only
     *        check the specified trusted domain and ignore all others
     * @param principal (optional) carry out the access check for specified principal
     * @return ResourceAccess object indicating whether or not the request will be granted or not
     */
    public ResourceAccess getResourceAccessExt(String action, String resource, String trustDomain, String principal) {
        updateServicePrincipal();
        try {
            return ztsClient.getResourceAccessExt(action, resource, trustDomain, principal);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getMessage());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Request by an instance to register itself based on its provider
     * attestation.
     * @param info InstanceRegisterInformation object for the request
     * @param responseHeaders contains the "location" returned for post refresh requests
     *   List should contain a single value
     * @return InstanceIdentity object that includes a x509 certificate for the service
     */
    public InstanceIdentity postInstanceRegisterInformation(InstanceRegisterInformation info,
            Map> responseHeaders) {
        updateServicePrincipal();
        try {
            return ztsClient.postInstanceRegisterInformation(info, responseHeaders);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Request by an instance to refresh its certificate. The instance must
     * authenticate itself using the certificate it has received from the
     * postInstanceRegisterInformation call.
     * @param provider Provider Service name
     * @param domain instance domain name
     * @param service instance service name
     * @param instanceId instance id as provided in the CSR
     * @param info InstanceRegisterInformation object for the request
     * @return InstanceIdentity object that includes a x509 certificate for the service
     */
    public InstanceIdentity postInstanceRefreshInformation(String provider, String domain,
            String service, String instanceId, InstanceRefreshInformation info) {
        updateServicePrincipal();
        try {
            return ztsClient.postInstanceRefreshInformation(provider, domain, service, instanceId, info);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }
    
    /**
     * Revoke an instance from refreshing its certificates.
     * @param provider Provider Service name
     * @param domain instance domain name
     * @param service instance service name
     * @param instanceId instance id as provided in the CSR
     */
    public void deleteInstanceIdentity(String provider, String domain,
            String service, String instanceId) {
        updateServicePrincipal();
        try {
            ztsClient.deleteInstanceIdentity(provider, domain, service, instanceId);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve list of CA Certificates in PEM format for the given bundle name
     * @param bundleName name of the CA Certificate bundle name
     * @return CA Certificate bundle including list of CA certificates on success. ZTSClientException will be thrown in case of failure
     */
    public CertificateAuthorityBundle getCertificateAuthorityBundle(String bundleName) {
        updateServicePrincipal();
        try {
            return ztsClient.getCertificateAuthorityBundle(bundleName);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve list of workloads running on the given ip address
     * @param ipAddress ip address of the workload
     * @return list of workloads on success. ZTSClientException will be thrown in case of failure
     */
    public Workloads getWorkloadsByIP(String ipAddress) {
        updateServicePrincipal();
        try {
            return ztsClient.getWorkloadsByIP(ipAddress);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve list of workloads running with given domain and service
     * @param domain name of the domain
     * @param service name of the service
     * @return list of workloads on success. ZTSClientException will be thrown in case of failure
     */
    public Workloads getWorkloadsByService(String domain, String service) {
        updateServicePrincipal();
        try {
            return ztsClient.getWorkloadsByService(domain, service);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Retrieve list of transport rules defined for given domain and service
     * @param domain name of the domain
     * @param service name of the service
     * @return list of transport rules on success. ZTSClientException will be thrown in case of failure
     */
    public TransportRules getTransportRules(String domain, String service) {
        updateServicePrincipal();
        try {
            return ztsClient.getTransportRules(domain, service);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    /**
     * Request an instance register token for the given service instance from the
     * given provider. Not all providers may support such functionality.
     * @param provider name of the provider
     * @param domain name of the domain
     * @param service name of the service
     * @param instanceId unique id assigned to the instance by the provider
     * @return instance register token. ZTSClientException will be thrown in case of failure
     */
    public InstanceRegisterToken getInstanceRegisterToken(String provider, String domain, String service, String instanceId) {
        try {
            return ztsClient.getInstanceRegisterToken(provider, domain, service, instanceId);
        } catch (ResourceException ex) {
            throw new ZTSClientException(ex.getCode(), ex.getData());
        } catch (Exception ex) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, ex.getMessage());
        }
    }

    static class ClientKeyRefresherListener implements KeyRefresherListener {

        long lastCertRefreshTime = 0;

        @Override
        public void onKeyChangeEvent() {
            lastCertRefreshTime = System.currentTimeMillis() / 1000;
        }

        long getLastCertRefreshTime() {
            return lastCertRefreshTime;
        }
    }

    static class PrefetchTokenScheduledItem {

        TokenType tokenType = TokenType.ACCESS;
        PrefetchTokenScheduledItem setTokenType(TokenType type) {
            tokenType = type;
            return this;
        }

        String providedZTSUrl;
        PrefetchTokenScheduledItem setProvidedZTSUrl(String u) {
            providedZTSUrl = u;
            return this;
        }
        
        ServiceIdentityProvider siaProvider;
        PrefetchTokenScheduledItem setSiaIdentityProvider(ServiceIdentityProvider s) {
            siaProvider = s;
            return this;
        }
        
        ZTSRDLGeneratedClient ztsClient;
        PrefetchTokenScheduledItem setZtsClient(ZTSRDLGeneratedClient z) {
            ztsClient = z;
            return this;
        }
        
        boolean isInvalid = false;
        PrefetchTokenScheduledItem setIsInvalid(boolean invalid) {
            isInvalid = invalid;
            return this;
        }
        
        String identityDomain;
        PrefetchTokenScheduledItem setIdentityDomain(String d) {
            identityDomain = d;
            return this;
        }
        
        String identityName;
        PrefetchTokenScheduledItem setIdentityName(String d) {
            identityName = d;
            return this;
        }
        
        String domainName;
        PrefetchTokenScheduledItem setDomainName(String d) {
            domainName = d;
            return this;
        }

        String cacheKey;
        PrefetchTokenScheduledItem setCacheKey(String key) {
            cacheKey = key;
            return this;
        }

        String roleName;
        PrefetchTokenScheduledItem setRoleName(String s) {
            roleName = s;
            return this;
        }

        List roleNames;
        PrefetchTokenScheduledItem setRoleNames(List stringList) {
            roleNames = stringList;
            return this;
        }

        String proxyForPrincipal;
        PrefetchTokenScheduledItem setProxyForPrincipal(String u) {
            proxyForPrincipal = u;
            return this;
        }

        String externalId;
        PrefetchTokenScheduledItem setExternalId(String id) {
            externalId = id;
            return this;
        }

        String authorizationDetails;
        PrefetchTokenScheduledItem setAuthorizationDetails(String details) {
            authorizationDetails = details;
            return this;
        }

        String idTokenServiceName;
        PrefetchTokenScheduledItem setIdTokenServiceName(String serviceName) {
            idTokenServiceName = serviceName;
            return this;
        }

        Integer minDuration;
        PrefetchTokenScheduledItem setMinDuration(Integer min) {
            minDuration = min;
            return this;
        }
        
        Integer maxDuration;
        PrefetchTokenScheduledItem setMaxDuration(Integer max) {
            maxDuration = max;
            return this;
        }
        
        long expiresAtUTC = 0;
        PrefetchTokenScheduledItem setExpiresAtUTC(long e) {
            expiresAtUTC = e;
            return this;
        }

        long fetchTime = 0;
        PrefetchTokenScheduledItem setFetchTime(long time) {
            fetchTime = time;
            return this;
        }

        ZTSClientNotificationSender notificationSender = null;
        PrefetchTokenScheduledItem setNotificationSender(ZTSClientNotificationSender notificationSender) {
            this.notificationSender = notificationSender;
            return this;
        }

        long lastFailTime = 0;
        PrefetchTokenScheduledItem setLastFailTime(long time) {
            lastFailTime = time;
            return this;
        }

        long lastNotificationTime = 0;
        PrefetchTokenScheduledItem setLastNotificationTime(long time) {
            lastNotificationTime = time;
            return this;
        }

        int tokenMinExpiryTime;
        PrefetchTokenScheduledItem setTokenMinExpiryTime(int t) {
            tokenMinExpiryTime = t;
            return this;
        }

        SSLContext sslContext;
        PrefetchTokenScheduledItem setSslContext(SSLContext ctx) {
            sslContext = ctx;
            return this;
        }
        
        String proxyUrl;
        PrefetchTokenScheduledItem setProxyUrl(String url) {
            proxyUrl = url;
            return this;
        }
        
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((domainName == null) ? 0 : domainName.hashCode());
            result = prime * result + ((identityDomain == null) ? 0 : identityDomain.hashCode());
            result = prime * result + ((identityName == null) ? 0 : identityName.hashCode());
            result = prime * result + ((roleName == null) ? 0 : roleName.hashCode());
            result = prime * result + ((roleNames == null) ? 0 : roleNames.hashCode());
            result = prime * result + ((proxyForPrincipal == null) ? 0 : proxyForPrincipal.hashCode());
            result = prime * result + ((externalId == null) ? 0 : externalId.hashCode());
            result = prime * result + ((idTokenServiceName == null) ? 0 : idTokenServiceName.hashCode());
            result = prime * result + ((sslContext == null) ? 0 : sslContext.hashCode());
            result = prime * result + ((proxyUrl == null) ? 0 : proxyUrl.hashCode());
            result = prime * result + tokenType.hashCode();
            result = prime * result + Boolean.hashCode(isInvalid);

            return result;
        }
        
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            PrefetchTokenScheduledItem other = (PrefetchTokenScheduledItem) obj;
            if (domainName == null) {
                if (other.domainName != null) {
                    return false;
                }
            } else if (!domainName.equals(other.domainName)) {
                return false;
            }
            if (identityDomain == null) {
                if (other.identityDomain != null) {
                    return false;
                }
            } else if (!identityDomain.equals(other.identityDomain)) {
                return false;
            }
            if (identityName == null) {
                if (other.identityName != null) {
                    return false;
                }
            } else if (!identityName.equals(other.identityName)) {
                return false;
            }
            if (roleName == null) {
                if (other.roleName != null) {
                    return false;
                }
            } else if (!roleName.equals(other.roleName)) {
                return false;
            }
            if (roleNames == null) {
                if (other.roleNames != null) {
                    return false;
                }
            } else if (!roleNames.equals(other.roleNames)) {
                return false;
            }
            if (proxyForPrincipal == null) {
                if (other.proxyForPrincipal != null) {
                    return false;
                }
            } else if (!proxyForPrincipal.equals(other.proxyForPrincipal)) {
                return false;
            }
            if (externalId == null) {
                if (other.externalId != null) {
                    return false;
                }
            } else if (!externalId.equals(other.externalId)) {
                return false;
            }
            if (idTokenServiceName == null) {
                if (other.idTokenServiceName != null) {
                    return false;
                }
            } else if (!idTokenServiceName.equals(other.idTokenServiceName)) {
                return false;
            }
            if (isInvalid != other.isInvalid) {
                return false;
            }
            if (tokenType != other.tokenType) {
                return false;
            }
            if (sslContext == null) {
                return other.sslContext == null;
            } else {
                return sslContext.equals(other.sslContext);
            }
        }

        public boolean shouldSendNotification() {
            if (notificationSender == null) {
                return false;
            }

            // If we successfully received a token, revert lastNotificationTime to be ready for future failures
            if (lastFailTime == 0) {
                lastNotificationTime = 0;
                return false;
            }

            return lastNotificationTime == 0;
        }
    }
    
    public class AWSHostNameVerifier implements HostnameVerifier {

        String dnsHostname;
        
        public AWSHostNameVerifier(String hostname) {
            dnsHostname = hostname;
        }
        
        @Override
        public boolean verify(String hostname, SSLSession session) {

            Certificate[] certs = null;
            try {
                certs = session.getPeerCertificates();
            } catch (SSLPeerUnverifiedException ignored) {
            }
            if (certs == null) {
                return false;
            }
            
            for (Certificate cert : certs) {
                try {
                    X509Certificate x509Cert = (X509Certificate) cert;
                    if (matchDnsHostname(x509Cert.getSubjectAlternativeNames())) {
                        return true;
                    }
                } catch (CertificateParsingException ignored) {
                }
            }
            return false;
        }
        
        boolean matchDnsHostname(Collection> altNames) {
            
            if (altNames == null) {
                return false;
            }
            
            // GeneralName ::= CHOICE {
            //     otherName                       [0]     OtherName,
            //     rfc822Name                      [1]     IA5String,
            //     dNSName                         [2]     IA5String,
            //     x400Address                     [3]     ORAddress,
            //     directoryName                   [4]     Name,
            //     ediPartyName                    [5]     EDIPartyName,
            //     uniformResourceIdentifier       [6]     IA5String,
            //     iPAddress                       [7]     OCTET STRING,
            //     registeredID                    [8]     OBJECT IDENTIFIER}
            
            for (@SuppressWarnings("rawtypes") List item : altNames) {
                Integer type = (Integer) item.get(0);
                if (type == 2) {
                    String dns = (String) item.get(1);
                    if (dnsHostname.equalsIgnoreCase(dns)) {
                        return true;
                    }
                }
            }
            
            return false;
        }
    }
    
    private static Set loadSvcProviderTokens() {
        
        ztsTokenProviders = ServiceLoader.load(ZTSClientService.class);
        svcLoaderCacheKeys = new AtomicReference<>();

        // if have service loader implementations, then stuff role tokens into cache
        // and keep track of these tokens so that they will get refreshed from
        // service loader and not zts server
        
        Set cacheKeySet = new HashSet<>();
        for (ZTSClientService provider: ztsTokenProviders) {
            Collection descs = provider.loadTokens();
            if (descs == null) {
                if (LOG.isInfoEnabled()) {
                    LOG.info("loadSvcProviderTokens: provider didn't return tokens: prov={}", provider);
                }
                continue;
            }
            for (ZTSClientService.RoleTokenDescriptor desc: descs) {
                if (desc.signedToken != null) {
                    // stuff token in cache and record service loader key
                    String key = cacheSvcProvRoleToken(desc);
                    if (key != null) {
                        cacheKeySet.add(key);
                    }
                }
            }
        }

        svcLoaderCacheKeys.set(cacheKeySet);
        return cacheKeySet;
    }

    boolean isEmpty(final String value) {
        return (value == null || value.isEmpty());
    }

    boolean isEmpty(final List list) {
        return (list == null || list.isEmpty());
    }

    /**
     * returns a cache key for the given list of roles.
     * if the list of roles contains multiple entries
     * then we have to sort the array first and then
     * generate the key based on the sorted list since
     * there is no guarantee what order the ZTS Server
     * might return the list of roles
     * 
     * @param roles list of role names
     * @return cache key for the list
     */
    static String multipleRoleKey(List roles) {
        
        // first check to make sure we have valid data
        
        if (roles == null || roles.isEmpty()) {
            return null;
        }
        
        // if we have a single role then that's the key
        
        if (roles.size() == 1) {
            return roles.get(0);
        }
        
        // if we have multiple roles, then we have to
        // sort the values and then generate the key
        // since we might have been given unmodifiable
        // list we need to make a copy so we can sort it

        List modList = new ArrayList<>(roles);
        Collections.sort(modList);
        return String.join(",", modList);
    }
    
    /**
     * stuff pre-loaded service token in cache. in this model an external
     * service (proxy user) has retrieved the role tokens and added to the
     * client cache so it can run without the need to contact zts server.
     * in this model we're going to look at the principal field only and
     * ignore the proxy field since the client doesn't need to know anything
     * about that detail.
     *
     * start prefetch task to reload to prevent expiry
     * return the cache key used
     */
    static String cacheSvcProvRoleToken(ZTSClientService.RoleTokenDescriptor desc) {

        if (cacheDisabled) {
            return null;
        }
        
        com.yahoo.athenz.auth.token.RoleToken rt = new com.yahoo.athenz.auth.token.RoleToken(desc.getSignedToken());
        String domainName = rt.getDomain();
        String principalName = rt.getPrincipal();
        boolean completeRoleSet = rt.getDomainCompleteRoleSet();

        // if the role token was for a complete set then we're not going
        // to use the rolename field (it indicates that the original request
        // was completed without the rolename field being specified)
        
        final String roleName = (completeRoleSet) ? null : multipleRoleKey(rt.getRoles());
        
        // parse principalName for the tenant domain and service name
        // we must have valid components otherwise we'll just
        // ignore the token - you can't have a principal without
        // valid domain and service names
        
        int index = principalName.lastIndexOf('.'); // ex: cities.burbank.mysvc
        if (index == -1) {
            LOG.error("cacheSvcProvRoleToken: Invalid principal in token: {}", rt.getSignedToken());
            return null;
        }

        final String tenantDomain = principalName.substring(0, index);
        final String tenantService = principalName.substring(index + 1);
        Long expiryTime = rt.getExpiryTime();

        RoleToken roleToken = new RoleToken().setToken(desc.getSignedToken()).setExpiryTime(expiryTime);

        final String key = getRoleTokenCacheKey(tenantDomain, tenantService, domainName, roleName, null);

        if (LOG.isInfoEnabled()) {
            LOG.info("cacheSvcProvRoleToken: cache-add key: {} expiry: {}", key, expiryTime);
        }

        ROLE_TOKEN_CACHE.put(key, roleToken);

        // setup prefetch task
        
        prefetchSvcProvTokens(tenantDomain, tenantService, domainName,
            key, roleName, null, null, expiryTime, null);

        return key;
    }
    
    static void prefetchSvcProvTokens(String domain, String service, String domainName,
            String cacheKey, String roleName, Integer minExpiryTime, Integer maxExpiryTime,
            Long expiryTimeUTC, String proxyForPrincipal) {
        
        if (domainName == null || domainName.trim().isEmpty()) {
            throw new ZTSClientException(ResourceException.BAD_REQUEST, "Domain Name cannot be empty");
        }

        PrefetchTokenScheduledItem item = new PrefetchTokenScheduledItem()
            .setTokenType(TokenType.SVC_ROLE)
            .setCacheKey(cacheKey)
            .setDomainName(domainName)
            .setRoleName(roleName)
            .setProxyForPrincipal(proxyForPrincipal)
            .setMinDuration(minExpiryTime)
            .setMaxDuration(maxExpiryTime)
            .setExpiresAtUTC(expiryTimeUTC)
            .setIdentityDomain(domain)
            .setIdentityName(service)
            .setTokenMinExpiryTime(ZTSClient.tokenMinExpiryTime);

        // we need to make sure we don't have duplicates in
        // our prefetch list so since we got a brand new
        // token now we're going to remove any others we have
        // in the list and add this one. Our items equals
        // method defines what attributes we're looking for
        // when comparing two items

        PREFETCH_SCHEDULED_ITEMS.remove(item);
        PREFETCH_SCHEDULED_ITEMS.add(item);

        startPrefetch();
    }

    static void startPrefetch() {

        if (FETCH_TIMER != null) {
            return;
        }

        synchronized (TIMER_LOCK) {
            if (FETCH_TIMER == null) {
                FETCH_TIMER = new Timer(true);
                // check the fetch items every prefetchInterval seconds.
                FETCH_TIMER.schedule(new TokenPrefetchTask(), 0, prefetchInterval * 1000);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy