diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionJsonRpcUnauthorizedResponse.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/ExpectLoginDisabled.java
similarity index 62%
rename from acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionJsonRpcUnauthorizedResponse.java
rename to acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/ExpectLoginDisabled.java
index 91e5ecfd630..6f68a38c105 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionJsonRpcUnauthorizedResponse.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/ExpectLoginDisabled.java
@@ -12,23 +12,16 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package org.hyperledger.besu.tests.acceptance.dsl.condition.net;
+package org.hyperledger.besu.tests.acceptance.dsl.condition.login;
 
 import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition;
 import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
-import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetVersionTransaction;
+import org.hyperledger.besu.tests.acceptance.dsl.transaction.login.LoginDisabledTransaction;
 
-public class ExpectNetVersionPermissionJsonRpcUnauthorizedResponse implements Condition {
-
-  private final NetVersionTransaction transaction;
-
-  public ExpectNetVersionPermissionJsonRpcUnauthorizedResponse(
-      final NetVersionTransaction transaction) {
-    this.transaction = transaction;
-  }
+public class ExpectLoginDisabled implements Condition {
 
   @Override
   public void verify(final Node node) {
-    node.execute(transaction);
+    node.execute(new LoginDisabledTransaction());
   }
 }
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java
index 09e4e831547..234d1324b93 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java
@@ -27,6 +27,10 @@ public Condition failure(final String username, final String password) {
     return new ExpectLoginUnauthorized(username, password);
   }
 
+  public Condition disabled() {
+    return new ExpectLoginDisabled();
+  }
+
   public Condition awaitResponse(final String username, final String password) {
     return new AwaitLoginResponse<>(new LoginTransaction(username, password));
   }
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionException.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionException.java
deleted file mode 100644
index 56bdc0fc59f..00000000000
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionException.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright ConsenSys AG.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-package org.hyperledger.besu.tests.acceptance.dsl.condition.net;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.catchThrowable;
-
-import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition;
-import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
-import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetVersionTransaction;
-
-import org.web3j.protocol.exceptions.ClientConnectionException;
-
-public class ExpectNetVersionPermissionException implements Condition {
-
-  private final NetVersionTransaction transaction;
-  private final String expectedMessage;
-
-  public ExpectNetVersionPermissionException(
-      final NetVersionTransaction transaction, final String expectedMessage) {
-    this.transaction = transaction;
-    this.expectedMessage = expectedMessage;
-  }
-
-  @Override
-  public void verify(final Node node) {
-    final Throwable thrown = catchThrowable(() -> node.execute(transaction));
-    assertThat(thrown).isInstanceOf(RuntimeException.class);
-
-    final Throwable cause = thrown.getCause();
-    assertThat(cause).isInstanceOf(ClientConnectionException.class);
-    assertThat(cause.getMessage()).contains(expectedMessage);
-  }
-}
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionConnectionExceptionWithCause.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectUnauthorized.java
similarity index 69%
rename from acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionConnectionExceptionWithCause.java
rename to acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectUnauthorized.java
index 7dbc96bf390..37a182bc4af 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionConnectionExceptionWithCause.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectUnauthorized.java
@@ -19,22 +19,20 @@
 
 import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition;
 import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
-import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetVersionTransaction;
+import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
 
-public class ExpectNetVersionConnectionExceptionWithCause implements Condition {
+public class ExpectUnauthorized implements Condition {
 
-  private final NetVersionTransaction transaction;
-  private final Class<? extends Throwable> cause;
+  private static final String UNAUTHORIZED = "Unauthorized";
+  private final Transaction<?> transaction;
 
-  public ExpectNetVersionConnectionExceptionWithCause(
-      final NetVersionTransaction transaction, final Class<? extends Throwable> cause) {
+  public ExpectUnauthorized(final Transaction<?> transaction) {
     this.transaction = transaction;
-    this.cause = cause;
   }
 
   @Override
   public void verify(final Node node) {
     final Throwable thrown = catchThrowable(() -> node.execute(transaction));
-    assertThat(thrown).isInstanceOf(cause);
+    assertThat(thrown.getMessage()).contains(UNAUTHORIZED);
   }
 }
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java
index 39044410241..004b12b9162 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java
@@ -35,6 +35,10 @@ public Condition netServicesOnlyJsonRpcEnabled() {
     return new ExpectNetServicesReturnsOnlyJsonRpcActive(transactions.netServices());
   }
 
+  public Condition netServicesUnauthorized() {
+    return new ExpectUnauthorized(transactions.netServices());
+  }
+
   public Condition netVersion() {
     return new ExpectNetVersionIsNotBlank(transactions.netVersion());
   }
@@ -47,16 +51,8 @@ public Condition netVersionExceptional(final String expectedMessage) {
     return new ExpectNetVersionConnectionException(transactions.netVersion(), expectedMessage);
   }
 
-  public Condition netVersionExceptional(final Class<? extends Throwable> cause) {
-    return new ExpectNetVersionConnectionExceptionWithCause(transactions.netVersion(), cause);
-  }
-
-  public Condition netVersionUnauthorizedExceptional(final String expectedMessage) {
-    return new ExpectNetVersionPermissionException(transactions.netVersion(), expectedMessage);
-  }
-
-  public Condition netVersionUnauthorizedResponse() {
-    return new ExpectNetVersionPermissionJsonRpcUnauthorizedResponse(transactions.netVersion());
+  public Condition netVersionUnauthorized() {
+    return new ExpectUnauthorized(transactions.netVersion());
   }
 
   public Condition awaitPeerCountExceptional() {
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
index 5032a8cc0b3..246766603ec 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
@@ -132,6 +132,10 @@ public void startNode(final BesuNode node) {
         params.add("--rpc-http-authentication-credentials-file");
         params.add(node.jsonRpcConfiguration().getAuthenticationCredentialsFile());
       }
+      if (node.jsonRpcConfiguration().getAuthenticationPublicKeyFile() != null) {
+        params.add("--rpc-http-authentication-jwt-public-key-file");
+        params.add(node.jsonRpcConfiguration().getAuthenticationPublicKeyFile().getAbsolutePath());
+      }
     }
 
     if (node.wsRpcEnabled()) {
@@ -149,6 +153,11 @@ public void startNode(final BesuNode node) {
         params.add("--rpc-ws-authentication-credentials-file");
         params.add(node.webSocketConfiguration().getAuthenticationCredentialsFile());
       }
+      if (node.webSocketConfiguration().getAuthenticationPublicKeyFile() != null) {
+        params.add("--rpc-ws-authentication-jwt-public-key-file");
+        params.add(
+            node.webSocketConfiguration().getAuthenticationPublicKeyFile().getAbsolutePath());
+      }
     }
 
     if (node.isMetricsEnabled()) {
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java
index b9b5f3fd947..de7d09bf3d5 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java
@@ -26,6 +26,7 @@
 import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
 import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationProvider;
 
+import java.io.File;
 import java.net.URISyntaxException;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -114,6 +115,19 @@ public BesuNodeConfigurationBuilder jsonRpcAuthenticationEnabled() throws URISyn
     return this;
   }
 
+  public BesuNodeConfigurationBuilder jsonRpcAuthenticationUsingPublicKeyEnabled()
+      throws URISyntaxException {
+    final File jwtPublicKey =
+        Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key").toURI())
+            .toAbsolutePath()
+            .toFile();
+
+    this.jsonRpcConfiguration.setAuthenticationEnabled(true);
+    this.jsonRpcConfiguration.setAuthenticationPublicKeyFile(jwtPublicKey);
+
+    return this;
+  }
+
   public BesuNodeConfigurationBuilder webSocketConfiguration(
       final WebSocketConfiguration webSocketConfiguration) {
     this.webSocketConfiguration = webSocketConfiguration;
@@ -130,7 +144,7 @@ public BesuNodeConfigurationBuilder webSocketEnabled() {
     final WebSocketConfiguration config = WebSocketConfiguration.createDefault();
     config.setEnabled(true);
     config.setPort(0);
-    config.setHostsWhitelist(Collections.singleton("*"));
+    config.setHostsWhitelist(Collections.singletonList("*"));
 
     this.webSocketConfiguration = config;
     return this;
@@ -153,6 +167,19 @@ public BesuNodeConfigurationBuilder webSocketAuthenticationEnabled() throws URIS
     return this;
   }
 
+  public BesuNodeConfigurationBuilder webSocketAuthenticationUsingPublicKeyEnabled()
+      throws URISyntaxException {
+    final File jwtPublicKey =
+        Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key").toURI())
+            .toAbsolutePath()
+            .toFile();
+
+    this.webSocketConfiguration.setAuthenticationEnabled(true);
+    this.webSocketConfiguration.setAuthenticationPublicKeyFile(jwtPublicKey);
+
+    return this;
+  }
+
   public BesuNodeConfigurationBuilder permissioningConfiguration(
       final PermissioningConfiguration permissioningConfiguration) {
     this.permissioningConfiguration = Optional.of(permissioningConfiguration);
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java
index d40b1140a6b..1707e047a35 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java
@@ -128,7 +128,7 @@ public BesuNode createArchiveNodeNetServicesDisabled(final String name) throws I
             .build());
   }
 
-  public BesuNode createArchiveNodeWithAuthentication(final String name)
+  public BesuNode createNodeWithAuthentication(final String name)
       throws IOException, URISyntaxException {
     return create(
         new BesuNodeConfigurationBuilder()
@@ -136,16 +136,19 @@ public BesuNode createArchiveNodeWithAuthentication(final String name)
             .jsonRpcEnabled()
             .jsonRpcAuthenticationEnabled()
             .webSocketEnabled()
+            .webSocketAuthenticationEnabled()
             .build());
   }
 
-  public BesuNode createArchiveNodeWithAuthenticationOverWebSocket(final String name)
+  public BesuNode createNodeWithAuthenticationUsingJwtPublicKey(final String name)
       throws IOException, URISyntaxException {
     return create(
         new BesuNodeConfigurationBuilder()
             .name(name)
+            .jsonRpcEnabled()
+            .jsonRpcAuthenticationUsingPublicKeyEnabled()
             .webSocketEnabled()
-            .webSocketAuthenticationEnabled()
+            .webSocketAuthenticationUsingPublicKeyEnabled()
             .build());
   }
 
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/login/LoginDisabledTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/login/LoginDisabledTransaction.java
new file mode 100644
index 00000000000..f4a556aef7c
--- /dev/null
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/login/LoginDisabledTransaction.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.tests.acceptance.dsl.transaction.login;
+
+import static org.assertj.core.api.Fail.fail;
+
+import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests;
+import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
+
+import java.io.IOException;
+
+import org.assertj.core.api.Assertions;
+
+public class LoginDisabledTransaction implements Transaction<Void> {
+
+  @Override
+  public Void execute(final NodeRequests node) {
+    try {
+      final String loginResponse = node.login().send("user", "password");
+      Assertions.assertThat(loginResponse).isEqualTo("Authentication not enabled");
+      return null;
+    } catch (final IOException e) {
+      fail("Login request failed with exception: ", e);
+      return null;
+    }
+  }
+}
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java
index f4b87f45af1..2b3386a0d91 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java
@@ -37,6 +37,9 @@ public Map<String, Map<String, String>> execute(final NodeRequests requestFactor
     } catch (final Exception e) {
       throw new RuntimeException(e);
     }
+    if (netServicesResponse.hasError()) {
+      throw new RuntimeException(netServicesResponse.getError().getMessage());
+    }
     return netServicesResponse.getResult();
   }
 }
diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java
index 6cb7950ad47..8d4db1397cb 100644
--- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java
+++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java
@@ -30,6 +30,9 @@ public String execute(final NodeRequests node) {
     try {
       final NetVersion result = node.net().netVersion().send();
       assertThat(result).isNotNull();
+      if (result.hasError()) {
+        throw new RuntimeException(result.getError().getMessage());
+      }
       return result.getNetVersion();
     } catch (final Exception e) {
       throw new RuntimeException(e);
diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java
index 58cd487175d..5ff7dd6d3ac 100644
--- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java
+++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java
@@ -27,39 +27,84 @@
 import org.junit.Test;
 
 public class HttpServiceLoginAcceptanceTest extends AcceptanceTestBase {
-
   private Cluster authenticatedCluster;
-  private BesuNode node;
+  private BesuNode nodeUsingAuthFile;
+  private BesuNode nodeUsingJwtPublicKey;
+
+  // token with payload{"iat": 1516239022,"exp": 4729363200,"permissions": ["net:peerCount"]}
+  private static final String TOKEN_ALLOWING_NET_PEER_COUNT =
+      "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWl"
+          + "zc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ.Y6mNV0nvjzOdqAgMgxknFAOUTKoeRAo4aifNgNrWtuXbJJgz6-"
+          + "H_0GvLgjlToohPiDZbBJXJJlgb4zzLLB-sRtFnGoPaMgz_d_6z958GjFD7x_Fl0HW-WrTjRNenZNfTyD86OEAf"
+          + "XHy-7N3OYY2a5yeDbppTJy6nnHTq9hY-ad22-oWL1RbK3T_hnUJII_uXCZ9bJggSfu5m-NNUrm3TeqdnQzIaIz"
+          + "DqHlL0wNZwVPB4cFGN7zKghReBpkRJ8OFlxexQ491Q5eSpuYquhef-yGCIaMfy7GVtpDSD3Y-hjOErr7gUNCUh"
+          + "1wlc3Rb7ru_0qNgCWTBPJeRK32GppYotwQ";
 
   @Before
   public void setUp() throws IOException, URISyntaxException {
     final ClusterConfiguration clusterConfiguration =
         new ClusterConfigurationBuilder().awaitPeerDiscovery(false).build();
-
     authenticatedCluster = new Cluster(clusterConfiguration, net);
-    node = besu.createArchiveNodeWithAuthentication("node1");
-    authenticatedCluster.start(node);
-    node.verify(login.awaitResponse("user", "badpassword"));
+
+    nodeUsingAuthFile = besu.createNodeWithAuthentication("node1");
+    nodeUsingJwtPublicKey = besu.createNodeWithAuthenticationUsingJwtPublicKey("node2");
+    authenticatedCluster.start(nodeUsingAuthFile, nodeUsingJwtPublicKey);
+
+    nodeUsingAuthFile.verify(login.awaitResponse("user", "badpassword"));
+    nodeUsingJwtPublicKey.verify(login.awaitResponse("user", "badpassword"));
   }
 
   @Test
   public void shouldFailLoginWithWrongCredentials() {
-    node.verify(login.failure("user", "badpassword"));
+    nodeUsingAuthFile.verify(login.failure("user", "badpassword"));
   }
 
   @Test
   public void shouldSucceedLoginWithCorrectCredentials() {
-    node.verify(login.success("user", "pegasys"));
+    nodeUsingAuthFile.verify(login.success("user", "pegasys"));
   }
 
   @Test
   public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() {
     final String token =
-        node.execute(permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
-    node.useAuthenticationTokenInHeaderForJsonRpc(token);
+        nodeUsingAuthFile.execute(
+            permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
+    nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
+    nodeUsingAuthFile.verify(net.awaitPeerCount(1));
+  }
+
+  @Test
+  public void jsonRpcMethodShouldFailOnNonPermittedMethod() {
+    final String token =
+        nodeUsingAuthFile.execute(
+            permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
+    nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
+    nodeUsingAuthFile.verify(net.netVersionUnauthorized());
+    nodeUsingAuthFile.verify(net.netServicesUnauthorized());
+  }
 
-    node.verify(net.awaitPeerCount(0));
-    node.verify(net.netVersionUnauthorizedExceptional("Unauthorized"));
+  @Test
+  public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() {
+    nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT);
+    nodeUsingJwtPublicKey.verify(net.awaitPeerCount(1));
+  }
+
+  @Test
+  public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldFailOnNonPermittedMethod() {
+    nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT);
+    nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized());
+    nodeUsingJwtPublicKey.verify(net.netServicesUnauthorized());
+  }
+
+  @Test
+  public void jsonRpcMethodShouldFailWhenThereIsNoToken() {
+    nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized());
+    nodeUsingJwtPublicKey.verify(net.netServicesUnauthorized());
+  }
+
+  @Test
+  public void loginShouldBeDisabledWhenUsingExternalJwtPublicKey() {
+    nodeUsingJwtPublicKey.verify(login.disabled());
   }
 
   @Override
diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java
index 96e7872138b..e81cdad8647 100644
--- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java
+++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java
@@ -16,6 +16,9 @@
 
 import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase;
 import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode;
+import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.Cluster;
+import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfiguration;
+import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfigurationBuilder;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -24,31 +27,91 @@
 import org.junit.Test;
 
 public class WebsocketServiceLoginAcceptanceTest extends AcceptanceTestBase {
-  private BesuNode node;
+  private BesuNode nodeUsingAuthFile;
+  private BesuNode nodeUsingJwtPublicKey;
+  private Cluster authenticatedCluster;
+
+  // token with payload{"iat": 1516239022,"exp": 4729363200,"permissions": ["net:peerCount"]}
+  private static final String TOKEN_ALLOWING_NET_PEER_COUNT =
+      "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWl"
+          + "zc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ.Y6mNV0nvjzOdqAgMgxknFAOUTKoeRAo4aifNgNrWtuXbJJgz6-"
+          + "H_0GvLgjlToohPiDZbBJXJJlgb4zzLLB-sRtFnGoPaMgz_d_6z958GjFD7x_Fl0HW-WrTjRNenZNfTyD86OEAf"
+          + "XHy-7N3OYY2a5yeDbppTJy6nnHTq9hY-ad22-oWL1RbK3T_hnUJII_uXCZ9bJggSfu5m-NNUrm3TeqdnQzIaIz"
+          + "DqHlL0wNZwVPB4cFGN7zKghReBpkRJ8OFlxexQ491Q5eSpuYquhef-yGCIaMfy7GVtpDSD3Y-hjOErr7gUNCUh"
+          + "1wlc3Rb7ru_0qNgCWTBPJeRK32GppYotwQ";
 
   @Before
   public void setUp() throws IOException, URISyntaxException {
-    node = besu.createArchiveNodeWithAuthenticationOverWebSocket("node1");
-    cluster.start(node);
-    node.useWebSocketsForJsonRpc();
+    final ClusterConfiguration clusterConfiguration =
+        new ClusterConfigurationBuilder().awaitPeerDiscovery(false).build();
+    authenticatedCluster = new Cluster(clusterConfiguration, net);
+
+    nodeUsingAuthFile = besu.createNodeWithAuthentication("node1");
+    nodeUsingJwtPublicKey = besu.createNodeWithAuthenticationUsingJwtPublicKey("node2");
+    authenticatedCluster.start(nodeUsingAuthFile, nodeUsingJwtPublicKey);
+
+    nodeUsingAuthFile.useWebSocketsForJsonRpc();
+    nodeUsingJwtPublicKey.useWebSocketsForJsonRpc();
+    nodeUsingAuthFile.verify(login.awaitResponse("user", "badpassword"));
+    nodeUsingJwtPublicKey.verify(login.awaitResponse("user", "badpassword"));
   }
 
   @Test
   public void shouldFailLoginWithWrongCredentials() {
-    node.verify(login.failure("user", "badpassword"));
+    nodeUsingAuthFile.verify(login.failure("user", "badpassword"));
   }
 
   @Test
   public void shouldSucceedLoginWithCorrectCredentials() {
-    node.verify(login.success("user", "pegasys"));
+    nodeUsingAuthFile.verify(login.success("user", "pegasys"));
   }
 
   @Test
   public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() {
     final String token =
-        node.execute(permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
-    node.useAuthenticationTokenInHeaderForJsonRpc(token);
-    node.verify(net.awaitPeerCount(0));
-    node.verify(net.netVersionUnauthorizedResponse());
+        nodeUsingAuthFile.execute(
+            permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
+    nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
+    nodeUsingAuthFile.verify(net.awaitPeerCount(1));
+  }
+
+  @Test
+  public void jsonRpcMethodShouldFailOnNonPermittedMethod() {
+    final String token =
+        nodeUsingAuthFile.execute(
+            permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
+    nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
+    nodeUsingAuthFile.verify(net.netVersionUnauthorized());
+    nodeUsingAuthFile.verify(net.netServicesUnauthorized());
+  }
+
+  @Test
+  public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() {
+    nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT);
+    nodeUsingJwtPublicKey.verify(net.awaitPeerCount(1));
+  }
+
+  @Test
+  public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldFailOnNonPermittedMethod() {
+    nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT);
+    nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized());
+    nodeUsingAuthFile.verify(net.netServicesUnauthorized());
+  }
+
+  @Test
+  public void jsonRpcMethodShouldFailWhenThereIsNoToken() {
+    nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized());
+    nodeUsingJwtPublicKey.verify(net.netServicesUnauthorized());
+  }
+
+  @Test
+  public void loginShouldBeDisabledWhenUsingExternalJwtPublicKey() {
+    nodeUsingJwtPublicKey.verify(login.disabled());
+  }
+
+  @Override
+  public void tearDownAcceptanceTestBase() {
+    authenticatedCluster.stop();
+    super.tearDownAcceptanceTestBase();
   }
 }
diff --git a/acceptance-tests/tests/src/test/resources/authentication/jwt_public_key b/acceptance-tests/tests/src/test/resources/authentication/jwt_public_key
new file mode 100644
index 00000000000..9107cf29702
--- /dev/null
+++ b/acceptance-tests/tests/src/test/resources/authentication/jwt_public_key
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6tMhjogMulRbYby7bCL
+rFhukDnxvm4XR3KSXLKdLLQHHyouMOQaLac9M+/Z1KkIpqfZPjLfW2/yUg2IKx4T
+dvFVzbVq17X6dq49ZS8jJtb8l2+Vius4d3LnpvxCOematRG9Acn+2qLwC+sK7RPY
+OxEqKPU5LNBH1C0FfviazY5jkixBFICzIq/SyyRnGX+iIONnNsu0TlhWVLSlZbg5
+NYf4cAzu/1d5MgspyZwnRo468gqaak3wQzkmk69Z25L1N7TXZvk2b7rT7/ssFnt+
+//fKVpD6qkQ3OopD+7gOziAYUxChw6RUWekV+uRgNADQhaqV6wDdogBz77wTJedV
+YwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
index 7c269ea5b7b..1da7a15dbea 100644
--- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
+++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
@@ -1151,12 +1151,15 @@ private JsonRpcConfiguration jsonRpcConfiguration() {
             "--rpc-http-host",
             "--rpc-http-port",
             "--rpc-http-authentication-enabled",
-            "--rpc-http-authentication-credentials-file"));
+            "--rpc-http-authentication-credentials-file",
+            "--rpc-http-authentication-public-key-file"));
 
