NIFIREG-411 Refactor nifi-registry-client to support other authN mechanisms

NIFIREG-411 Update CertificateUtils with changes from NiFi and add protocol to NiFiRegistryClientConfig

This closes #293.

Signed-off-by: Kevin Doran <kdoran@apache.org>
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/AccessClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/AccessClient.java
new file mode 100644
index 0000000..23cbcbc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/AccessClient.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client;
+
+import java.io.IOException;
+
+/**
+ * Client for interacting with the AccessResource.
+ */
+public interface AccessClient {
+
+    /**
+     * Get an access token by authenticating with a username and password aginst the configured identity provider.
+     *
+     * @param username the username
+     * @param password the password
+     * @return the access token
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NiFiRegistryException if an non I/O error occurs
+     */
+    String getToken(String username, String password) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets an access token via spnego. It is expected that the caller of this method has wrapped the call
+     * in a {@code doAs()} using a {@link javax.security.auth.Subject}.
+     *
+     * @return the token
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NiFiRegistryException if an non I/O error occurs
+     */
+    String getTokenFromKerberosTicket() throws NiFiRegistryException, IOException;
+
+    /**
+     * Performs a logout for the user represented by the given token.
+     *
+     * @param token the toke to authenticate with
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NiFiRegistryException if an non I/O error occurs
+     */
+    void logout(String token) throws NiFiRegistryException, IOException;
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
index a7497a1..2d2d8c8 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
@@ -29,10 +29,17 @@
     BucketClient getBucketClient();
 
     /**
+     * @deprecated use getBucketClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with buckets on behalf of the given proxied entities
      */
     BucketClient getBucketClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with buckets using the given request config
+     */
+    BucketClient getBucketClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -41,10 +48,17 @@
     FlowClient getFlowClient();
 
     /**
+     * @deprecated use getFlowClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with flows on behalf of the given proxied entities
      */
     FlowClient getFlowClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with flows using the given request config
+     */
+    FlowClient getFlowClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -53,10 +67,17 @@
     FlowSnapshotClient getFlowSnapshotClient();
 
     /**
+     * @deprecated use getFlowSnapshotClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with flows/snapshots on behalf of the given proxied entities
      */
     FlowSnapshotClient getFlowSnapshotClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with flows/snapshots using the given request config
+     */
+    FlowSnapshotClient getFlowSnapshotClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -65,10 +86,17 @@
     ItemsClient getItemsClient();
 
     /**
+     * @deprecated use getItemsClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with bucket items on behalf of the given proxied entities
      */
     ItemsClient getItemsClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with bucket items using the given request config
+     */
+    ItemsClient getItemsClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -77,10 +105,17 @@
     UserClient getUserClient();
 
     /**
+     * @deprecated use getUserClient(RequestConfig requestConfig)
+     *
      * @return the client for obtaining information about the current user based on the given proxied entities
      */
     UserClient getUserClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for obtaining information about the current user based on the request config
+     */
+    UserClient getUserClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -89,10 +124,17 @@
     BundleClient getBundleClient();
 
     /**
+     * @deprecated use getBundleClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with extension bundles on behalf of the given proxied entities
      */
     BundleClient getBundleClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with extension bundles using the given request config
+     */
+    BundleClient getBundleClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -101,10 +143,17 @@
     BundleVersionClient getBundleVersionClient();
 
     /**
+     * @deprecated use getBundleVersionClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with extension bundle versions on behalf of the given proxied entities
      */
     BundleVersionClient getBundleVersionClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with extension bundle versions using the given request config
+     */
+    BundleVersionClient getBundleVersionClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -113,10 +162,17 @@
     ExtensionRepoClient getExtensionRepoClient();
 
     /**
+     * @deprecated use getExtensionRepoClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with the extension repository on behalf of the given proxied entities
      */
     ExtensionRepoClient getExtensionRepoClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with the extension repository using the given request config
+     */
+    ExtensionRepoClient getExtensionRepoClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -125,10 +181,17 @@
     ExtensionClient getExtensionClient();
 
     /**
+     * @deprecated use getExtensionClient(RequestConfig requestConfig)
+     *
      * @return the client for interacting with extensions on behalf of the given proxied entities
      */
     ExtensionClient getExtensionClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with extensions using the given request config
+     */
+    ExtensionClient getExtensionClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -141,12 +204,19 @@
     /**
      * Returns client for interacting with tenants.
      *
+     * @deprecated use getTenantsClient(RequestConfig requestConfig)
+     *
      * @param proxiedEntity The given proxied entities.
      *
      * @return the client for interacting with tenants on behalf of the given proxied entities.
      */
     TenantsClient getTenantsClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with tenants using the given request config
+     */
+    TenantsClient getTenantsClient(RequestConfig requestConfig);
+
     //-------------------------------------------------------------------------------------------
 
     /**
@@ -159,12 +229,26 @@
     /**
      * Returns client for interacting with access policies.
      *
+     * @deprecated use getPoliciesClient(RequestConfig requestConfig)
+     *
      * @param proxiedEntity The given proxied entities.
      *
      * @return the client for interacting with access policies on behalf of the given proxied entities.
      */
     PoliciesClient getPoliciesClient(String ... proxiedEntity);
 
+    /**
+     * @return the client for interacting with access policies using the given request config
+     */
+    PoliciesClient getPoliciesClient(RequestConfig requestConfig);
+
+    //-------------------------------------------------------------------------------------------
+
+    /**
+     * @return the client for obtaining access tokens
+     */
+    AccessClient getAccessClient();
+
     //-------------------------------------------------------------------------------------------
 
     /**
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
index de77b51..0c1b21b 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.client;
 
+import org.apache.nifi.registry.security.util.CertificateUtils;
 import org.apache.nifi.registry.security.util.KeyStoreUtils;
 import org.apache.nifi.registry.security.util.KeystoreType;
 
@@ -36,6 +37,8 @@
  */
 public class NiFiRegistryClientConfig {
 
+    public static final String DEFAULT_PROTOCOL = CertificateUtils.getHighestCurrentSupportedTlsProtocolVersion();
+
     private final String baseUrl;
     private final SSLContext sslContext;
     private final String keystoreFilename;
@@ -45,6 +48,7 @@
     private final String truststoreFilename;
     private final String truststorePass;
     private final KeystoreType truststoreType;
+    private final String protocol;
     private final HostnameVerifier hostnameVerifier;
     private final Integer readTimeout;
     private final Integer connectTimeout;
@@ -60,6 +64,7 @@
         this.truststoreFilename = builder.truststoreFilename;
         this.truststorePass = builder.truststorePass;
         this.truststoreType = builder.truststoreType;
+        this.protocol = builder.protocol == null ? DEFAULT_PROTOCOL : builder.protocol;
         this.hostnameVerifier = builder.hostnameVerifier;
         this.readTimeout = builder.readTimeout;
         this.connectTimeout = builder.connectTimeout;
@@ -118,7 +123,7 @@
                 // initialize the ssl context
                 KeyManager[] keyManagers = keyManagerFactory != null ? keyManagerFactory.getKeyManagers() : null;
                 TrustManager[] trustManagers = trustManagerFactory != null ? trustManagerFactory.getTrustManagers() : null;
-                final SSLContext sslContext = SSLContext.getInstance("TLS");
+                final SSLContext sslContext = SSLContext.getInstance(getProtocol());
                 sslContext.init(keyManagers, trustManagers, new SecureRandom());
                 sslContext.getDefaultSSLParameters().setNeedClientAuth(true);
 
@@ -159,6 +164,10 @@
         return truststoreType;
     }
 
+    public String getProtocol() {
+        return protocol;
+    }
+
     public HostnameVerifier getHostnameVerifier() {
         return hostnameVerifier;
     }
@@ -185,6 +194,7 @@
         private String truststoreFilename;
         private String truststorePass;
         private KeystoreType truststoreType;
+        private String protocol;
         private HostnameVerifier hostnameVerifier;
         private Integer readTimeout;
         private Integer connectTimeout;
@@ -234,6 +244,11 @@
             return this;
         }
 