-    if (isRpcHttpAuthenticationEnabled && rpcHttpAuthenticationCredentialsFile() == null) {
+    if (isRpcHttpAuthenticationEnabled
+        && rpcHttpAuthenticationCredentialsFile() == null
+        && rpcHttpAuthenticationPublicKeyFile() == null) {
       throw new ParameterException(
           commandLine,
-          "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file");
+          "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file");
     }
 
     final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault();
@@ -1168,6 +1171,7 @@ private JsonRpcConfiguration jsonRpcConfiguration() {
     jsonRpcConfiguration.setHostsWhitelist(hostsWhitelist);
     jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled);
     jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile());
+    jsonRpcConfiguration.setAuthenticationPublicKeyFile(rpcHttpAuthenticationPublicKeyFile());
     return jsonRpcConfiguration;
   }
 
@@ -1184,12 +1188,15 @@ private WebSocketConfiguration webSocketConfiguration() {
             "--rpc-ws-host",
             "--rpc-ws-port",
             "--rpc-ws-authentication-enabled",
-            "--rpc-ws-authentication-credentials-file"));
+            "--rpc-ws-authentication-credentials-file",
+            "--rpc-ws-authentication-public-key-file"));
 
-    if (isRpcWsAuthenticationEnabled && rpcWsAuthenticationCredentialsFile() == null) {
+    if (isRpcWsAuthenticationEnabled
+        && rpcWsAuthenticationCredentialsFile() == null
+        && rpcWsAuthenticationPublicKeyFile() == null) {
       throw new ParameterException(
           commandLine,
-          "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file");
+          "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file");
     }
 
     final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
@@ -1200,6 +1207,7 @@ private WebSocketConfiguration webSocketConfiguration() {
     webSocketConfiguration.setAuthenticationEnabled(isRpcWsAuthenticationEnabled);
     webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile());
     webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
+    webSocketConfiguration.setAuthenticationPublicKeyFile(rpcWsAuthenticationPublicKeyFile());
     return webSocketConfiguration;
   }
 
@@ -1680,6 +1688,24 @@ private String rpcWsAuthenticationCredentialsFile() {
     return filename;
   }
 
+  private File rpcHttpAuthenticationPublicKeyFile() {
+    if (isDocker) {
+      final File keyFile = new File(DOCKER_RPC_HTTP_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION);
+      return keyFile.exists() ? keyFile : null;
+    } else {
+      return standaloneCommands.rpcHttpAuthenticationPublicKeyFile;
+    }
+  }
+
+  private File rpcWsAuthenticationPublicKeyFile() {
+    if (isDocker) {
+      final File keyFile = new File(DOCKER_RPC_WS_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION);
+      return keyFile.exists() ? keyFile : null;
+    } else {
+      return standaloneCommands.rpcWsAuthenticationPublicKeyFile;
+    }
+  }
+
   private String nodePermissionsConfigFile() {
     return permissionsConfigFile(standaloneCommands.nodePermissionsConfigFile);
   }