+        public Builder protocol(final String protocol) {
+            this.protocol = protocol;
+            return this;
+        }
+
         public Builder hostnameVerifier(final HostnameVerifier hostnameVerifier) {
             this.hostnameVerifier = hostnameVerifier;
             return this;
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/RequestConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/RequestConfig.java
new file mode 100644
index 0000000..fcb83e9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/RequestConfig.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client;
+
+import java.util.Map;
+
+/**
+ * Configuration applied to each client request.
+ */
+public interface RequestConfig {
+
+    /**
+     * @return the headers to apply to each request
+     */
+    Map<String,String> getHeaders();
+
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java
index a6f9ac0..41005c1 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java
@@ -18,20 +18,20 @@
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.MediaType;
 import java.io.IOException;
-import java.util.Map;
 
 public class AbstractCRUDJerseyClient extends AbstractJerseyClient {
 
     protected final WebTarget baseTarget;
 
-    public AbstractCRUDJerseyClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(headers);
+    public AbstractCRUDJerseyClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.baseTarget = baseTarget;
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
index dd9792d..ad5ea41 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
@@ -18,6 +18,7 @@
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.ws.rs.WebApplicationException;
@@ -26,7 +27,6 @@
 import javax.ws.rs.core.Response;
 import java.io.IOException;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -36,16 +36,17 @@
  */
 public class AbstractJerseyClient {
 
-    private final Map<String,String> headers;
+    private static final RequestConfig EMPTY_REQUEST_CONFIG = () ->  Collections.emptyMap();
 
-    public AbstractJerseyClient(final Map<String, String> headers) {
-        this.headers = headers == null ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(headers));
+    private final RequestConfig requestConfig;
+
+    public AbstractJerseyClient(final RequestConfig requestConfig) {
+        this.requestConfig = (requestConfig == null ? EMPTY_REQUEST_CONFIG : requestConfig);
     }
 
-    protected Map<String,String> getHeaders() {
-        return headers;
+    protected RequestConfig getRequestConfig() {
+        return this.requestConfig;
     }
-
     /**
      * Adds query parameters for the given RevisionInfo if populated.
      *
@@ -80,7 +81,10 @@
      */
     protected Invocation.Builder getRequestBuilder(final WebTarget webTarget) {
         final Invocation.Builder requestBuilder = webTarget.request();
+
+        final Map<String,String> headers = requestConfig.getHeaders();
         headers.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue()));
+
         return requestBuilder;
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyAccessClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyAccessClient.java
new file mode 100644
index 0000000..4913f15
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyAccessClient.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.client.AccessClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.client.impl.request.BasicAuthRequestConfig;
+import org.apache.nifi.registry.client.impl.request.BearerTokenRequestConfig;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Jersey implementation of AccessClient.
+ */
+public class JerseyAccessClient extends AbstractJerseyClient implements AccessClient {
+
+    private final WebTarget accessTarget;
+
+    public JerseyAccessClient(final WebTarget baseTarget) {
+        super(null);
+        this.accessTarget = baseTarget.path("/access");
+    }
+
+    @Override
+    public String getToken(final String username, final String password) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(username)) {
+            throw new IllegalArgumentException("Username is required");
+        }
+
+        if (StringUtils.isBlank(password)) {
+            throw new IllegalArgumentException("Password is required");
+        }
+
+        return executeAction("Error performing login", () -> {
+            final WebTarget target = accessTarget.path("token/login");
+            final Invocation.Builder requestBuilder = getRequestBuilder(target);
+
+            final RequestConfig basicCredsConfig = new BasicAuthRequestConfig(username, password);
+            final Map<String,String> basicAuthHeaders = basicCredsConfig.getHeaders();
+            basicAuthHeaders.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue()));
+
+            return requestBuilder.post(Entity.json(null), String.class);
+        });
+    }
+
+    @Override
+    public String getTokenFromKerberosTicket() throws NiFiRegistryException, IOException {
+        return executeAction("Error performing kerberos login", () -> {
+            final WebTarget target = accessTarget.path("token/kerberos");
+            return getRequestBuilder(target).post(Entity.json(null), String.class);
+        });
+    }
+
+    @Override
+    public void logout(final String token) throws IOException, NiFiRegistryException {
+        if (StringUtils.isBlank(token)) {
+            throw new IllegalArgumentException("Token is required");
+        }
+
+        executeAction("Error performing logout", () -> {
+            final WebTarget target = accessTarget.path("logout");
+            final Invocation.Builder requestBuilder = getRequestBuilder(target);
+
+            final RequestConfig tokenConfig = new BearerTokenRequestConfig(token);
+            final Map<String,String> bearerHeaders = tokenConfig.getHeaders();
+            bearerHeaders.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue()));
+
+            requestBuilder.delete();
+            return null;
+        });
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
index 8c02e3a..6d35998 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
@@ -20,6 +20,7 @@
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.client.BucketClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
@@ -30,7 +31,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Jersey implementation of BucketClient.
@@ -41,11 +41,11 @@
 
 
     public JerseyBucketClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyBucketClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
+    public JerseyBucketClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.bucketsTarget = baseTarget.path("/buckets");
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java
index 5425531..a3ce5aa 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java
@@ -19,6 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.BundleClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.extension.bundle.Bundle;
 import org.apache.nifi.registry.extension.bundle.BundleFilterParams;
 