diff --git a/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java b/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
index 3ae903cd490..3e3cd4c21bc 100644
--- a/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
+++ b/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
@@ -55,6 +55,9 @@ public interface DefaultCommandValues {
       "/etc/besu/rpc_http_auth_config.toml";
   String DOCKER_RPC_WS_AUTHENTICATION_CREDENTIALS_FILE_LOCATION =
       "/etc/besu/rpc_ws_auth_config.toml";
+  String DOCKER_RPC_HTTP_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION =
+      "/etc/besu/rpc_http_auth_public_key";
+  String DOCKER_RPC_WS_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION = "/etc/besu/rpc_ws_auth_public_key";
   String DOCKER_PRIVACY_PUBLIC_KEY_FILE = "/etc/besu/privacy_public_key";
   String DOCKER_PERMISSIONS_CONFIG_FILE_LOCATION = "/etc/besu/permissions_config.toml";
   String PERMISSIONING_CONFIG_LOCATION = "permissions_config.toml";
diff --git a/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java
index 18ade6d464d..7990a8f79d1 100644
--- a/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java
+++ b/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java
@@ -65,6 +65,13 @@ class StandaloneCommand implements DefaultCommandValues {
       arity = "1")
   String rpcHttpAuthenticationCredentialsFile = null;
 
+  @CommandLine.Option(
+      names = {"--rpc-http-authentication-jwt-public-key-file"},
+      paramLabel = MANDATORY_FILE_FORMAT_HELP,
+      description = "JWT public key file for JSON-RPC HTTP authentication",
+      arity = "1")
+  final File rpcHttpAuthenticationPublicKeyFile = null;
+
   @CommandLine.Option(
       names = {"--rpc-ws-authentication-credentials-file"},
       paramLabel = MANDATORY_FILE_FORMAT_HELP,
@@ -73,6 +80,13 @@ class StandaloneCommand implements DefaultCommandValues {
       arity = "1")
   String rpcWsAuthenticationCredentialsFile = null;
 
+  @CommandLine.Option(
+      names = {"--rpc-ws-authentication-jwt-public-key-file"},
+      paramLabel = MANDATORY_FILE_FORMAT_HELP,
+      description = "JWT public key file for JSON-RPC WebSocket authentication",
+      arity = "1")
+  final File rpcWsAuthenticationPublicKeyFile = null;
+
   @CommandLine.Option(
       names = {"--privacy-public-key-file"},
       description = "The enclave's public key file")
diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
index bd627dd82ad..06fb6c0d9b5 100644
--- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
+++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.isNotNull;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 import org.hyperledger.besu.BesuInfo;
@@ -2771,6 +2772,32 @@ public void rpcWsAuthCredentialsFileOptionDisabledUnderDocker() {
     assertThat(commandOutput.toString()).isEmpty();
   }
 
+  @Test
+  public void rpcHttpAuthPublicKeyFileOptionDisabledUnderDocker() {
+    System.setProperty("besu.docker", "true");
+
+    assumeFalse(isFullInstantiation());
+
+    final Path path = Paths.get(".");
+    parseCommand("--rpc-http-authentication-public-key-file", path.toString());
+    assertThat(commandErrorOutput.toString())
+        .startsWith("Unknown options: --rpc-http-authentication-public-key-file, .");
+    assertThat(commandOutput.toString()).isEmpty();
+  }
+
+  @Test
+  public void rpcWsAuthPublicKeyFileOptionDisabledUnderDocker() {
+    System.setProperty("besu.docker", "true");
+
+    assumeFalse(isFullInstantiation());
+
+    final Path path = Paths.get(".");
+    parseCommand("--rpc-ws-authentication-public-key-file", path.toString());
+    assertThat(commandErrorOutput.toString())
+        .startsWith("Unknown options: --rpc-ws-authentication-public-key-file, .");
+    assertThat(commandOutput.toString()).isEmpty();
+  }
+
   @Test
   public void permissionsConfigFileOptionDisabledUnderDocker() {
     System.setProperty("besu.docker", "true");
@@ -2995,4 +3022,50 @@ public void requiredBlocksMulpleBlocksTwoArgs() {
     assertThat(requiredBlocksArg.getValue())
         .containsEntry(block2, Hash.fromHexStringLenient(hash2));
   }
+
+  @Test
+  public void httpAuthenticationPublicKeyIsConfigured() throws IOException {
+    final Path publicKey = Files.createTempFile("public_key", "");
+    parseCommand("--rpc-http-authentication-jwt-public-key-file", publicKey.toString());
+
+    verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
+    verify(mockRunnerBuilder).build();
+
+    assertThat(jsonRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath())
+        .isEqualTo(publicKey.toString());
+  }
+
+  @Test
+  public void httpAuthenticationWithoutRequiredConfiguredOptionsMustFail() {
+    parseCommand("--rpc-http-enabled", "--rpc-http-authentication-enabled");
+
+    verifyNoInteractions(mockRunnerBuilder);
+    assertThat(commandOutput.toString()).isEmpty();
+    assertThat(commandErrorOutput.toString())
+        .contains(
+            "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file");
+  }
+
+  @Test
+  public void wsAuthenticationPublicKeyIsConfigured() throws IOException {
+    final Path publicKey = Files.createTempFile("public_key", "");
+    parseCommand("--rpc-ws-authentication-jwt-public-key-file", publicKey.toString());
+
+    verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
+    verify(mockRunnerBuilder).build();
+
+    assertThat(wsRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath())
+        .isEqualTo(publicKey.toString());
+  }
+
+  @Test
+  public void wsAuthenticationWithoutRequiredConfiguredOptionsMustFail() {
+    parseCommand("--rpc-ws-enabled", "--rpc-ws-authentication-enabled");
+
+    verifyNoInteractions(mockRunnerBuilder);
+    assertThat(commandOutput.toString()).isEmpty();
+    assertThat(commandErrorOutput.toString())
+        .contains(
+            "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file");
+  }
 }
diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml
index 602b286651d..1f26adb0022 100644
--- a/besu/src/test/resources/everything_config.toml
+++ b/besu/src/test/resources/everything_config.toml
@@ -50,6 +50,7 @@ rpc-http-apis=["DEBUG","ETH"]
 rpc-http-cors-origins=["none"]
 rpc-http-authentication-enabled=false
 rpc-http-authentication-credentials-file="none"
+rpc-http-authentication-jwt-public-key-file="none"
 
 # GRAPHQL HTTP
 graphql-http-enabled=false
@@ -65,6 +66,7 @@ rpc-ws-host="9.10.11.12"
 rpc-ws-port=9101
 rpc-ws-authentication-enabled=false
 rpc-ws-authentication-credentials-file="none"
+rpc-ws-authentication-jwt-public-key-file="none"
 
 # Prometheus Metrics Endpoint
 metrics-enabled=false
diff --git a/ethereum/api/build.gradle b/ethereum/api/build.gradle
index a36486030ee..bd4f690f770 100644
--- a/ethereum/api/build.gradle
+++ b/ethereum/api/build.gradle
@@ -50,6 +50,7 @@ dependencies {
   implementation 'io.vertx:vertx-web'
   implementation 'io.vertx:vertx-auth-jwt'
   implementation 'io.vertx:vertx-unit'
+  implementation 'org.bouncycastle:bcprov-jdk15on'
 
   testImplementation 'com.squareup.okhttp3:okhttp'
   testImplementation 'junit:junit'
diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java
index 7f15c6b8037..4b1fd0c1fdb 100644
--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java
@@ -14,6 +14,7 @@
  */
 package org.hyperledger.besu.ethereum.api.jsonrpc;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -35,6 +36,7 @@ public class JsonRpcConfiguration {
   private List<String> hostsWhitelist = Arrays.asList("localhost", "127.0.0.1");
   private boolean authenticationEnabled = false;
   private String authenticationCredentialsFile;
+  private File authenticationPublicKeyFile;
 
   public static JsonRpcConfiguration createDefault() {
     final JsonRpcConfiguration config = new JsonRpcConfiguration();
@@ -102,6 +104,30 @@ public void setHostsWhitelist(final List<String> hostsWhitelist) {
     this.hostsWhitelist = hostsWhitelist;
   }
 
+  public boolean isAuthenticationEnabled() {
+    return authenticationEnabled;
+  }
+
+  public void setAuthenticationEnabled(final boolean authenticationEnabled) {
+    this.authenticationEnabled = authenticationEnabled;
+  }
+
+  public void setAuthenticationCredentialsFile(final String authenticationCredentialsFile) {
+    this.authenticationCredentialsFile = authenticationCredentialsFile;
+  }
+
+  public String getAuthenticationCredentialsFile() {
+    return authenticationCredentialsFile;
+  }
+
+  public File getAuthenticationPublicKeyFile() {
+    return authenticationPublicKeyFile;
+  }
+
+  public void setAuthenticationPublicKeyFile(final File authenticationPublicKeyFile) {
+    this.authenticationPublicKeyFile = authenticationPublicKeyFile;
+  }
+
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
@@ -113,6 +139,7 @@ public String toString() {
         .add("rpcApis", rpcApis)
         .add("authenticationEnabled", authenticationEnabled)
         .add("authenticationCredentialsFile", authenticationCredentialsFile)
+        .add("authenticationPublicKeyFile", authenticationPublicKeyFile)
         .toString();
   }
 
@@ -127,30 +154,26 @@ public boolean equals(final Object o) {
     final JsonRpcConfiguration that = (JsonRpcConfiguration) o;
     return enabled == that.enabled
         && port == that.port
+        && authenticationEnabled == that.authenticationEnabled
         && Objects.equals(host, that.host)
         && Objects.equals(corsAllowedDomains, that.corsAllowedDomains)
+        && Objects.equals(rpcApis, that.rpcApis)
         && Objects.equals(hostsWhitelist, that.hostsWhitelist)
-        && Objects.equals(rpcApis, that.rpcApis);
+        && Objects.equals(authenticationCredentialsFile, that.authenticationCredentialsFile)
+        && Objects.equals(authenticationPublicKeyFile, that.authenticationPublicKeyFile);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(enabled, port, host, corsAllowedDomains, hostsWhitelist, rpcApis);
-  }
-
-  public boolean isAuthenticationEnabled() {
-    return authenticationEnabled;
-  }
-
-  public void setAuthenticationEnabled(final boolean authenticationEnabled) {
-    this.authenticationEnabled = authenticationEnabled;
-  }
-
-  public void setAuthenticationCredentialsFile(final String authenticationCredentialsFile) {
-    this.authenticationCredentialsFile = authenticationCredentialsFile;
-  }
-
-  public String getAuthenticationCredentialsFile() {
-    return authenticationCredentialsFile;
+    return Objects.hash(
+        enabled,
+        port,
+        host,
+        corsAllowedDomains,
+        rpcApis,
+        hostsWhitelist,
+        authenticationEnabled,
+        authenticationCredentialsFile,
+        authenticationPublicKeyFile);
   }
 }
diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java
index 913f3f7a7a6..7e132b8e58e 100644
--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java
@@ -17,10 +17,7 @@
 import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
 import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
 
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.util.Base64;
+import java.io.File;
 import java.util.Optional;
 import javax.annotation.Nullable;
 
@@ -30,7 +27,6 @@
 import io.vertx.core.http.HttpServerResponse;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.auth.AuthProvider;
-import io.vertx.ext.auth.PubSecKeyOptions;
 import io.vertx.ext.auth.User;
 import io.vertx.ext.auth.jwt.JWTAuth;
 import io.vertx.ext.auth.jwt.JWTAuthOptions;
@@ -42,12 +38,13 @@ public class AuthenticationService {
 
   private final JWTAuth jwtAuthProvider;
   @VisibleForTesting public final JWTAuthOptions jwtAuthOptions;
-  private final AuthProvider credentialAuthProvider;
+  private final Optional<AuthProvider> credentialAuthProvider;
+  private static final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
 
   private AuthenticationService(
       final JWTAuth jwtAuthProvider,
       final JWTAuthOptions jwtAuthOptions,
-      final AuthProvider credentialAuthProvider) {
+      final Optional<AuthProvider> credentialAuthProvider) {
     this.jwtAuthProvider = jwtAuthProvider;
     this.jwtAuthOptions = jwtAuthOptions;
     this.credentialAuthProvider = credentialAuthProvider;
@@ -65,25 +62,11 @@ private AuthenticationService(
    */
   public static Optional<AuthenticationService> create(
       final Vertx vertx, final JsonRpcConfiguration config) {
-    final Optional<JWTAuthOptions> jwtAuthOptions =
-        makeJwtAuthOptions(
-            config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile());
-    if (!jwtAuthOptions.isPresent()) {
-      return Optional.empty();
-    }
-
-    final Optional<AuthProvider> credentialAuthProvider =
-        makeCredentialAuthProvider(
-            vertx, config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile());
-    if (!credentialAuthProvider.isPresent()) {
-      return Optional.empty();
-    }
-
-    return Optional.of(
-        new AuthenticationService(
-            jwtAuthOptions.map(o -> JWTAuth.create(vertx, o)).get(),
-            jwtAuthOptions.get(),
-            credentialAuthProvider.get()));
+    return create(
+        vertx,
+        config.isAuthenticationEnabled(),
+        config.getAuthenticationCredentialsFile(),
+        config.getAuthenticationPublicKeyFile());
   }
 
   /**
@@ -98,55 +81,35 @@ public static Optional<AuthenticationService> create(
    */
   public static Optional<AuthenticationService> create(
       final Vertx vertx, final WebSocketConfiguration config) {
-    final Optional<JWTAuthOptions> jwtAuthOptions =
-        makeJwtAuthOptions(
-            config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile());
-    if (!jwtAuthOptions.isPresent()) {
+    return create(
+        vertx,
+        config.isAuthenticationEnabled(),
+        config.getAuthenticationCredentialsFile(),
+        config.getAuthenticationPublicKeyFile());
+  }
+
+  private static Optional<AuthenticationService> create(
+      final Vertx vertx,
+      final boolean authenticationEnabled,
+      final String authenticationCredentialsFile,
+      final File authenticationPublicKeyFile) {
+    if (!authenticationEnabled
+        && authenticationCredentialsFile == null
+        && authenticationPublicKeyFile == null) {
       return Optional.empty();
     }
 
+    final JWTAuthOptions jwtAuthOptions =
+        authenticationPublicKeyFile == null
+            ? jwtAuthOptionsFactory.createWithGeneratedKeyPair()
+            : jwtAuthOptionsFactory.createForExternalPublicKey(authenticationPublicKeyFile);
+
     final Optional<AuthProvider> credentialAuthProvider =
-        makeCredentialAuthProvider(
-            vertx, config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile());
-    if (!credentialAuthProvider.isPresent()) {
-      return Optional.empty();
-    }
+        makeCredentialAuthProvider(vertx, authenticationEnabled, authenticationCredentialsFile);
 
     return Optional.of(
         new AuthenticationService(
-            jwtAuthOptions.map(o -> JWTAuth.create(vertx, o)).get(),
-            jwtAuthOptions.get(),
-            credentialAuthProvider.get()));
-  }
-
-  private static Optional<JWTAuthOptions> makeJwtAuthOptions(
-      final boolean authenticationEnabled, @Nullable final String authenticationCredentialsFile) {
-    if (authenticationEnabled && authenticationCredentialsFile != null) {
-      final KeyPairGenerator keyGenerator;
-      try {
-        keyGenerator = KeyPairGenerator.getInstance("RSA");
-        keyGenerator.initialize(1024);
-      } catch (final NoSuchAlgorithmException e) {
-        throw new RuntimeException(e);
-      }
-
-      final KeyPair keypair = keyGenerator.generateKeyPair();
-
-      final JWTAuthOptions jwtAuthOptions =
-          new JWTAuthOptions()
-              .setPermissionsClaimKey("permissions")
-              .addPubSecKey(
-                  new PubSecKeyOptions()
-                      .setAlgorithm("RS256")
-                      .setPublicKey(
-                          Base64.getEncoder().encodeToString(keypair.getPublic().getEncoded()))
-                      .setSecretKey(
-                          Base64.getEncoder().encodeToString(keypair.getPrivate().getEncoded())));
-
-      return Optional.of(jwtAuthOptions);
-    } else {
-      return Optional.empty();
-    }
+            JWTAuth.create(vertx, jwtAuthOptions), jwtAuthOptions, credentialAuthProvider));
   }
 
   private static Optional<AuthProvider> makeCredentialAuthProvider(
@@ -181,6 +144,15 @@ public static void handleDisabledLogin(final RoutingContext routingContext) {
    * @param routingContext Routing context associated with this request
    */
   public void handleLogin(final RoutingContext routingContext) {
+    if (credentialAuthProvider.isPresent()) {
+      login(routingContext, credentialAuthProvider.get());
+    } else {
+      handleDisabledLogin(routingContext);
+    }
+  }
+
+  private void login(
+      final RoutingContext routingContext, final AuthProvider credentialAuthProvider) {
     final JsonObject requestBody = routingContext.getBodyAsJson();
 
     if (requestBody == null) {
diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
index 16a486c6a70..0238b69a3f9 100644
--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
@@ -71,7 +71,7 @@ public static void getUser(
       final String token,
       final Handler<Optional<User>> handler) {
     try {
-      if (!authenticationService.isPresent()) {
+      if (authenticationService.isEmpty()) {
         handler.handle(Optional.empty());
       } else {
         authenticationService
@@ -80,8 +80,14 @@ public static void getUser(
             .authenticate(
                 new JsonObject().put("jwt", token),
                 (r) -> {
-                  final User user = r.result();
-                  handler.handle(Optional.of(user));
+                  if (r.succeeded()) {
+                    final Optional<User> user = Optional.ofNullable(r.result());
+                    validateExpExists(user);
+                    handler.handle(user);
+                  } else {
+                    LOG.debug("Invalid JWT token", r.cause());
+                    handler.handle(Optional.empty());
+                  }
                 });
       }
     } catch (Exception e) {
@@ -89,6 +95,12 @@ public static void getUser(
     }
   }
 
+  private static void validateExpExists(final Optional<User> user) {
+    if (!user.map(User::principal).map(p -> p.containsKey("exp")).orElse(false)) {
+      throw new IllegalStateException("Invalid JWT doesn't have expiry");
+    }
+  }
+
   public static String getJwtTokenFromAuthorizationHeaderValue(final String value) {
     if (value != null) {
       final String bearerSchemaName = "Bearer ";
diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java
new file mode 100644
index 00000000000..2be5695dc88
--- /dev/null
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+import io.vertx.ext.auth.PubSecKeyOptions;
+import io.vertx.ext.auth.jwt.JWTAuthOptions;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
+
+public class JWTAuthOptionsFactory {
+
+  private static final String ALGORITHM = "RS256";
+  private static final String PERMISSIONS = "permissions";
+
+  public JWTAuthOptions createForExternalPublicKey(final File externalPublicKeyFile) {
+    final byte[] externalJwtPublicKey = readPublicKey(externalPublicKeyFile);
+    final String base64EncodedPublicKey = Base64.getEncoder().encodeToString(externalJwtPublicKey);
+    return new JWTAuthOptions()
+        .setPermissionsClaimKey(PERMISSIONS)
+        .addPubSecKey(
+            new PubSecKeyOptions().setAlgorithm(ALGORITHM).setPublicKey(base64EncodedPublicKey));
+  }
+
+  public JWTAuthOptions createWithGeneratedKeyPair() {
+    final KeyPair keypair = generateJwtKeyPair();
+    return new JWTAuthOptions()
+        .setPermissionsClaimKey(PERMISSIONS)
+        .addPubSecKey(
+            new PubSecKeyOptions()
+                .setAlgorithm(ALGORITHM)
+                .setPublicKey(Base64.getEncoder().encodeToString(keypair.getPublic().getEncoded()))
+                .setSecretKey(
+                    Base64.getEncoder().encodeToString(keypair.getPrivate().getEncoded())));
+  }
+
+  private byte[] readPublicKey(final File publicKeyFile) {
+    try (final BufferedReader reader = Files.newBufferedReader(publicKeyFile.toPath(), UTF_8);
+        final PemReader pemReader = new PemReader(reader)) {
+      final PemObject pemObject = pemReader.readPemObject();
+      if (pemObject == null) {
+        throw new IllegalStateException("Authentication RPC public key file format is invalid");
+      }
+      return pemObject.getContent();
+    } catch (IOException e) {
+      throw new IllegalStateException("Authentication RPC public key could not be read", e);
+    }
+  }
+
+  private KeyPair generateJwtKeyPair() {
+    final KeyPairGenerator keyGenerator;
+    try {
+      keyGenerator = KeyPairGenerator.getInstance("RSA");
+      keyGenerator.initialize(2048);
+    } catch (final NoSuchAlgorithmException e) {
+      throw new RuntimeException(e);
+    }
+
+    return keyGenerator.generateKeyPair();
+  }
+}
diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java
index 3ff98c99b09..6bfdd4b3da0 100644
--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java
@@ -17,6 +17,7 @@
 import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi;
 import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
 
+import java.io.File;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -37,7 +38,8 @@ public class WebSocketConfiguration {
   private List<RpcApi> rpcApis;
   private boolean authenticationEnabled = false;
   private String authenticationCredentialsFile;
-  private Collection<String> hostsWhitelist = Collections.singletonList("localhost");
+  private List<String> hostsWhitelist = Arrays.asList("localhost", "127.0.0.1");
+  private File authenticationPublicKeyFile;
 
   public static WebSocketConfiguration createDefault() {
     final WebSocketConfiguration config = new WebSocketConfiguration();
@@ -82,38 +84,6 @@ public void setRpcApis(final List<RpcApi> rpcApis) {
     this.rpcApis = rpcApis;
   }
 
-  @Override
-  public String toString() {
-    return MoreObjects.toStringHelper(this)
-        .add("enabled", enabled)
-        .add("port", port)
-        .add("host", host)
-        .add("rpcApis", rpcApis)
-        .add("authenticationEnabled", authenticationEnabled)
-        .add("authenticationCredentialsFile", authenticationCredentialsFile)
-        .toString();
-  }
-
-  @Override
-  public boolean equals(final Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    final WebSocketConfiguration that = (WebSocketConfiguration) o;
-    return enabled == that.enabled
-        && port == that.port
-        && Objects.equals(host, that.host)
-        && Objects.equals(rpcApis, that.rpcApis);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(enabled, port, host, rpcApis);
-  }
-
   public boolean isAuthenticationEnabled() {
     return authenticationEnabled;
   }
@@ -130,11 +100,65 @@ public String getAuthenticationCredentialsFile() {
     return authenticationCredentialsFile;
   }
 
-  public void setHostsWhitelist(final Collection<String> hostsWhitelist) {
+  public void setHostsWhitelist(final List<String> hostsWhitelist) {
     this.hostsWhitelist = hostsWhitelist;
   }
 
   public Collection<String> getHostsWhitelist() {
     return Collections.unmodifiableCollection(this.hostsWhitelist);
   }
+
+  public File getAuthenticationPublicKeyFile() {
+    return authenticationPublicKeyFile;
+  }
+
+  public void setAuthenticationPublicKeyFile(final File authenticationPublicKeyFile) {
+    this.authenticationPublicKeyFile = authenticationPublicKeyFile;
+  }
+
+  @Override
+  public boolean equals(final Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    final WebSocketConfiguration that = (WebSocketConfiguration) o;
+    return enabled == that.enabled
+        && port == that.port
+        && authenticationEnabled == that.authenticationEnabled
+        && Objects.equals(host, that.host)
+        && Objects.equals(rpcApis, that.rpcApis)
+        && Objects.equals(authenticationCredentialsFile, that.authenticationCredentialsFile)
+        && Objects.equals(hostsWhitelist, that.hostsWhitelist)
+        && Objects.equals(authenticationPublicKeyFile, that.authenticationPublicKeyFile);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        enabled,
+        port,
+        host,
+        rpcApis,
+        authenticationEnabled,
+        authenticationCredentialsFile,
+        hostsWhitelist,
+        authenticationPublicKeyFile);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("enabled", enabled)
+        .add("port", port)
+        .add("host", host)
+        .add("rpcApis", rpcApis)
+        .add("authenticationEnabled", authenticationEnabled)
+        .add("authenticationCredentialsFile", authenticationCredentialsFile)
+        .add("hostsWhitelist", hostsWhitelist)
+        .add("authenticationPublicKeyFile", authenticationPublicKeyFile)
+        .toString();
+  }
 }
diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java
index 74b387f8782..b33c2d7cdde 100644
--- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java
+++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java
@@ -15,11 +15,30 @@
 package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import java.util.Optional;
+
+import io.vertx.core.Handler;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.User;
+import io.vertx.ext.auth.jwt.JWTAuth;
+import io.vertx.ext.auth.jwt.JWTAuthOptions;
+import io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl;
 import org.junit.Test;
 
 public class AuthenticationUtilsTest {
 
+  private static String INVALID_TOKEN_WITHOUT_EXP =
+      "ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn"
+          + "0.eyJpYXQiOjE1MTYyMzkwMjIsInBlcm1pc3Npb25zIjpbIm5ldDpwZWVyQ291bnQiXX0";
+  private static String VALID_TOKEN =
+      "ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0.eyJpYXQiOjE1"
+          + "MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWlzc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ";
+  private static String VALID_TOKEN_DECODED_PAYLOAD =
+      "{\"iat\": 1516239022,\"exp\": 4729363200," + "\"permissions\": [\"net:peerCount\"]}";
+
   @Test
   public void getJwtTokenFromNullStringShouldReturnNull() {
     final String headerValue = null;
@@ -55,4 +74,43 @@ public void getJwtTokenFromValidAuthorizationHeaderValueShouldReturnToken() {
 
     assertThat(token).isEqualTo("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9");
   }
+
+  @Test
+  public void getUserFailsIfTokenDoesNotHaveExpiryClaim() {
+    final AuthenticationService authenticationService = mock(AuthenticationService.class);
+    final JWTAuth jwtAuth = new JWTAuthProviderImpl(null, new JWTAuthOptions());
+    final StubUserHandler handler = new StubUserHandler();
+    when(authenticationService.getJwtAuthProvider()).thenReturn(jwtAuth);
+
+    AuthenticationUtils.getUser(
+        Optional.of(authenticationService), INVALID_TOKEN_WITHOUT_EXP, handler);
+
+    assertThat(handler.getEvent()).isEmpty();
+  }
+
+  @Test
+  public void getUserSucceedsWithValidToken() {
+    final AuthenticationService authenticationService = mock(AuthenticationService.class);
+    final JWTAuth jwtAuth = new JWTAuthProviderImpl(null, new JWTAuthOptions());
+    final StubUserHandler handler = new StubUserHandler();
+    when(authenticationService.getJwtAuthProvider()).thenReturn(jwtAuth);
+
+    AuthenticationUtils.getUser(Optional.of(authenticationService), VALID_TOKEN, handler);
+
+    assertThat(handler.getEvent().get().principal())
+        .isEqualTo(new JsonObject(VALID_TOKEN_DECODED_PAYLOAD));
+  }
+
+  private static class StubUserHandler implements Handler<Optional<User>> {
+    private Optional<User> event;
+
+    @Override
+    public void handle(final Optional<User> event) {
+      this.event = event;
+    }
+
+    public Optional<User> getEvent() {
+      return event;
+    }
+  }
 }
diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java
new file mode 100644
index 00000000000..77d9406938f
--- /dev/null
+++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import io.vertx.ext.auth.PubSecKeyOptions;
+import io.vertx.ext.auth.jwt.JWTAuthOptions;
+import org.junit.Test;
+
+public class JWTAuthOptionsFactoryTest {
+
+  private static final String JWT_PUBLIC_KEY =
+      "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6tMhjogMulRbYby7bCL"
+          + "rFhukDnxvm4XR3KSXLKdLLQHHyouMOQaLac9M+/Z1KkIpqfZPjLfW2/yUg2IKx4T"
+          + "dvFVzbVq17X6dq49ZS8jJtb8l2+Vius4d3LnpvxCOematRG9Acn+2qLwC+sK7RPY"
+          + "OxEqKPU5LNBH1C0FfviazY5jkixBFICzIq/SyyRnGX+iIONnNsu0TlhWVLSlZbg5"
+          + "NYf4cAzu/1d5MgspyZwnRo468gqaak3wQzkmk69Z25L1N7TXZvk2b7rT7/ssFnt+"
+          + "//fKVpD6qkQ3OopD+7gOziAYUxChw6RUWekV+uRgNADQhaqV6wDdogBz77wTJedV"
+          + "YwIDAQAB";
+
+  @Test
+  public void createsOptionsWithGeneratedKeyPair() {
+    final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
+    final JWTAuthOptions jwtAuthOptions = jwtAuthOptionsFactory.createWithGeneratedKeyPair();
+
+    assertThat(jwtAuthOptions.getPubSecKeys()).isNotNull();
+    assertThat(jwtAuthOptions.getPubSecKeys()).hasSize(1);
+    assertThat(jwtAuthOptions.getPubSecKeys().get(0).getAlgorithm()).isEqualTo("RS256");
+    assertThat(jwtAuthOptions.getPubSecKeys().get(0).getPublicKey()).isNotEmpty();
+    assertThat(jwtAuthOptions.getPubSecKeys().get(0).getSecretKey()).isNotEmpty();
+  }
+
+  @Test
+  public void createsOptionsWithGeneratedKeyPairThatIsDifferentEachTime() {
+    final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
+    final JWTAuthOptions jwtAuthOptions1 = jwtAuthOptionsFactory.createWithGeneratedKeyPair();
+    final JWTAuthOptions jwtAuthOptions2 = jwtAuthOptionsFactory.createWithGeneratedKeyPair();
+
+    final PubSecKeyOptions pubSecKeyOptions1 = jwtAuthOptions1.getPubSecKeys().get(0);
+    final PubSecKeyOptions pubSecKeyOptions2 = jwtAuthOptions2.getPubSecKeys().get(0);
+    assertThat(pubSecKeyOptions1.getPublicKey()).isNotEqualTo(pubSecKeyOptions2.getPublicKey());
+    assertThat(pubSecKeyOptions1.getSecretKey()).isNotEqualTo(pubSecKeyOptions2.getSecretKey());
+  }
+
+  @Test
+  public void createsOptionsUsingPublicKeyFile() throws URISyntaxException {
+    final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
+    final File enclavePublicKeyFile =
+        Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key").toURI())
+            .toAbsolutePath()
+            .toFile();
+
+    final JWTAuthOptions jwtAuthOptions =
+        jwtAuthOptionsFactory.createForExternalPublicKey(enclavePublicKeyFile);
+    assertThat(jwtAuthOptions.getPubSecKeys()).hasSize(1);
+    assertThat(jwtAuthOptions.getPubSecKeys().get(0).getAlgorithm()).isEqualTo("RS256");
+    assertThat(jwtAuthOptions.getPubSecKeys().get(0).getSecretKey()).isNull();
+    assertThat(jwtAuthOptions.getPubSecKeys().get(0).getPublicKey()).isEqualTo(JWT_PUBLIC_KEY);
+  }
+
+  @Test
+  public void failsToCreateOptionsWhenPublicKeyFileDoesNotExist() {
+    final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
+    final File enclavePublicKeyFile = new File("doesNotExist");
+
+    assertThatThrownBy(() -> jwtAuthOptionsFactory.createForExternalPublicKey(enclavePublicKeyFile))
+        .isInstanceOf(IllegalStateException.class)
+        .hasMessage("Authentication RPC public key could not be read");
+  }
+
+  @Test
+  public void failsToCreateOptionsWhenPublicKeyFileIsInvalid() throws IOException {
+    final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
+    final Path enclavePublicKey = Files.createTempFile("enclave", "pub");
+    Files.writeString(enclavePublicKey, "invalidDataNo---HeadersAndNotBase64");
+
+    assertThatThrownBy(
+            () -> jwtAuthOptionsFactory.createForExternalPublicKey(enclavePublicKey.toFile()))
+        .isInstanceOf(IllegalStateException.class)
+        .hasMessage("Authentication RPC public key file format is invalid");
+  }
+}
diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java
index 4d98cbec074..03ad8b58d62 100644
--- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java
+++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java
@@ -75,7 +75,7 @@ public void before() throws URISyntaxException {
     websocketConfiguration.setPort(0);
     websocketConfiguration.setAuthenticationEnabled(true);
     websocketConfiguration.setAuthenticationCredentialsFile(authTomlPath);
-    websocketConfiguration.setHostsWhitelist(Collections.singleton("*"));
+    websocketConfiguration.setHostsWhitelist(Collections.singletonList("*"));
 
     final Map<String, JsonRpcMethod> websocketMethods =
         new WebSocketMethodsFactory(
diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java
index 8f62a42457f..c54c2f59e98 100644
--- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java
+++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java
@@ -60,7 +60,7 @@ public void before() {
 
     websocketConfiguration = WebSocketConfiguration.createDefault();
     websocketConfiguration.setPort(0);
-    websocketConfiguration.setHostsWhitelist(Collections.singleton("*"));
+    websocketConfiguration.setHostsWhitelist(Collections.singletonList("*"));
 
     final Map<String, JsonRpcMethod> websocketMethods =
         new WebSocketMethodsFactory(
diff --git a/ethereum/api/src/test/resources/authentication/jwt_public_key b/ethereum/api/src/test/resources/authentication/jwt_public_key
new file mode 100644
index 00000000000..9107cf29702
--- /dev/null
+++ b/ethereum/api/src/test/resources/authentication/jwt_public_key
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6tMhjogMulRbYby7bCL
+rFhukDnxvm4XR3KSXLKdLLQHHyouMOQaLac9M+/Z1KkIpqfZPjLfW2/yUg2IKx4T
+dvFVzbVq17X6dq49ZS8jJtb8l2+Vius4d3LnpvxCOematRG9Acn+2qLwC+sK7RPY
+OxEqKPU5LNBH1C0FfviazY5jkixBFICzIq/SyyRnGX+iIONnNsu0TlhWVLSlZbg5
+NYf4cAzu/1d5MgspyZwnRo468gqaak3wQzkmk69Z25L1N7TXZvk2b7rT7/ssFnt+
+//fKVpD6qkQ3OopD+7gOziAYUxChw6RUWekV+uRgNADQhaqV6wDdogBz77wTJedV
+YwIDAQAB
+-----END PUBLIC KEY-----