@@ -27,7 +28,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Jersey implementation of BundleClient.
@@ -38,11 +38,11 @@
     private final WebTarget extensionBundlesTarget;
 
     public JerseyBundleClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyBundleClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(headers);
+    public JerseyBundleClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.bucketExtensionBundlesTarget = baseTarget.path("buckets/{bucketId}/bundles");
         this.extensionBundlesTarget = baseTarget.path("bundles");
     }
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java
index e9867ba..27775d0 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java
@@ -19,12 +19,13 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.BundleVersionClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.extension.bundle.BundleType;
 import org.apache.nifi.registry.extension.bundle.BundleVersion;
 import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
 import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
-import org.apache.nifi.registry.extension.component.manifest.Extension;
 import org.apache.nifi.registry.extension.component.ExtensionMetadata;
+import org.apache.nifi.registry.extension.component.manifest.Extension;
 import org.glassfish.jersey.media.multipart.FormDataMultiPart;
 import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
 import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
@@ -39,7 +40,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Jersey implementation of BundleVersionClient.
@@ -50,11 +50,11 @@
     private final WebTarget extensionBundlesTarget;
 
     public JerseyBundleVersionClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyBundleVersionClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(headers);
+    public JerseyBundleVersionClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.bucketExtensionBundlesTarget = baseTarget.path("buckets/{bucketId}/bundles");
         this.extensionBundlesTarget = baseTarget.path("bundles");
     }
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java
index eb8082b..bbd440e 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java
@@ -19,6 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.ExtensionClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.extension.bundle.BundleType;
 import org.apache.nifi.registry.extension.component.ExtensionFilterParams;
 import org.apache.nifi.registry.extension.component.ExtensionMetadataContainer;
@@ -31,7 +32,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 public class JerseyExtensionClient extends AbstractJerseyClient implements ExtensionClient {
@@ -39,11 +39,11 @@
     private final WebTarget extensionsTarget;
 
     public JerseyExtensionClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyExtensionClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(headers);
+    public JerseyExtensionClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.extensionsTarget = baseTarget.path("extensions");
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
index f4ad5d5..3a0daf5 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
@@ -19,6 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.ExtensionRepoClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.extension.component.manifest.Extension;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
@@ -37,7 +38,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 
 public class JerseyExtensionRepoClient extends AbstractJerseyClient implements ExtensionRepoClient {
@@ -45,11 +45,11 @@
     private WebTarget extensionRepoTarget;
 
     public JerseyExtensionRepoClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyExtensionRepoClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(headers);
+    public JerseyExtensionRepoClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.extensionRepoTarget = baseTarget.path("extension-repository");
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
index d17e27a..4a61a30 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
@@ -19,6 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.FlowClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
@@ -31,7 +32,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Jersey implementation of FlowClient.
@@ -42,11 +42,11 @@
     private final WebTarget bucketFlowsTarget;
 
     public JerseyFlowClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyFlowClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
+    public JerseyFlowClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.flowsTarget = baseTarget.path("/flows");
         this.bucketFlowsTarget = baseTarget.path("/buckets/{bucketId}/flows");
     }
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
index befe389..19890ca 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
@@ -19,6 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.FlowSnapshotClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 
@@ -29,7 +30,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Jersey implementation of FlowSnapshotClient.
@@ -40,11 +40,11 @@
     final WebTarget flowsFlowSnapshotTarget;
 
     public JerseyFlowSnapshotClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyFlowSnapshotClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
+    public JerseyFlowSnapshotClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.bucketFlowSnapshotTarget = baseTarget.path("/buckets/{bucketId}/flows/{flowId}/versions");
         this.flowsFlowSnapshotTarget = baseTarget.path("/flows/{flowId}/versions");
     }
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
index 6b01fc4..85c965f 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
@@ -20,6 +20,7 @@
 import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.client.ItemsClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.field.Fields;
 
 import javax.ws.rs.client.WebTarget;
@@ -27,7 +28,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Jersey implementation of ItemsClient.
@@ -37,11 +37,11 @@
     private final WebTarget itemsTarget;
 
     public JerseyItemsClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyItemsClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
+    public JerseyItemsClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.itemsTarget = baseTarget.path("/items");
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
index f876277..3e64bad 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
@@ -23,6 +23,7 @@
 import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.client.AccessClient;
 import org.apache.nifi.registry.client.BucketClient;
 import org.apache.nifi.registry.client.BundleClient;
 import org.apache.nifi.registry.client.BundleVersionClient;
@@ -34,9 +35,10 @@
 import org.apache.nifi.registry.client.NiFiRegistryClient;
 import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
 import org.apache.nifi.registry.client.PoliciesClient;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.client.TenantsClient;
 import org.apache.nifi.registry.client.UserClient;
-import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig;
 import org.glassfish.jersey.client.ClientConfig;
 import org.glassfish.jersey.client.ClientProperties;
 import org.glassfish.jersey.client.RequestEntityProcessing;
@@ -50,11 +52,6 @@
 import javax.ws.rs.client.WebTarget;
 import java.io.IOException;
 import java.net.URI;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * A NiFiRegistryClient that uses Jersey Client.
@@ -136,42 +133,62 @@
     }
 
     @Override
+    public BucketClient getBucketClient(String... proxiedEntity) {
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyBucketClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public BucketClient getBucketClient(RequestConfig requestConfig) {
+        return new JerseyBucketClient(baseTarget, requestConfig);
+    }
+
+    @Override
     public FlowClient getFlowClient() {
         return this.flowClient;
     }
 
     @Override
+    public FlowClient getFlowClient(String... proxiedEntity) {
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyFlowClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public FlowClient getFlowClient(RequestConfig requestConfig) {
+        return new JerseyFlowClient(baseTarget, requestConfig);
+    }
+
+    @Override
     public FlowSnapshotClient getFlowSnapshotClient() {
         return this.flowSnapshotClient;
     }
 
     @Override
+    public FlowSnapshotClient getFlowSnapshotClient(String... proxiedEntity) {
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyFlowSnapshotClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public FlowSnapshotClient getFlowSnapshotClient(RequestConfig requestConfig) {
+        return new JerseyFlowSnapshotClient(baseTarget, requestConfig);
+    }
+
+    @Override
     public ItemsClient getItemsClient() {
         return this.itemsClient;
     }
 
     @Override
-    public BucketClient getBucketClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyBucketClient(baseTarget, headers);
-    }
-
-    @Override
-    public FlowClient getFlowClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyFlowClient(baseTarget, headers);
-    }
-
-    @Override
-    public FlowSnapshotClient getFlowSnapshotClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyFlowSnapshotClient(baseTarget, headers);
-    }
-
-    @Override
     public ItemsClient getItemsClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyItemsClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyItemsClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public ItemsClient getItemsClient(RequestConfig requestConfig) {
+        return new JerseyItemsClient(baseTarget, requestConfig);
     }
 
     @Override
@@ -181,8 +198,13 @@
 
     @Override
     public UserClient getUserClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyUserClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyUserClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public UserClient getUserClient(RequestConfig requestConfig) {
+        return new JerseyUserClient(baseTarget, requestConfig);
     }
 
     @Override
@@ -192,8 +214,13 @@
 
     @Override
     public BundleClient getBundleClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyBundleClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyBundleClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public BundleClient getBundleClient(RequestConfig requestConfig) {
+        return new JerseyBundleClient(baseTarget, requestConfig);
     }
 
     @Override
@@ -203,8 +230,13 @@
 
     @Override
     public BundleVersionClient getBundleVersionClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyBundleVersionClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyBundleVersionClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public BundleVersionClient getBundleVersionClient(RequestConfig requestConfig) {
+        return new JerseyBundleVersionClient(baseTarget, requestConfig);
     }
 
     @Override
@@ -213,20 +245,30 @@
     }
 
     @Override
+    public ExtensionRepoClient getExtensionRepoClient(String... proxiedEntity) {
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyExtensionRepoClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public ExtensionRepoClient getExtensionRepoClient(RequestConfig requestConfig) {
+        return new JerseyExtensionRepoClient(baseTarget, requestConfig);
+    }
+
+    @Override
     public ExtensionClient getExtensionClient() {
         return new JerseyExtensionClient(baseTarget);
     }
 
     @Override
     public ExtensionClient getExtensionClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyExtensionClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyExtensionClient(baseTarget, requestConfig);
     }
 
     @Override
-    public ExtensionRepoClient getExtensionRepoClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyExtensionRepoClient(baseTarget, headers);
+    public ExtensionClient getExtensionClient(RequestConfig requestConfig) {
+        return new JerseyExtensionClient(baseTarget, requestConfig);
     }
 
     @Override
@@ -236,8 +278,13 @@
 
     @Override
     public TenantsClient getTenantsClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyTenantsClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyTenantsClient(baseTarget, requestConfig);
+    }
+
+    @Override
+    public TenantsClient getTenantsClient(RequestConfig requestConfig) {
+        return new JerseyTenantsClient(baseTarget, requestConfig);
     }
 
     @Override
@@ -247,27 +294,18 @@
 
     @Override
     public PoliciesClient getPoliciesClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyPoliciesClient(baseTarget, headers);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        return new JerseyPoliciesClient(baseTarget, requestConfig);
     }
 
-    private Map<String,String> getHeaders(String[] proxiedEntities) {
-        final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
-
-        final Map<String,String> headers = new HashMap<>();
-        if (proxiedEntitiesValue != null) {
-            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
-        }
-        return headers;
+    @Override
+    public PoliciesClient getPoliciesClient(RequestConfig requestConfig) {
+        return new JerseyPoliciesClient(baseTarget, requestConfig);
     }
 
-    private String getProxiedEntitesValue(String[] proxiedEntities) {
-        if (proxiedEntities == null) {
-            return null;
-        }
-
-        final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities).map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
-        return StringUtils.join(proxiedEntityChain, "");
+    @Override
+    public AccessClient getAccessClient() {
+        return new JerseyAccessClient(baseTarget);
     }
 
     @Override
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java
index 15577f6..ae9e8dd 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java
@@ -20,11 +20,10 @@
 import org.apache.nifi.registry.authorization.AccessPolicy;
 import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.client.PoliciesClient;
+import org.apache.nifi.registry.client.RequestConfig;
 
 import javax.ws.rs.client.WebTarget;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
 
 public class JerseyPoliciesClient extends AbstractCRUDJerseyClient implements PoliciesClient {
 
@@ -32,11 +31,11 @@
     public static final String POLICIES_PATH = "policies";
 
     public JerseyPoliciesClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyPoliciesClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(baseTarget, headers);
+    public JerseyPoliciesClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(baseTarget, requestConfig);
     }
 
     @Override
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java
index 78867c7..3f95357 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java
@@ -19,15 +19,14 @@
 import org.apache.nifi.registry.authorization.User;
 import org.apache.nifi.registry.authorization.UserGroup;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.client.TenantsClient;
 import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.ws.rs.client.WebTarget;
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 public class JerseyTenantsClient extends AbstractCRUDJerseyClient implements TenantsClient {
     public static final String USER = "User";
@@ -37,11 +36,11 @@
     public static final String USER_GROUPS_PATH = "user-groups";
 
     public JerseyTenantsClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyTenantsClient(final WebTarget baseTarget, final Map<String, String> headers) {
-        super(baseTarget.path("/tenants"), headers);
+    public JerseyTenantsClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(baseTarget.path("/tenants"), requestConfig);
     }
 
     @Override
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
index 7625f35..484c5cb 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
@@ -16,25 +16,24 @@
  */
 package org.apache.nifi.registry.client.impl;
 
-import org.apache.nifi.registry.client.NiFiRegistryException;
-import org.apache.nifi.registry.client.UserClient;
 import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.client.UserClient;
 
 import javax.ws.rs.client.WebTarget;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
 
 public class JerseyUserClient extends AbstractJerseyClient implements UserClient {
 
     private final WebTarget accessTarget;
 
     public JerseyUserClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
+        this(baseTarget, null);
     }
 
-    public JerseyUserClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
+    public JerseyUserClient(final WebTarget baseTarget, final RequestConfig requestConfig) {
+        super(requestConfig);
         this.accessTarget = baseTarget.path("/access");
     }
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BasicAuthRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BasicAuthRequestConfig.java
new file mode 100644
index 0000000..4105b4f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BasicAuthRequestConfig.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl.request;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.client.RequestConfig;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of RequestConfig for a request with basic auth.
+ */
+public class BasicAuthRequestConfig implements RequestConfig {
+
+    public static final String AUTHORIZATION_HEADER = "Authorization";
+    public static final String BASIC = "Basic";
+
+    private final String username;
+    private final String password;
+
+    public BasicAuthRequestConfig(final String username, final String password) {
+        this.username = Validate.notBlank(username);
+        this.password = Validate.notBlank(password);
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        final String basicCreds = username + ":" + password;
+        final byte[] basicCredsBytes = basicCreds.getBytes(StandardCharsets.UTF_8);
+
+        final Base64.Encoder encoder = Base64.getEncoder();
+        final String encodedBasicCreds = encoder.encodeToString(basicCredsBytes);
+
+        final Map<String,String> headers = new HashMap<>();
+        headers.put(AUTHORIZATION_HEADER, BASIC + " " + encodedBasicCreds);
+        return headers;
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BearerTokenRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BearerTokenRequestConfig.java
new file mode 100644
index 0000000..9daf77b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BearerTokenRequestConfig.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl.request;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.client.RequestConfig;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of RequestConfig for a request with a bearer token.
+ */
+public class BearerTokenRequestConfig implements RequestConfig {
+
+    public static final String AUTHORIZATION_HEADER = "Authorization";
+    public static final String BEARER = "Bearer";
+
+    private final String token;
+
+    public BearerTokenRequestConfig(final String token) {
+        this.token = Validate.notBlank(token);
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        final Map<String,String> headers = new HashMap<>();
+        headers.put(AUTHORIZATION_HEADER, BEARER + " " + token);
+        return headers;
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
new file mode 100644
index 0000000..3a89898
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl.request;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of RequestConfig that produces headers for a request with proxied-entities.
+ */
+public class ProxiedEntityRequestConfig implements RequestConfig {
+
+    private final String[] proxiedEntities;
+
+    public ProxiedEntityRequestConfig(final String... proxiedEntities) {
+        this.proxiedEntities = Validate.notNull(proxiedEntities);
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
+
+        final Map<String,String> headers = new HashMap<>();
+        if (proxiedEntitiesValue != null) {
+            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
+        }
+        return headers;
+    }
+
+    private String getProxiedEntitesValue(final String[] proxiedEntities) {
+        if (proxiedEntities == null) {
+            return null;
+        }
+
+        final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities)
+                .map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
+        return StringUtils.join(proxiedEntityChain, "");
+    }
+
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBasicAuthRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBasicAuthRequestConfig.java
new file mode 100644
index 0000000..91c22cf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBasicAuthRequestConfig.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl.request;
+
+import org.apache.nifi.registry.client.RequestConfig;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertEquals;
+
+public class TestBasicAuthRequestConfig {
+
+    @Test
+    public void testBasicAuthRequestConfig() {
+        final String username = "user1";
+        final String password = "password";
+        final String basicCreds = username + ":" + password;
+
+        final String expectedHeaderValue = "Basic " + Base64.getEncoder().encodeToString(basicCreds.getBytes(StandardCharsets.UTF_8));
+
+        final RequestConfig requestConfig = new BasicAuthRequestConfig(username, password);
+
+        final Map<String,String> headers = requestConfig.getHeaders();
+        assertNotNull(headers);
+        assertEquals(1, headers.size());
+
+        final String authorizationHeaderValue = headers.get("Authorization");
+        assertEquals(expectedHeaderValue, authorizationHeaderValue);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBearerTokenRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBearerTokenRequestConfig.java
new file mode 100644
index 0000000..eeaacd3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBearerTokenRequestConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl.request;
+
+import org.apache.nifi.registry.client.RequestConfig;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class TestBearerTokenRequestConfig {
+
+    @Test
+    public void testBearerTokenRequestConfig() {
+        final String token = "some-token";
+        final String expectedHeaderValue = "Bearer " + token;
+
+        final RequestConfig requestConfig = new BearerTokenRequestConfig(token);
+
+        final Map<String,String> headers = requestConfig.getHeaders();
+        assertNotNull(headers);
+        assertEquals(1, headers.size());
+
+        final String authorizationHeaderValue = headers.get("Authorization");
+        assertEquals(expectedHeaderValue, authorizationHeaderValue);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestProxiedEntityRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestProxiedEntityRequestConfig.java
new file mode 100644
index 0000000..e5427ac
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestProxiedEntityRequestConfig.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.client.impl.request;
+
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class TestProxiedEntityRequestConfig {
+
+    @Test
+    public void testSingleProxiedEntity() {
+        final String proxiedEntity = "user1";
+        final String expectedProxiedEntitiesChain = "<user1>";
+
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+
+        final Map<String,String> headers = requestConfig.getHeaders();
+        assertNotNull(headers);
+        assertEquals(1, headers.size());
+
+        final String proxiedEntitiesChainHeaderValue = headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+        assertEquals(expectedProxiedEntitiesChain, proxiedEntitiesChainHeaderValue);
+    }
+
+    @Test
+    public void testMultipleProxiedEntity() {
+        final String proxiedEntity1 = "user1";
+        final String proxiedEntity2 = "user2";
+        final String proxiedEntity3 = "user3";
+        final String expectedProxiedEntitiesChain = "<user1><user2><user3>";
+
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(
+                proxiedEntity1, proxiedEntity2, proxiedEntity3);
+
+        final Map<String,String> headers = requestConfig.getHeaders();
+        assertNotNull(headers);
+        assertEquals(1, headers.size());
+
+        final String proxiedEntitiesChainHeaderValue = headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+        assertEquals(expectedProxiedEntitiesChain, proxiedEntitiesChainHeaderValue);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java
index 5fd6d07..30e77db 100644
--- a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java
@@ -17,8 +17,6 @@
 package org.apache.nifi.registry.security.util;
 
 import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.security.util.KeyStoreUtils;
-import org.apache.nifi.registry.security.util.KeystoreType;
 import org.bouncycastle.asn1.ASN1Encodable;
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
 import org.bouncycastle.asn1.ASN1Set;
@@ -41,6 +39,7 @@
 import org.bouncycastle.cert.X509v3CertificateBuilder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
 import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.crypto.tls.TlsException;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.operator.ContentSigner;
 import org.bouncycastle.operator.OperatorCreationException;
@@ -52,16 +51,13 @@
 import javax.naming.InvalidNameException;
 import javax.naming.ldap.LdapName;
 import javax.naming.ldap.Rdn;
+import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSocket;
-import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.math.BigInteger;
 import java.net.Socket;
-import java.net.URL;
 import java.security.KeyPair;
-import java.security.KeyStore;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.security.Security;
@@ -80,12 +76,19 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public final class CertificateUtils {
     private static final Logger logger = LoggerFactory.getLogger(CertificateUtils.class);
     private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not authenticated";
     private static final Map<ASN1ObjectIdentifier, Integer> dnOrderMap = createDnOrderMap();
 
+    public static final String JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION = "TLSv1.2";
+    public static final String JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION = "TLSv1.3";
+    public static final String[] JAVA_8_SUPPORTED_TLS_PROTOCOL_VERSIONS = new String[]{JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION};
+    public static final String[] JAVA_11_SUPPORTED_TLS_PROTOCOL_VERSIONS = new String[]{JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION, JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION};
+
     static {
         Security.addProvider(new BouncyCastleProvider());
     }
@@ -120,67 +123,6 @@
         return Collections.unmodifiableMap(orderMap);
     }
 
-    public enum ClientAuth {
-        NONE(0, "none"),
-        WANT(1, "want"),
-        NEED(2, "need");
-
-        private int value;
-        private String description;
-
-        ClientAuth(int value, String description) {
-            this.value = value;
-            this.description = description;
-        }
-
-        @Override
-        public String toString() {
-            return "Client Auth: " + this.description + " (" + this.value + ")";
-        }
-    }
-
-    /**
-     * Returns true if the given keystore can be loaded using the given keystore type and password. Returns false otherwise.
-     *
-     * @param keystore     the keystore to validate
-     * @param keystoreType the type of the keystore
-     * @param password     the password to access the keystore
-     * @return true if valid; false otherwise
-     */
-    public static boolean isStoreValid(final URL keystore, final KeystoreType keystoreType, final char[] password) {
-
-        if (keystore == null) {
-            throw new IllegalArgumentException("keystore may not be null");
-        } else if (keystoreType == null) {
-            throw new IllegalArgumentException("keystore type may not be null");
-        } else if (password == null) {
-            throw new IllegalArgumentException("password may not be null");
-        }
-
-        BufferedInputStream bis = null;
-        final KeyStore ks;
-        try {
-
-            // load the keystore
-            bis = new BufferedInputStream(keystore.openStream());
-            ks = KeyStoreUtils.getKeyStore(keystoreType.name());
-            ks.load(bis, password);
-
-            return true;
-
-        } catch (Exception e) {
-            return false;
-        } finally {
-            if (bis != null) {
-                try {
-                    bis.close();
-                } catch (final IOException ioe) {
-                    logger.warn("Failed to close input stream", ioe);
-                }
-            }
-        }
-    }
-
     /**
      * Extracts the username from the specified DN. If the username cannot be extracted because the CN is in an unrecognized format, the entire CN is returned. If the CN cannot be extracted because
      * the DN is in an unrecognized format, the entire DN is returned.
@@ -245,7 +187,7 @@
 
     /**
      * Returns the DN extracted from the peer certificate (the server DN if run on the client; the client DN (if available) if run on the server).
-     *
+     * <p>
      * If the client auth setting is WANT or NONE and a client certificate is not present, this method will return {@code null}.
      * If the client auth is NEED, it will throw a {@link CertificateException}.
      *
@@ -260,15 +202,15 @@
 
             boolean clientMode = sslSocket.getUseClientMode();
             logger.debug("SSL Socket in {} mode", clientMode ? "client" : "server");
-            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+            SslContextFactory.ClientAuth clientAuth = getClientAuthStatus(sslSocket);
             logger.debug("SSL Socket client auth status: {}", clientAuth);
 
             if (clientMode) {
                 logger.debug("This socket is in client mode, so attempting to extract certificate from remote 'server' socket");
-               dn = extractPeerDNFromServerSSLSocket(sslSocket);
+                dn = extractPeerDNFromServerSSLSocket(sslSocket);
             } else {
                 logger.debug("This socket is in server mode, so attempting to extract certificate from remote 'client' socket");
-               dn = extractPeerDNFromClientSSLSocket(sslSocket);
+                dn = extractPeerDNFromClientSSLSocket(sslSocket);
             }
         }
 
@@ -277,7 +219,7 @@
 
     /**
      * Returns the DN extracted from the client certificate.
-     *
+     * <p>
      * If the client auth setting is WANT or NONE and a certificate is not present (and {@code respectClientAuth} is {@code true}), this method will return {@code null}.
      * If the client auth is NEED, it will throw a {@link CertificateException}.
      *
@@ -288,34 +230,34 @@
     private static String extractPeerDNFromClientSSLSocket(SSLSocket sslSocket) throws CertificateException {
         String dn = null;
 
-            /** The clientAuth value can be "need", "want", or "none"
-             * A client must send client certificates for need, should for want, and will not for none.
-             * This method should throw an exception if none are provided for need, return null if none are provided for want, and return null (without checking) for none.
-             */
+        /** The clientAuth value can be "need", "want", or "none"
+         * A client must send client certificates for need, should for want, and will not for none.
+         * This method should throw an exception if none are provided for need, return null if none are provided for want, and return null (without checking) for none.
+         */
 
-            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
-            logger.debug("SSL Socket client auth status: {}", clientAuth);
+        SslContextFactory.ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+        logger.debug("SSL Socket client auth status: {}", clientAuth);
 
-            if (clientAuth != ClientAuth.NONE) {
-                try {
-                    final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
-                    if (certChains != null && certChains.length > 0) {
-                        X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
-                        dn = x509Certificate.getSubjectDN().getName().trim();
-                        logger.debug("Extracted DN={} from client certificate", dn);
-                    }
-                } catch (SSLPeerUnverifiedException e) {
-                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
-                        logger.error("The incoming request did not contain client certificates and thus the DN cannot" +
-                                " be extracted. Check that the other endpoint is providing a complete client certificate chain");
-                    }
-                    if (clientAuth == ClientAuth.WANT) {
-                        logger.warn("Suppressing missing client certificate exception because client auth is set to 'want'");
-                        return dn;
-                    }
-                    throw new CertificateException(e);
+        if (clientAuth != SslContextFactory.ClientAuth.NONE) {
+            try {
+                final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
+                if (certChains != null && certChains.length > 0) {
+                    X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
+                    dn = x509Certificate.getSubjectDN().getName().trim();
+                    logger.debug("Extracted DN={} from client certificate", dn);
                 }
+            } catch (SSLPeerUnverifiedException e) {
+                if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                    logger.error("The incoming request did not contain client certificates and thus the DN cannot" +
+                            " be extracted. Check that the other endpoint is providing a complete client certificate chain");
+                }
+                if (clientAuth == SslContextFactory.ClientAuth.WANT) {
+                    logger.warn("Suppressing missing client certificate exception because client auth is set to 'want'");
+                    return dn;
+                }
+                throw new CertificateException(e);
             }
+        }
         return dn;
     }
 
@@ -330,26 +272,26 @@
         String dn = null;
         if (socket instanceof SSLSocket) {
             final SSLSocket sslSocket = (SSLSocket) socket;
-                try {
-                    final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
-                    if (certChains != null && certChains.length > 0) {
-                        X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
-                        dn = x509Certificate.getSubjectDN().getName().trim();
-                        logger.debug("Extracted DN={} from server certificate", dn);
-                    }
-                } catch (SSLPeerUnverifiedException e) {
-                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
-                        logger.error("The server did not present a certificate and thus the DN cannot" +
-                                " be extracted. Check that the other endpoint is providing a complete certificate chain");
-                    }
-                    throw new CertificateException(e);
+            try {
+                final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
+                if (certChains != null && certChains.length > 0) {
+                    X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
+                    dn = x509Certificate.getSubjectDN().getName().trim();
+                    logger.debug("Extracted DN={} from server certificate", dn);
                 }
+            } catch (SSLPeerUnverifiedException e) {
+                if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                    logger.error("The server did not present a certificate and thus the DN cannot" +
+                            " be extracted. Check that the other endpoint is providing a complete certificate chain");
+                }
+                throw new CertificateException(e);
+            }
         }
         return dn;
     }
 
-    private static ClientAuth getClientAuthStatus(SSLSocket sslSocket) {
-        return sslSocket.getNeedClientAuth() ? ClientAuth.NEED : sslSocket.getWantClientAuth() ? ClientAuth.WANT : ClientAuth.NONE;
+    private static SslContextFactory.ClientAuth getClientAuthStatus(SSLSocket sslSocket) {
+        return sslSocket.getNeedClientAuth() ? SslContextFactory.ClientAuth.REQUIRED : sslSocket.getWantClientAuth() ? SslContextFactory.ClientAuth.WANT : SslContextFactory.ClientAuth.NONE;
     }
 
     /**
@@ -360,6 +302,7 @@
      * @return a new {@code java.security.cert.X509Certificate}
      * @throws CertificateException if there is an error generating the new certificate
      */
+    @SuppressWarnings("deprecation")
     public static X509Certificate convertLegacyX509Certificate(javax.security.cert.X509Certificate legacyCertificate) throws CertificateException {
         if (legacyCertificate == null) {
             throw new IllegalArgumentException("The X.509 certificate cannot be null");
@@ -373,14 +316,14 @@
     }
 
     /**
-     * Accepts an abstract {@link Certificate} and returns an {@link X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} returns an array of the
+     * Accepts an abstract {@link java.security.cert.Certificate} and returns an {@link X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} returns an array of the
      * abstract certificates, they must be translated to X.509 to replace the functionality of {@code sslSocket.getSession().getPeerCertificateChain()}.
      *
      * @param abstractCertificate the {@code java.security.cert.Certificate}
      * @return a new {@code java.security.cert.X509Certificate}
      * @throws CertificateException if there is an error generating the new certificate
      */
-    public static X509Certificate convertAbstractX509Certificate(Certificate abstractCertificate) throws CertificateException {
+    public static X509Certificate convertAbstractX509Certificate(java.security.cert.Certificate abstractCertificate) throws CertificateException {
         if (abstractCertificate == null || !(abstractCertificate instanceof X509Certificate)) {
             throw new IllegalArgumentException("The certificate cannot be null and must be an X.509 certificate");
         }
@@ -405,9 +348,9 @@
 
     /**
      * Reorders DN to the order the elements appear in the RFC 2253 table
-     *
+     * <p>
      * https://www.ietf.org/rfc/rfc2253.txt
-     *
+     * <p>
      * String  X.500 AttributeType
      * ------------------------------
      * CN      commonName
@@ -456,7 +399,7 @@
 
     /**
      * Reverses the X500Name in order make the certificate be in the right order
-     * [see https://stackoverflow.com/questions/7567837/attributes-reversed-in-certificate-subject-and-issuer/12645265]
+     * [see http://stackoverflow.com/questions/7567837/attributes-reversed-in-certificate-subject-and-issuer/12645265]
      *
      * @param x500Name the X500Name created with the intended order
      * @return the X500Name reversed
@@ -498,7 +441,7 @@
      * @param signingAlgorithm        the signing algorithm to use for the {@link X509Certificate}
      * @param certificateDurationDays the duration in days for which the {@link X509Certificate} should be valid
      * @return a self-signed {@link X509Certificate} suitable for use as a Certificate Authority
-     * @throws CertificateException      if there is an generating the new certificate
+     * @throws CertificateException if there is an generating the new certificate
      */
     public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays)
             throws CertificateException {
@@ -540,12 +483,12 @@
     /**
      * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
      *
-     * @param dn the distinguished name to use
-     * @param publicKey the public key to issue the certificate to
-     * @param issuer the issuer's certificate
-     * @param issuerKeyPair the issuer's keypair
+     * @param dn               the distinguished name to use
+     * @param publicKey        the public key to issue the certificate to
+     * @param issuer           the issuer's certificate
+     * @param issuerKeyPair    the issuer's keypair
      * @param signingAlgorithm the signing algorithm to use
-     * @param days the number of days it should be valid for
+     * @param days             the number of days it should be valid for
      * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
      * @throws CertificateException if there is an error issuing the certificate
      */
@@ -557,13 +500,13 @@
     /**
      * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
      *
-     * @param dn the distinguished name to use
-     * @param publicKey the public key to issue the certificate to
-     * @param extensions extensions extracted from the CSR
-     * @param issuer the issuer's certificate
-     * @param issuerKeyPair the issuer's keypair
+     * @param dn               the distinguished name to use
+     * @param publicKey        the public key to issue the certificate to
+     * @param extensions       extensions extracted from the CSR
+     * @param issuer           the issuer's certificate
+     * @param issuerKeyPair    the issuer's keypair
      * @param signingAlgorithm the signing algorithm to use
-     * @param days the number of days it should be valid for
+     * @param days             the number of days it should be valid for
      * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
      * @throws CertificateException if there is an error issuing the certificate
      */
@@ -596,7 +539,7 @@
             certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));
 
             // (3) subjectAlternativeName
-            if(extensions != null && extensions.getExtension(Extension.subjectAlternativeName) != null) {
+            if (extensions != null && extensions.getExtension(Extension.subjectAlternativeName) != null) {
                 certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName));
             }
 
@@ -609,15 +552,15 @@
 
     /**
      * Returns true if the two provided DNs are equivalent, regardless of the order of the elements. Returns false if one or both are invalid DNs.
-     *
+     * <p>
      * Example:
-     *
+     * <p>
      * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true
      * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true
      * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false
      * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false
      * CN=test1, O=testOrg, C=US compared to                           -> false
-     *                           compared to                           -> true
+     * compared to                           -> true
      *
      * @param dn1 the first DN to compare
      * @param dn2 the second DN to compare
@@ -665,6 +608,86 @@
         return null;
     }
 
+    /**
+     * Returns {@code true} if this exception is due to a TLS problem (either directly or because of its cause, if present). Traverses the cause chain recursively.
+     *
+     * @param e the exception to evaluate
+     * @return true if the direct or indirect cause of this exception was TLS-related
+     */
+    public static boolean isTlsError(Throwable e) {
+        if (e == null) {
+            return false;
+        } else {
+            if (e instanceof CertificateException || e instanceof TlsException || e instanceof SSLException) {
+                return true;
+            } else if (e.getCause() != null) {
+                return isTlsError(e.getCause());
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Returns the JVM Java major version based on the System properties (e.g. {@code JVM 1.8.0.231} -> {code 8}).
+     *
+     * @return the Java major version
+     */
+    public static int getJavaVersion() {
+        String version = System.getProperty("java.version");
+        return parseJavaVersion(version);
+    }
+
+    /**
+     * Returns the major version parsed from the provided Java version string (e.g. {@code "1.8.0.231"} -> {@code 8}).
+     *
+     * @param version the Java version string
+     * @return the major version as an int
+     */
+    public static int parseJavaVersion(String version) {
+        String majorVersion;
+        if (version.startsWith("1.")) {
+            majorVersion = version.substring(2, 3);
+        } else {
+            Pattern majorVersion9PlusPattern = Pattern.compile("(\\d+).*");
+            Matcher m = majorVersion9PlusPattern.matcher(version);
+            if (m.find()) {
+                majorVersion = m.group(1);
+            } else {
+                throw new IllegalArgumentException("Could not detect major version of " + version);
+            }
+        }
+        return Integer.parseInt(majorVersion);
+    }
+
+    /**
+     * Returns a {@code String[]} of supported TLS protocol versions based on the current Java platform version.
+     *
+     * @return the supported TLS protocol version(s)
+     */
+    public static String[] getCurrentSupportedTlsProtocolVersions() {
+        int javaMajorVersion = getJavaVersion();
+        if (javaMajorVersion < 11) {
+            return JAVA_8_SUPPORTED_TLS_PROTOCOL_VERSIONS;
+        } else {
+            return JAVA_11_SUPPORTED_TLS_PROTOCOL_VERSIONS;
+        }
+    }
+
+    /**
+     * Returns the highest supported TLS protocol version based on the current Java platform version.
+     *
+     * @return the TLS protocol (e.g. {@code "TLSv1.2"})
+     */
+    public static String getHighestCurrentSupportedTlsProtocolVersion() {
+        int javaMajorVersion = getJavaVersion();
+        if (javaMajorVersion < 11) {
+            return JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION;
+        } else {
+            return JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION;
+        }
+    }
+
     private CertificateUtils() {
     }
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index 7b0446d..3af1a0d 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -24,6 +24,14 @@
 import org.apache.nifi.registry.authorization.Permissions;
 import org.apache.nifi.registry.authorization.Tenant;
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.AccessClient;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+import org.apache.nifi.registry.client.impl.request.BearerTokenRequestConfig;
 import org.apache.nifi.registry.extension.ExtensionManager;
 import org.apache.nifi.registry.properties.AESSensitivePropertyProvider;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
@@ -36,6 +44,7 @@
 import org.apache.nifi.registry.security.identity.IdentityMapper;
 import org.apache.nifi.registry.service.RegistryService;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -55,9 +64,9 @@
 
 import javax.sql.DataSource;
 import javax.ws.rs.client.Entity;
-import javax.ws.rs.core.Form;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.io.IOException;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -614,6 +623,51 @@
 
     }
 
+    @Test
+    public void testAccessClient() throws IOException, NiFiRegistryException {
+        final String baseUrl = createBaseURL();
+        LOGGER.info("Using base url = " + baseUrl);
+
+        final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl);
+        Assert.assertNotNull(clientConfig);
+
+        final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder()
+                .config(clientConfig)
+                .build();
+
+        final String username = "pasteur";
+        final String password = "password";
+
+        // authenticate with the username and password to obtain a token
+        final AccessClient accessClient = client.getAccessClient();
+        final String token = accessClient.getToken(username, password);
+        assertNotNull(token);
+
+        // use the token to check the status of the current user
+        final RequestConfig requestConfig = new BearerTokenRequestConfig(token);
+        final UserClient userClient = client.getUserClient(requestConfig);
+        assertEquals(username, userClient.getAccessStatus().getIdentity());
+
+        // use the token to logout
+        accessClient.logout(token);
+
+        // check the status of the current user again and should be unauthorized
+        try {
+            userClient.getAccessStatus();
+            Assert.fail("Should have failed with an unauthorized exception");
+        } catch (Exception e) {
+            //LOGGER.error(e.getMessage(), e);
+        }
+
+        // try to get a token with an invalid username and password
+        try {
+            accessClient.getToken("user-does-not-exist", "bad-password");
+            Assert.fail("Should have failed with an unauthorized exception");
+        } catch (Exception e) {
+
+        }
+    }
+
     /** A helper method to lookup identifiers for tenant identities using the REST API
      *
      * @param tenantIdentity - the identity to lookup
@@ -746,12 +800,6 @@
 
     }
 
-    private static Form encodeCredentialsForURLFormParams(String username, String password) {
-        return new Form()
-                .param("username", username)
-                .param("password", password);
-    }
-
     private static String encodeCredentialsForBasicAuth(String username, String password) {
         final String credentials = username + ":" + password;
         final String base64credentials =  new String(Base64.getEncoder().encode(credentials.getBytes(Charset.forName("UTF-8"))));
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
index e49edf2..92f5458 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
@@ -28,9 +28,11 @@
 import org.apache.nifi.registry.client.NiFiRegistryClient;
 import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.client.TenantsClient;
 import org.apache.nifi.registry.client.UserClient;
 import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
@@ -164,7 +166,8 @@
     @Test
     public void testGetAccessStatusWithProxiedEntity() throws IOException, NiFiRegistryException {
         final String proxiedEntity = "user2";
-        final UserClient userClient = client.getUserClient(proxiedEntity);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        final UserClient userClient = client.getUserClient(requestConfig);
         final CurrentUser status = userClient.getAccessStatus();
         assertEquals("user2", status.getIdentity());
         assertFalse(status.isAnonymous());
@@ -173,7 +176,8 @@
     @Test
     public void testCreatedBucketWithProxiedEntity() throws IOException, NiFiRegistryException {
         final String proxiedEntity = "user2";
-        final BucketClient bucketClient = client.getBucketClient(proxiedEntity);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        final BucketClient bucketClient = client.getBucketClient(requestConfig);
 
         final Bucket bucket = new Bucket();
         bucket.setName("Bucket 1");
@@ -193,8 +197,9 @@
         // this user shouldn't have access to anything
         final String proxiedEntity = NO_ACCESS_IDENTITY;
 
-        final FlowClient proxiedFlowClient = client.getFlowClient(proxiedEntity);
-        final FlowSnapshotClient proxiedFlowSnapshotClient = client.getFlowSnapshotClient(proxiedEntity);
+        final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity);
+        final FlowClient proxiedFlowClient = client.getFlowClient(requestConfig);
+        final FlowSnapshotClient proxiedFlowSnapshotClient = client.getFlowSnapshotClient(requestConfig);
 
         try {
             proxiedFlowClient.get("1");