3465 jwt auth (#3508)

* refactored auth to allow variant JWT rules to be adopted by different stacks
* configures JWT secret from a file, or creates a temporary one
* jwt auth is optional for engine apis
* test coverage of new cli options

Signed-off-by: Justin Florentine <justin+github@florentine.us>
pull/3433/head
Justin Florentine 3 years ago committed by GitHub
parent 9cabf6f528
commit 6947a6c34d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 2
      besu/src/main/java/org/hyperledger/besu/Runner.java
  3. 40
      besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
  4. 43
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  5. 1
      besu/src/test/java/org/hyperledger/besu/RunnerBuilderTest.java
  6. 2
      besu/src/test/java/org/hyperledger/besu/RunnerTest.java
  7. 15
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  8. 2
      besu/src/test/resources/everything_config.toml
  9. 3
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java
  10. 71
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java
  11. 206
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java
  12. 94
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
  13. 292
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/DefaultAuthenticationService.java
  14. 165
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/EngineAuthService.java
  15. 41
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java
  16. 1
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JwtAlgorithm.java
  17. 26
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/UnsecurableEngineApiException.java
  18. 5
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandler.java
  19. 67
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java
  20. 100
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
  21. 75
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationServiceTest.java
  22. 58
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java
  23. 120
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/EngineAuthServiceTest.java
  24. 10
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java
  25. 1
      ethereum/api/src/test/resources/authentication/ee-jwt-secret-too-short.hex
  26. 1
      ethereum/api/src/test/resources/authentication/ee-jwt-secret.hex

@ -5,6 +5,7 @@
### Additions and Improvements
- Execution layer (The Merge):
- Execution specific RPC endpoint [[#3378](https://github.com/hyperledger/besu/issues/3378)
- Adds JWT authentication to Engine APIs
- Tracing APIs: trace_rawTransaction, trace_get, trace_callMany
### Bug Fixes

@ -144,6 +144,7 @@ public class Runner implements AutoCloseable {
writeBesuNetworksToFile();
writePidFile();
} catch (final Exception ex) {
LOG.error("unable to start main loop", ex);
throw new IllegalStateException("Startup failed", ex);
}
}
@ -358,7 +359,6 @@ public class Runner implements AutoCloseable {
final Properties properties, final String fileName, final String fileHeader) {
final File file = new File(dataDir.toFile(), String.format("besu.%s", fileName));
file.deleteOnExit();
try (final FileOutputStream fileOutputStream = new FileOutputStream(file)) {
properties.store(
fileOutputStream,

@ -34,6 +34,9 @@ import org.hyperledger.besu.ethereum.api.graphql.GraphQLHttpService;
import org.hyperledger.besu.ethereum.api.graphql.GraphQLProvider;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcHttpService;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.DefaultAuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.EngineAuthService;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.LivenessCheck;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.ReadinessCheck;
@ -610,7 +613,7 @@ public class RunnerBuilder {
new HealthService(new LivenessCheck()),
new HealthService(new ReadinessCheck(peerNetwork, synchronizer))));
if (engineJsonRpcConfiguration.isPresent()) {
if (engineJsonRpcConfiguration.isPresent() && engineJsonRpcConfiguration.get().isEnabled()) {
final Map<String, JsonRpcMethod> engineMethods =
jsonRpcMethods(
protocolSchedule,
@ -636,6 +639,15 @@ public class RunnerBuilder {
dataDir,
rpcEndpointServiceImpl);
Optional<AuthenticationService> authToUse =
engineJsonRpcConfiguration.get().isAuthenticationEnabled()
? Optional.of(
new EngineAuthService(
vertx,
Optional.ofNullable(
engineJsonRpcConfiguration.get().getAuthenticationPublicKeyFile()),
dataDir))
: Optional.empty();
engineJsonRpcHttpService =
Optional.of(
new JsonRpcHttpService(
@ -645,6 +657,7 @@ public class RunnerBuilder {
metricsSystem,
natService,
engineMethods,
authToUse,
new HealthService(new LivenessCheck()),
new HealthService(new ReadinessCheck(peerNetwork, synchronizer))));
}
@ -731,11 +744,13 @@ public class RunnerBuilder {
webSocketsJsonRpcMethods,
privacyParameters,
protocolSchedule,
blockchainQueries));
blockchainQueries,
DefaultAuthenticationService.create(vertx, webSocketConfiguration)));
createPrivateTransactionObserver(subscriptionManager, privacyParameters);
if (engineWebSocketConfiguration.isPresent()) {
if (engineWebSocketConfiguration.isPresent()
&& engineWebSocketConfiguration.get().isEnabled()) {
final Map<String, JsonRpcMethod> engineMethods =
jsonRpcMethods(
protocolSchedule,
@ -761,6 +776,16 @@ public class RunnerBuilder {
dataDir,
rpcEndpointServiceImpl);
Optional<AuthenticationService> authToUse =
engineWebSocketConfiguration.get().isAuthenticationEnabled()
? Optional.of(
new EngineAuthService(
vertx,
Optional.ofNullable(
engineWebSocketConfiguration.get().getAuthenticationPublicKeyFile()),
dataDir))
: Optional.empty();
engineWebSocketService =
Optional.of(
createWebsocketService(
@ -770,7 +795,8 @@ public class RunnerBuilder {
engineMethods,
privacyParameters,
protocolSchedule,
blockchainQueries));
blockchainQueries,
authToUse));
}
}
@ -1067,7 +1093,8 @@ public class RunnerBuilder {
final Map<String, JsonRpcMethod> jsonRpcMethods,
final PrivacyParameters privacyParameters,
final ProtocolSchedule protocolSchedule,
final BlockchainQueries blockchainQueries) {
final BlockchainQueries blockchainQueries,
final Optional<AuthenticationService> authenticationService) {
final WebSocketMethodsFactory websocketMethodsFactory =
new WebSocketMethodsFactory(subscriptionManager, jsonRpcMethods);
@ -1092,7 +1119,8 @@ public class RunnerBuilder {
besuController.getProtocolManager().ethContext().getScheduler(),
webSocketConfiguration.getTimeoutSec());
return new WebSocketService(vertx, configuration, websocketRequestHandler);
return new WebSocketService(
vertx, configuration, websocketRequestHandler, authenticationService);
}
private Optional<MetricsService> createMetricsService(

@ -707,6 +707,17 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
arity = "1")
private final Integer engineRpcWsPort = DEFAULT_WEBSOCKET_ENGINE_PORT;
@Option(
names = {"--engine-jwt-secret"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description = "Path to file containing shared secret key for JWT signature verification")
private final Path engineJwtKeyFile = null;
@Option(
names = {"--engine-jwt-enabled"},
description = "Require authentication for Engine APIs (default: ${DEFAULT-VALUE})")
private final Boolean isEngineAuthEnabled = false;
@Option(
names = {"--rpc-ws-max-frame-size"},
description =
@ -869,7 +880,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
names = {"--engine-host-allowlist"},
paramLabel = "<hostname>[,<hostname>...]... or * or all",
description =
"Comma separated list of hostnames to allow for ENGINE API access, or * to accept any host (default: ${DEFAULT-VALUE})",
"Comma separated list of hostnames to allow for ENGINE API access (applies to both HTTP and websockets), or * to accept any host (default: ${DEFAULT-VALUE})",
defaultValue = "localhost,127.0.0.1")
private final JsonRPCAllowlistHostsProperty engineHostsAllowlist =
new JsonRPCAllowlistHostsProperty();
@ -1816,8 +1827,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
graphQLConfiguration = graphQLConfiguration();
webSocketConfiguration = webSocketConfiguration(rpcWsPort, rpcWsApis, hostsAllowlist);
engineWebSocketConfiguration =
webSocketConfiguration(
engineRpcWsPort, Arrays.asList("ENGINE", "ETH"), engineHostsAllowlist);
engineWebSocketConfiguration(engineRpcWsPort, engineHostsAllowlist);
apiConfiguration = apiConfiguration();
// hostsWhitelist is a hidden option. If it is specified, add the list to hostAllowlist
if (!hostsWhitelist.isEmpty()) {
@ -1982,9 +1992,36 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
final Integer listenPort, final List<String> allowCallsFrom) {
JsonRpcConfiguration engineConfig =
jsonRpcConfiguration(listenPort, Arrays.asList("ENGINE", "ETH"), allowCallsFrom);
engineConfig.setEnabled(isMergeEnabled());
if (isEngineAuthEnabled) {
engineConfig.setAuthenticationEnabled(true);
engineConfig.setAuthenticationAlgorithm(JwtAlgorithm.HS256);
if (engineJwtKeyFile != null && java.nio.file.Files.exists(engineJwtKeyFile)) { // NOSONAR
engineConfig.setAuthenticationPublicKeyFile(engineJwtKeyFile.toFile());
} else {
logger.info(
"Engine API authentication enabled without key file. Expect ephemeral jwt.hex dile in datadir");
}
}
return engineConfig;
}
private WebSocketConfiguration engineWebSocketConfiguration(
final Integer listenPort, final List<String> allowCallsFrom) {
final WebSocketConfiguration webSocketConfiguration =
webSocketConfiguration(listenPort, Arrays.asList("ENGINE", "ETH"), allowCallsFrom);
webSocketConfiguration.setEnabled(isMergeEnabled());
if (isEngineAuthEnabled) {
webSocketConfiguration.setAuthenticationEnabled(true);
webSocketConfiguration.setAuthenticationAlgorithm(JwtAlgorithm.HS256);
if (engineJwtKeyFile != null && java.nio.file.Files.exists(engineJwtKeyFile)) { // NOSONAR
webSocketConfiguration.setAuthenticationPublicKeyFile(engineJwtKeyFile.toFile());
}
}
return webSocketConfiguration;
}
private JsonRpcConfiguration jsonRpcConfiguration(
final Integer listenPort, final List<String> apiGroups, final List<String> allowCallsFrom) {
checkRpcTlsClientAuthOptionsDependencies();

@ -219,6 +219,7 @@ public final class RunnerBuilderTest {
JsonRpcConfiguration jrpc = JsonRpcConfiguration.createDefault();
jrpc.setEnabled(true);
JsonRpcConfiguration engine = JsonRpcConfiguration.createEngineDefault();
engine.setEnabled(true);
EthNetworkConfig mockMainnet = mock(EthNetworkConfig.class);
when(mockMainnet.getNetworkId()).thenReturn(BigInteger.ONE);
MergeConfigOptions.setMergeEnabled(true);

@ -235,7 +235,7 @@ public final class RunnerTest {
try {
runnerAhead.startExternalServices();
runnerAhead.startEthereumMainLoop();
assertThat(pidPath.toFile().exists()).isTrue();
assertThat(pidPath.toFile()).exists();
final SynchronizerConfiguration syncConfigBehind =
SynchronizerConfiguration.builder()

@ -1977,6 +1977,21 @@ public class BesuCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void engineApiAuthOptions() {
parseCommand(
"--rpc-http-enabled",
"--Xmerge-support",
"true",
"--engine-jwt-enabled",
"--engine-jwt-secret",
"/tmp/fakeKey.hex");
verify(mockRunnerBuilder).engineJsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
assertThat(jsonRpcConfigArgumentCaptor.getValue().isAuthenticationEnabled()).isTrue();
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcHttpNoAuthApiMethodsCannotBeInvalid() {
parseCommand("--rpc-http-enabled", "--rpc-http-api-method-no-auth", "invalid");

@ -44,6 +44,8 @@ random-peer-priority-enabled=false
host-whitelist=["all"]
host-allowlist=["all"]
engine-host-allowlist=["all"]
engine-jwt-enabled=false
engine-jwt-secret="/tmp/jwt.hex"
required-blocks=["8675309=123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"]
discovery-dns-url="enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@nodes.example.org"

@ -70,6 +70,9 @@ public class JsonRpcConfiguration {
engineMethodGroup.add(RpcApis.ENGINE.name());
engineMethodGroup.add(RpcApis.ETH.name());
config.setRpcApis(engineMethodGroup);
config.setAuthenticationEnabled(true);
config.setAuthenticationAlgorithm(JwtAlgorithm.HS256);
config.setAuthenticationPublicKeyFile(null); // ephemeral key will be generated on startup.
return config;
}

@ -24,6 +24,7 @@ import org.hyperledger.besu.ethereum.api.handlers.HandlerFactory;
import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationUtils;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.DefaultAuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.context.ContextKey;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
@ -192,12 +193,12 @@ public class JsonRpcHttpService {
metricsSystem,
natService,
methods,
AuthenticationService.create(vertx, config),
DefaultAuthenticationService.create(vertx, config),
livenessService,
readinessService);
}
private JsonRpcHttpService(
public JsonRpcHttpService(
final Vertx vertx,
final Path dataDir,
final JsonRpcConfiguration config,
@ -363,7 +364,7 @@ public class JsonRpcHttpService {
.route("/login")
.method(HttpMethod.POST)
.produces(APPLICATION_JSON)
.handler(AuthenticationService::handleDisabledLogin);
.handler(DefaultAuthenticationService::handleDisabledLogin);
}
return router;
}
@ -568,32 +569,41 @@ public class JsonRpcHttpService {
if (authenticationService.isPresent() && token == null && config.getNoAuthRpcApis().isEmpty()) {
// no auth token when auth required
handleJsonRpcUnauthorizedError(routingContext, null, JsonRpcError.UNAUTHORIZED);
return;
}
// Parse json
try {
final String json = routingContext.getBodyAsString().trim();
if (!json.isEmpty() && json.charAt(0) == '{') {
final JsonObject requestBodyJsonObject =
ContextKey.REQUEST_BODY_AS_JSON_OBJECT.extractFrom(
routingContext, () -> new JsonObject(json));
AuthenticationUtils.getUser(
authenticationService,
token,
user -> handleJsonSingleRequest(routingContext, requestBodyJsonObject, user));
} else {
final JsonArray array = new JsonArray(json);
if (array.size() < 1) {
handleJsonRpcError(routingContext, null, INVALID_REQUEST);
return;
} else {
// Parse json
try {
final String json = routingContext.getBodyAsString().trim();
if (!json.isEmpty() && json.charAt(0) == '{') {
final JsonObject requestBodyJsonObject =
ContextKey.REQUEST_BODY_AS_JSON_OBJECT.extractFrom(
routingContext, () -> new JsonObject(json));
if (authenticationService.isPresent()) {
authenticationService
.get()
.authenticate(
token,
user -> handleJsonSingleRequest(routingContext, requestBodyJsonObject, user));
} else {
handleJsonSingleRequest(routingContext, requestBodyJsonObject, Optional.empty());
}
} else {
final JsonArray array = new JsonArray(json);
if (array.size() < 1) {
handleJsonRpcError(routingContext, null, INVALID_REQUEST);
return;
}
if (authenticationService.isPresent()) {
authenticationService
.get()
.authenticate(token, user -> handleJsonBatchRequest(routingContext, array, user));
} else {
handleJsonBatchRequest(routingContext, array, Optional.empty());
}
}
AuthenticationUtils.getUser(
authenticationService,
token,
user -> handleJsonBatchRequest(routingContext, array, user));
} catch (final DecodeException | NullPointerException ex) {
handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR);
}
} catch (final DecodeException | NullPointerException ex) {
handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR);
}
}
@ -745,8 +755,11 @@ public class JsonRpcHttpService {
final JsonRpcMethod method = rpcMethods.get(requestBody.getMethod());
if (AuthenticationUtils.isPermitted(
authenticationService, user, method, config.getNoAuthRpcApis())) {
if (!authenticationService.isPresent()
|| (authenticationService.isPresent()
&& authenticationService
.get()
.isPermitted(user, method, config.getNoAuthRpcApis()))) {
// Generate response
try (final OperationTimer.TimingContext ignored =
requestTimer.labels(requestBody.getMethod()).startTimer()) {

@ -1,8 +1,8 @@
/*
* Copyright ConsenSys AG.
* Copyright Hyperledger Besu.
*
* 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
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
@ -11,206 +11,30 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import java.io.File;
import java.util.Collection;
import java.util.Optional;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.core.Handler;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
/** Provides authentication handlers for use in the http and websocket services */
public class AuthenticationService {
private final JWTAuth jwtAuthProvider;
@VisibleForTesting public final JWTAuthOptions jwtAuthOptions;
private final Optional<AuthProvider> credentialAuthProvider;
private static final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
private AuthenticationService(
final JWTAuth jwtAuthProvider,
final JWTAuthOptions jwtAuthOptions,
final Optional<AuthProvider> credentialAuthProvider) {
this.jwtAuthProvider = jwtAuthProvider;
this.jwtAuthOptions = jwtAuthOptions;
this.credentialAuthProvider = credentialAuthProvider;
}
/**
* Creates a ready for use set of authentication providers if authentication is configured to be
* on
*
* @param vertx The vertx instance that will be providing requests that this set of authentication
* providers will be handling
* @param config The {{@link JsonRpcConfiguration}} that describes this rpc setup
* @return Optionally an authentication service. If empty then authentication isn't to be enabled
* on this service
*/
public static Optional<AuthenticationService> create(
final Vertx vertx, final JsonRpcConfiguration config) {
return create(
vertx,
config.isAuthenticationEnabled(),
config.getAuthenticationCredentialsFile(),
config.getAuthenticationPublicKeyFile(),
config.getAuthenticationAlgorithm());
}
/**
* Creates a ready for use set of authentication providers if authentication is configured to be
* on
*
* @param vertx The vertx instance that will be providing requests that this set of authentication
* providers will be handling
* @param config The {{@link JsonRpcConfiguration}} that describes this rpc setup
* @return Optionally an authentication service. If empty then authentication isn't to be enabled
* on this service
*/
public static Optional<AuthenticationService> create(
final Vertx vertx, final WebSocketConfiguration config) {
return create(
vertx,
config.isAuthenticationEnabled(),
config.getAuthenticationCredentialsFile(),
config.getAuthenticationPublicKeyFile(),
config.getAuthenticationAlgorithm());
}
private static Optional<AuthenticationService> create(
final Vertx vertx,
final boolean authenticationEnabled,
final String authenticationCredentialsFile,
final File authenticationPublicKeyFile,
final JwtAlgorithm authenticationAlgorithm) {
if (!authenticationEnabled) {
return Optional.empty();
}
final JWTAuthOptions jwtAuthOptions;
if (authenticationPublicKeyFile == null) {
jwtAuthOptions = jwtAuthOptionsFactory.createWithGeneratedKeyPair();
} else {
jwtAuthOptions =
authenticationAlgorithm == null
? jwtAuthOptionsFactory.createForExternalPublicKey(authenticationPublicKeyFile)
: jwtAuthOptionsFactory.createForExternalPublicKeyWithAlgorithm(
authenticationPublicKeyFile, authenticationAlgorithm.toString());
}
final Optional<AuthProvider> credentialAuthProvider =
makeCredentialAuthProvider(vertx, authenticationEnabled, authenticationCredentialsFile);
return Optional.of(
new AuthenticationService(
JWTAuth.create(vertx, jwtAuthOptions), jwtAuthOptions, credentialAuthProvider));
}
private static Optional<AuthProvider> makeCredentialAuthProvider(
final Vertx vertx,
final boolean authenticationEnabled,
@Nullable final String authenticationCredentialsFile) {
if (authenticationEnabled && authenticationCredentialsFile != null) {
return Optional.of(
new TomlAuthOptions().setTomlPath(authenticationCredentialsFile).createProvider(vertx));
} else {
return Optional.empty();
}
}
/**
* Static route for terminating login requests when Authentication is disabled
*
* @param routingContext The vertx routing context for this request
*/
public static void handleDisabledLogin(final RoutingContext routingContext) {
routingContext
.response()
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage("Authentication not enabled")
.end();
}
/**
* Handles a login request and checks the provided credentials against our credential auth
* provider
*
* @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) {
routingContext
.response()
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage(HttpResponseStatus.BAD_REQUEST.reasonPhrase())
.end();
return;
}
// Check user
final JsonObject authParams = new JsonObject();
authParams.put("username", requestBody.getValue("username"));
authParams.put("password", requestBody.getValue("password"));
credentialAuthProvider.authenticate(
authParams,
(r) -> {
if (r.failed()) {
routingContext
.response()
.setStatusCode(HttpResponseStatus.UNAUTHORIZED.code())
.setStatusMessage(HttpResponseStatus.UNAUTHORIZED.reasonPhrase())
.end();
} else {
final User user = r.result();
final JWTOptions options =
new JWTOptions().setExpiresInMinutes(5).setAlgorithm("RS256");
final JsonObject jwtContents =
new JsonObject()
.put("permissions", user.principal().getValue("permissions"))
.put("username", user.principal().getValue("username"));
final String privacyPublicKey = user.principal().getString("privacyPublicKey");
if (privacyPublicKey != null) {
jwtContents.put("privacyPublicKey", privacyPublicKey);
}
public interface AuthenticationService {
void handleLogin(RoutingContext routingContext);
final String token = jwtAuthProvider.generateToken(jwtContents, options);
JWTAuth getJwtAuthProvider();
final JsonObject responseBody = new JsonObject().put("token", token);
final HttpServerResponse response = routingContext.response();
if (!response.closed()) {
response.setStatusCode(200);
response.putHeader("Content-Type", "application/json");
response.end(responseBody.encode());
}
}
});
}
void authenticate(String token, Handler<Optional<User>> handler);
public JWTAuth getJwtAuthProvider() {
return jwtAuthProvider;
}
boolean isPermitted(
final Optional<User> optionalUser,
final JsonRpcMethod jsonRpcMethod,
final Collection<String> noAuthMethods);
}

@ -14,101 +14,7 @@
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuthenticationUtils {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationUtils.class);
public static boolean isPermitted(
final Optional<AuthenticationService> authenticationService,
final Optional<User> optionalUser,
final JsonRpcMethod jsonRpcMethod,
final Collection<String> noAuthMethods) {
AtomicBoolean foundMatchingPermission = new AtomicBoolean();
if (authenticationService.isEmpty()) {
// no auth provider configured thus anything is permitted
return true;
}
// if the method is configured as a no auth method we skip permission check
if (noAuthMethods.stream().anyMatch(m -> m.equals(jsonRpcMethod.getName()))) {
return true;
}
if (optionalUser.isPresent()) {
User user = optionalUser.get();
for (String perm : jsonRpcMethod.getPermissions()) {
user.isAuthorized(
perm,
(authed) -> {
if (authed.result()) {
LOG.trace(
"user {} authorized : {} via permission {}",
user,
jsonRpcMethod.getName(),
perm);
foundMatchingPermission.set(true);
}
});
// exit if a matching permission was found, no need to keep checking
if (foundMatchingPermission.get()) {
return foundMatchingPermission.get();
}
}
}
if (!foundMatchingPermission.get()) {
LOG.trace("user NOT authorized : {}", jsonRpcMethod.getName());
}
return foundMatchingPermission.get();
}
public static void getUser(
final Optional<AuthenticationService> authenticationService,
final String token,
final Handler<Optional<User>> handler) {
try {
if (authenticationService.isEmpty()) {
handler.handle(Optional.empty());
} else {
authenticationService
.get()
.getJwtAuthProvider()
.authenticate(
new JsonObject().put("token", token),
(r) -> {
if (r.succeeded()) {
final Optional<User> user = Optional.ofNullable(r.result());
validateExpiryExists(user);
handler.handle(user);
} else {
LOG.debug("Invalid JWT token", r.cause());
handler.handle(Optional.empty());
}
});
}
} catch (Exception e) {
handler.handle(Optional.empty());
}
}
private static void validateExpiryExists(final Optional<User> user) {
if (!user.map(User::attributes).map(a -> a.containsKey("exp")).orElse(false)) {
throw new IllegalStateException("Invalid JWT doesn't have expiry");
}
}
public static String getJwtTokenFromAuthorizationHeaderValue(final String value) {
if (value != null) {

@ -0,0 +1,292 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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 org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import java.io.File;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.authentication.AuthenticationProvider;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Provides authentication handlers for use in the http and websocket services */
public class DefaultAuthenticationService implements AuthenticationService {
public static final String USERNAME = "username";
private final JWTAuth jwtAuthProvider;
@VisibleForTesting public final JWTAuthOptions jwtAuthOptions;
private final Optional<AuthenticationProvider> credentialAuthProvider;
private static final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory();
private static final Logger LOG = LoggerFactory.getLogger(DefaultAuthenticationService.class);
public DefaultAuthenticationService(
final JWTAuth jwtAuthProvider,
final JWTAuthOptions jwtAuthOptions,
final Optional<AuthenticationProvider> credentialAuthProvider) {
this.jwtAuthProvider = jwtAuthProvider;
this.jwtAuthOptions = jwtAuthOptions;
this.credentialAuthProvider = credentialAuthProvider;
}
/**
* Creates a ready for use set of authentication providers if authentication is enabled
*
* @param vertx The vertx instance that will be providing requests that this set of authentication
* providers will be handling
* @param config The {{@link JsonRpcConfiguration}} that describes this rpc setup
* @return Optionally an authentication service. If empty then authentication isn't to be enabled
* on this service
*/
public static Optional<AuthenticationService> create(
final Vertx vertx, final JsonRpcConfiguration config) {
return create(
vertx,
config.isAuthenticationEnabled(),
config.getAuthenticationCredentialsFile(),
config.getAuthenticationPublicKeyFile(),
config.getAuthenticationAlgorithm());
}
/**
* Creates a ready for use set of authentication providers if authentication is enabled
*
* @param vertx The vertx instance that will be providing requests that this set of authentication
* providers will be handling
* @param config The {{@link WebSocketConfiguration}} that describes this rpc setup
* @return Optionally an authentication service. If empty then authentication isn't to be enabled
* on this service
*/
public static Optional<AuthenticationService> create(
final Vertx vertx, final WebSocketConfiguration config) {
return create(
vertx,
config.isAuthenticationEnabled(),
config.getAuthenticationCredentialsFile(),
config.getAuthenticationPublicKeyFile(),
config.getAuthenticationAlgorithm());
}
private static Optional<AuthenticationService> create(
final Vertx vertx,
final boolean authenticationEnabled,
final String authenticationCredentialsFile,
final File authenticationPublicKeyFile,
final JwtAlgorithm authenticationAlgorithm) {
if (!authenticationEnabled) {
return Optional.empty();
}
final JWTAuthOptions jwtAuthOptions;
if (authenticationPublicKeyFile == null) {
jwtAuthOptions = jwtAuthOptionsFactory.createWithGeneratedKeyPair();
} else {
jwtAuthOptions =
authenticationAlgorithm == null
? jwtAuthOptionsFactory.createForExternalPublicKey(authenticationPublicKeyFile)
: jwtAuthOptionsFactory.createForExternalPublicKeyWithAlgorithm(
authenticationPublicKeyFile, authenticationAlgorithm);
}
final Optional<AuthenticationProvider> credentialAuthProvider =
makeCredentialAuthProvider(vertx, authenticationEnabled, authenticationCredentialsFile);
return Optional.of(
new DefaultAuthenticationService(
JWTAuth.create(vertx, jwtAuthOptions), jwtAuthOptions, credentialAuthProvider));
}
private static Optional<AuthenticationProvider> makeCredentialAuthProvider(
final Vertx vertx,
final boolean authenticationEnabled,
@Nullable final String authenticationCredentialsFile) {
if (authenticationEnabled && authenticationCredentialsFile != null) {
return Optional.of(
new TomlAuthOptions().setTomlPath(authenticationCredentialsFile).createProvider(vertx));
} else {
return Optional.empty();
}
}
/**
* Static route for terminating login requests when Authentication is disabled
*
* @param routingContext The vertx routing context for this request
*/
public static void handleDisabledLogin(final RoutingContext routingContext) {
routingContext
.response()
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage("Authentication not enabled")
.end();
}
/**
* Handles a login request and checks the provided credentials against our credential auth
* provider
*
* @param routingContext Routing context associated with this request
*/
@Override
public void handleLogin(final RoutingContext routingContext) {
if (credentialAuthProvider.isPresent()) {
login(routingContext, credentialAuthProvider.get());
} else {
handleDisabledLogin(routingContext);
}
}
private void login(
final RoutingContext routingContext, final AuthenticationProvider credentialAuthProvider) {
final JsonObject requestBody = routingContext.getBodyAsJson();
if (requestBody == null) {
routingContext
.response()
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage(HttpResponseStatus.BAD_REQUEST.reasonPhrase())
.end();
return;
}
// Check user
final JsonObject authParams = new JsonObject();
authParams.put(USERNAME, requestBody.getValue(USERNAME));
authParams.put("password", requestBody.getValue("password"));
credentialAuthProvider.authenticate(
authParams,
r -> {
if (r.failed()) {
routingContext
.response()
.setStatusCode(HttpResponseStatus.UNAUTHORIZED.code())
.setStatusMessage(HttpResponseStatus.UNAUTHORIZED.reasonPhrase())
.end();
} else {
final User user = r.result();
final JWTOptions options =
new JWTOptions().setExpiresInMinutes(5).setAlgorithm("RS256");
final JsonObject jwtContents =
new JsonObject()
.put("permissions", user.principal().getValue("permissions"))
.put(USERNAME, user.principal().getValue(USERNAME));
final String privacyPublicKey = user.principal().getString("privacyPublicKey");
if (privacyPublicKey != null) {
jwtContents.put("privacyPublicKey", privacyPublicKey);
}
final String token = jwtAuthProvider.generateToken(jwtContents, options);
final JsonObject responseBody = new JsonObject().put("token", token);
final HttpServerResponse response = routingContext.response();
if (!response.closed()) {
response.setStatusCode(200);
response.putHeader("Content-Type", "application/json");
response.end(responseBody.encode());
}
}
});
}
@Override
public JWTAuth getJwtAuthProvider() {
return jwtAuthProvider;
}
@Override
public void authenticate(final String token, final Handler<Optional<User>> handler) {
try {
getJwtAuthProvider()
.authenticate(
new JsonObject().put("token", token),
r -> {
if (r.succeeded()) {
final Optional<User> user = Optional.ofNullable(r.result());
validateExpiryExists(user);
handler.handle(user);
} else {
LOG.debug("Invalid JWT token {}", r.cause().toString());
handler.handle(Optional.empty());
}
});
} catch (Exception e) {
LOG.debug("exception validating JWT ", e);
handler.handle(Optional.empty());
}
}
@Override
public boolean isPermitted(
final Optional<User> optionalUser,
final JsonRpcMethod jsonRpcMethod,
final Collection<String> noAuthMethods) {
AtomicBoolean foundMatchingPermission = new AtomicBoolean();
// if the method is configured as a no auth method we skip permission check
if (noAuthMethods.stream().anyMatch(m -> m.equals(jsonRpcMethod.getName()))) {
return true;
}
if (optionalUser.isPresent()) {
User user = optionalUser.get();
for (String perm : jsonRpcMethod.getPermissions()) {
user.isAuthorized(
perm,
(authed) -> {
if (authed.result()) {
LOG.trace(
"user {} authorized : {} via permission {}",
user,
jsonRpcMethod.getName(),
perm);
foundMatchingPermission.set(true);
}
});
// exit if a matching permission was found, no need to keep checking
if (foundMatchingPermission.get()) {
return foundMatchingPermission.get();
}
}
}
if (!foundMatchingPermission.get()) {
LOG.trace("user NOT authorized : {}", jsonRpcMethod.getName());
}
return foundMatchingPermission.get();
}
private void validateExpiryExists(final Optional<User> user) {
if (!user.map(User::attributes).map(a -> a.containsKey("exp")).orElse(false)) {
throw new IllegalStateException("Invalid JWT doesn't have expiry");
}
}
}

@ -0,0 +1,165 @@
/*
* Copyright Hyperledger Besu.
*
* 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 org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.impl.Codec;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
import org.apache.tuweni.bytes.Bytes32;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EngineAuthService implements AuthenticationService {
private static final Logger LOG = LoggerFactory.getLogger(EngineAuthService.class);
private final JWTAuth jwtAuthProvider;
public EngineAuthService(final Vertx vertx, final Optional<File> signingKey, final Path datadir) {
final JWTAuthOptions jwtAuthOptions =
engineApiJWTOptions(JwtAlgorithm.HS256, signingKey, datadir);
this.jwtAuthProvider = JWTAuth.create(vertx, jwtAuthOptions);
}
private JWTAuthOptions engineApiJWTOptions(
final JwtAlgorithm jwtAlgorithm, final Optional<File> keyFile, final Path datadir) {
byte[] signingKey = null;
if (!keyFile.isPresent()) {
final File jwtFile = new File(datadir.toFile(), "jwt.hex");
jwtFile.deleteOnExit();
final byte[] ephemeralKey = Bytes32.random().toArray();
try {
Files.writeString(jwtFile.toPath(), Codec.base16Encode(ephemeralKey));
} catch (IOException ioe) {
LOG.warn("Unable to write ephemeral jwt key file to {}", jwtFile.toPath().toString());
LOG.info("JWT KEY: {}", Codec.base16Encode(ephemeralKey));
}
signingKey = ephemeralKey;
} else { // user configured option to use a specified file
if (keyFile.get().exists()) {
try {
final String keyHex = Files.readAllLines(keyFile.get().toPath()).get(0);
if (keyHex.length() >= 64) {
signingKey = Codec.base16Decode(keyHex);
} else {
UnsecurableEngineApiException e =
new UnsecurableEngineApiException("signing key too short, 256 bits required");
e.fillInStackTrace();
throw e;
}
} catch (IOException ioe) {
UnsecurableEngineApiException e =
new UnsecurableEngineApiException(
"Could not read key from " + keyFile.get().toString());
e.fillInStackTrace();
e.initCause(ioe);
throw e;
}
} else {
UnsecurableEngineApiException e =
new UnsecurableEngineApiException(
"Could not read key from " + keyFile.get().toString());
e.fillInStackTrace();
throw e;
}
}
if (signingKey == null || signingKey.length < 32) {
UnsecurableEngineApiException e =
new UnsecurableEngineApiException(
"Could not read at least 256 bits of key from "
+ (keyFile.isPresent() ? keyFile.get().toString() : "undefined"));
e.fillInStackTrace();
throw e;
}
return new JWTAuthOptions()
.setJWTOptions(new JWTOptions().setIgnoreExpiration(true).setLeeway(5))
.addPubSecKey(
new PubSecKeyOptions()
.setAlgorithm(jwtAlgorithm.toString())
.setBuffer(Buffer.buffer(signingKey)));
}
@Override
public void handleLogin(final RoutingContext routingContext) {
LOG.warn("Engine Auth does not support logins, no login handled");
}
@Override
public JWTAuth getJwtAuthProvider() {
return this.jwtAuthProvider;
}
@Override
public void authenticate(final String token, final Handler<Optional<User>> handler) {
try {
JsonObject jwt = new JsonObject().put("token", token);
getJwtAuthProvider()
.authenticate(
jwt,
r -> {
if (r.succeeded()) {
if (issuedRecently(r.result().attributes().getLong("iat"))) {
final Optional<User> user = Optional.ofNullable(r.result());
handler.handle(user);
} else {
LOG.warn("Client sent stale token: {}", r.result().attributes());
handler.handle(Optional.empty());
}
} else {
LOG.debug("Authentication failed: {}", r.cause().toString());
handler.handle(Optional.empty());
}
});
} catch (Exception e) {
LOG.debug("exception validating JWT ", e);
handler.handle(Optional.empty());
}
}
@Override
public boolean isPermitted(
final Optional<User> optionalUser,
final JsonRpcMethod jsonRpcMethod,
final Collection<String> noAuthMethods) {
return true; // no AuthZ for engine APIs
}
private boolean issuedRecently(final long iat) {
long iatSecondsSinceEpoch = iat;
long nowSecondsSinceEpoch = System.currentTimeMillis() / 1000;
return (Math.abs((nowSecondsSinceEpoch - iatSecondsSinceEpoch)) <= 5);
}
}

@ -1,5 +1,5 @@
/*
* Copyright ConsenSys AG.
* Copyright Hyperledger Besu Contributors
*
* 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
@ -24,43 +24,64 @@ import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.impl.Codec;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
public class JWTAuthOptionsFactory {
private static final String DEFAULT_ALGORITHM = "RS256";
private static final JwtAlgorithm DEFAULT = JwtAlgorithm.RS256;
public JWTAuthOptions createForExternalPublicKey(final File externalPublicKeyFile) {
return createForExternalPublicKeyWithAlgorithm(externalPublicKeyFile, DEFAULT_ALGORITHM);
return createForExternalPublicKeyWithAlgorithm(externalPublicKeyFile, DEFAULT);
}
public JWTAuthOptions createForExternalPublicKeyWithAlgorithm(
final File externalPublicKeyFile, final String algorithm) {
final File externalPublicKeyFile, final JwtAlgorithm algorithm) {
final byte[] externalJwtPublicKey = readPublicKey(externalPublicKeyFile);
return new JWTAuthOptions()
.addPubSecKey(
new PubSecKeyOptions()
.setAlgorithm(algorithm)
.setAlgorithm(algorithm.toString())
.setBuffer(keyPairToPublicPemString(externalJwtPublicKey)));
}
public JWTAuthOptions createWithGeneratedKeyPair() {
final KeyPair keypair = generateJwtKeyPair();
public JWTAuthOptions createWithGeneratedKeyPair(final JwtAlgorithm jwtAlgorithm) {
if (jwtAlgorithm.toString().startsWith("H")) {
throw new IllegalArgumentException(
"Cannot use keypairs with HMAC tokens, please call createWithGeneratedKey");
}
final KeyPair keypair = generateRsaKeyPair();
return new JWTAuthOptions()
.addPubSecKey(
new PubSecKeyOptions()
.setAlgorithm(DEFAULT_ALGORITHM)
.setAlgorithm(jwtAlgorithm.toString())
.setBuffer(keyPairToPublicPemString(keypair.getPublic().getEncoded())))
.addPubSecKey(
new PubSecKeyOptions()
.setAlgorithm(DEFAULT_ALGORITHM)
.setAlgorithm(jwtAlgorithm.toString())
.setBuffer(keyPairToPrivatePemString(keypair)));
}
public JWTAuthOptions engineApiJWTOptions(final JwtAlgorithm jwtAlgorithm) {
byte[] ephemeralKey = Bytes32.random().toArray();
return new JWTAuthOptions()
.setJWTOptions(new JWTOptions().setIgnoreExpiration(true).setLeeway(5))
.addPubSecKey(
new PubSecKeyOptions()
.setAlgorithm(jwtAlgorithm.toString())
.setBuffer(Buffer.buffer(ephemeralKey)));
}
public JWTAuthOptions createWithGeneratedKeyPair() {
return createWithGeneratedKeyPair(JwtAlgorithm.RS256);
}
private byte[] readPublicKey(final File publicKeyFile) {
try (final BufferedReader reader = Files.newBufferedReader(publicKeyFile.toPath(), UTF_8);
final PemReader pemReader = new PemReader(reader)) {
@ -74,7 +95,7 @@ public class JWTAuthOptionsFactory {
}
}
private KeyPair generateJwtKeyPair() {
private KeyPair generateRsaKeyPair() {
final KeyPairGenerator keyGenerator;
try {
keyGenerator = KeyPairGenerator.getInstance("RSA");

@ -20,6 +20,7 @@ public enum JwtAlgorithm {
RS512,
ES256,
ES384,
HS256,
ES512;
public static JwtAlgorithm fromString(final String str) {

@ -0,0 +1,26 @@
/*
* Copyright Hyperledger Besu.
*
* 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;
/*
* Thrown when config options are insufficient to secure the Engine API
*/
public class UnsecurableEngineApiException extends RuntimeException {
public UnsecurableEngineApiException(final String reason) {
super(reason);
}
}

@ -20,7 +20,6 @@ import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRp
import org.hyperledger.besu.ethereum.api.handlers.IsAliveHandler;
import org.hyperledger.besu.ethereum.api.handlers.RpcMethodTimeoutException;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationUtils;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
@ -154,7 +153,9 @@ public class WebSocketRequestHandler {
try {
LOG.debug("WS-RPC request -> {}", requestBody.getMethod());
requestBody.setConnectionId(websocket.textHandlerID());
if (AuthenticationUtils.isPermitted(authenticationService, user, method, noAuthApiMethods)) {
if (authenticationService.isEmpty()
|| (authenticationService.isPresent()
&& authenticationService.get().isPermitted(user, method, noAuthApiMethods))) {
final JsonRpcRequestContext requestContext =
new JsonRpcRequestContext(
requestBody, user, new IsAliveHandler(ethScheduler, timeoutSec));

@ -18,6 +18,7 @@ import static com.google.common.collect.Streams.stream;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationUtils;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.DefaultAuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager;
import java.net.InetSocketAddress;
@ -70,10 +71,10 @@ public class WebSocketService {
vertx,
configuration,
websocketRequestHandler,
AuthenticationService.create(vertx, configuration));
DefaultAuthenticationService.create(vertx, configuration));
}
private WebSocketService(
public WebSocketService(
final Vertx vertx,
final WebSocketConfiguration configuration,
final WebSocketRequestHandler websocketRequestHandler,
@ -132,16 +133,26 @@ public class WebSocketService {
buffer.toString(),
socketAddressAsString(socketAddress));
AuthenticationUtils.getUser(
authenticationService,
token,
user ->
websocketRequestHandler.handle(
authenticationService,
websocket,
buffer.toString(),
user,
configuration.getRpcApisNoAuth()));
if (authenticationService.isPresent()) {
authenticationService
.get()
.authenticate(
token,
user ->
websocketRequestHandler.handle(
authenticationService,
websocket,
buffer.toString(),
user,
configuration.getRpcApisNoAuth()));
} else {
websocketRequestHandler.handle(
Optional.empty(),
websocket,
buffer.toString(),
Optional.empty(),
configuration.getRpcApisNoAuth());
}
});
websocket.textMessageHandler(
@ -151,16 +162,26 @@ public class WebSocketService {
payload,
socketAddressAsString(socketAddress));
AuthenticationUtils.getUser(
authenticationService,
token,
user ->
websocketRequestHandler.handle(
authenticationService,
websocket,
payload,
user,
configuration.getRpcApisNoAuth()));
if (authenticationService.isPresent()) {
authenticationService
.get()
.authenticate(
token,
user ->
websocketRequestHandler.handle(
authenticationService,
websocket,
payload,
user,
configuration.getRpcApisNoAuth()));
} else {
websocketRequestHandler.handle(
Optional.empty(),
websocket,
payload,
Optional.empty(),
configuration.getRpcApisNoAuth());
}
});
websocket.closeHandler(
@ -226,7 +247,7 @@ public class WebSocketService {
router
.post("/login")
.produces(APPLICATION_JSON)
.handler(AuthenticationService::handleDisabledLogin);
.handler(DefaultAuthenticationService::handleDisabledLogin);
}
router.route().handler(WebSocketService::handleHttpNotSupported);

@ -23,7 +23,6 @@ import static org.mockito.Mockito.spy;
import org.hyperledger.besu.config.StubGenesisConfigOptions;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationUtils;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.filter.FilterManager;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthAccounts;
@ -399,42 +398,37 @@ public class JsonRpcHttpServiceLoginTest {
final User user = r.result();
// single eth/blockNumber method permitted
Assertions.assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
ethBlockNumber,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), ethBlockNumber, Collections.emptyList()))
.isTrue();
// eth/accounts NOT permitted
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
ethAccounts,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), ethAccounts, Collections.emptyList()))
.isFalse();
// allowed by web3/*
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
web3ClientVersion,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), web3ClientVersion, Collections.emptyList()))
.isTrue();
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
web3Sha3,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), web3Sha3, Collections.emptyList()))
.isTrue();
// NO net permissions
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
netVersion,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), netVersion, Collections.emptyList()))
.isFalse();
});
}
@ -474,42 +468,37 @@ public class JsonRpcHttpServiceLoginTest {
final User user = r.result();
// single eth/blockNumber method permitted
Assertions.assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
ethBlockNumber,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), ethBlockNumber, Collections.emptyList()))
.isTrue();
// eth/accounts IS permitted
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
ethAccounts,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), ethAccounts, Collections.emptyList()))
.isTrue();
// allowed by *:*
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
web3ClientVersion,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), web3ClientVersion, Collections.emptyList()))
.isTrue();
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
web3Sha3,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), web3Sha3, Collections.emptyList()))
.isTrue();
// YES net permissions
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.of(user),
netVersion,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.of(user), netVersion, Collections.emptyList()))
.isTrue();
});
}
@ -520,11 +509,10 @@ public class JsonRpcHttpServiceLoginTest {
final JsonRpcMethod ethAccounts = new EthAccounts();
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService,
Optional.empty(),
ethAccounts,
Collections.emptyList()))
service
.authenticationService
.get()
.isPermitted(Optional.empty(), ethAccounts, Collections.emptyList()))
.isFalse();
}

@ -21,13 +21,22 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguratio
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.User;
import org.junit.Test;
public class AuthenticationServiceTest {
private final Vertx vertx = Vertx.vertx();
private static final String INVALID_TOKEN_WITHOUT_EXP =
"ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn"
+ "0.eyJpYXQiOjE1MTYyMzkwMjIsInBlcm1pc3Npb25zIjpbIm5ldDpwZWVyQ291bnQiXX0";
@Test
public void authenticationServiceNotCreatedWhenRpcAuthenticationDisabledAndHasCredentialsFile() {
@ -36,7 +45,7 @@ public class AuthenticationServiceTest {
jsonRpcConfiguration.setAuthenticationCredentialsFile("some/file/path");
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, jsonRpcConfiguration);
DefaultAuthenticationService.create(vertx, jsonRpcConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -49,7 +58,7 @@ public class AuthenticationServiceTest {
jsonRpcConfiguration.setAuthenticationPublicKeyFile(publicKeyFile);
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, jsonRpcConfiguration);
DefaultAuthenticationService.create(vertx, jsonRpcConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -64,7 +73,7 @@ public class AuthenticationServiceTest {
jsonRpcConfiguration.setAuthenticationCredentialsFile("some/file/path");
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, jsonRpcConfiguration);
DefaultAuthenticationService.create(vertx, jsonRpcConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -75,7 +84,7 @@ public class AuthenticationServiceTest {
webSocketConfiguration.setAuthenticationCredentialsFile("some/file/path");
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, webSocketConfiguration);
DefaultAuthenticationService.create(vertx, webSocketConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -88,7 +97,7 @@ public class AuthenticationServiceTest {
webSocketConfiguration.setAuthenticationPublicKeyFile(publicKeyFile);
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, webSocketConfiguration);
DefaultAuthenticationService.create(vertx, webSocketConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -103,7 +112,7 @@ public class AuthenticationServiceTest {
webSocketConfiguration.setAuthenticationCredentialsFile("some/file/path");
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, webSocketConfiguration);
DefaultAuthenticationService.create(vertx, webSocketConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -114,7 +123,7 @@ public class AuthenticationServiceTest {
jsonRpcConfiguration.setAuthenticationAlgorithm(JwtAlgorithm.RS256);
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, jsonRpcConfiguration);
DefaultAuthenticationService.create(vertx, jsonRpcConfiguration);
assertThat(authenticationService).isEmpty();
}
@ -125,7 +134,57 @@ public class AuthenticationServiceTest {
webSocketConfiguration.setAuthenticationAlgorithm(JwtAlgorithm.RS256);
final Optional<AuthenticationService> authenticationService =
AuthenticationService.create(vertx, webSocketConfiguration);
DefaultAuthenticationService.create(vertx, webSocketConfiguration);
assertThat(authenticationService).isEmpty();
}
@Test
public void getUserFailsIfTokenDoesNotHaveExpiryClaim() {
final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
webSocketConfiguration.setAuthenticationEnabled(true);
final AuthenticationService authenticationService =
DefaultAuthenticationService.create(vertx, webSocketConfiguration).get();
final StubUserHandler handler = new StubUserHandler();
authenticationService.authenticate(INVALID_TOKEN_WITHOUT_EXP, handler);
assertThat(handler.getEvent()).isEmpty();
}
@Test
public void getUserSucceedsWithValidToken() {
final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
webSocketConfiguration.setAuthenticationEnabled(true);
webSocketConfiguration.setAuthenticationPublicKeyFile(null);
final AuthenticationService authenticationService =
DefaultAuthenticationService.create(vertx, webSocketConfiguration).get();
final StubUserHandler handler = new StubUserHandler();
final JsonObject jwtContents =
new JsonObject()
.put("permissions", new JsonArray(Arrays.asList("net:peerCount")))
.put("username", "successKid");
final JWTOptions options = new JWTOptions().setExpiresInMinutes(5).setAlgorithm("RS256");
final String token =
authenticationService.getJwtAuthProvider().generateToken(jwtContents, options);
authenticationService.authenticate(token, handler);
User successKid = handler.getEvent().get();
assertThat(successKid.attributes().getLong("exp")).isNotNull();
assertThat(successKid.attributes().getLong("iat")).isNotNull();
assertThat(successKid.principal().getJsonArray("permissions").getString(0))
.isEqualTo("net:peerCount");
}
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;
}
}
}

@ -15,27 +15,11 @@
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.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 final String INVALID_TOKEN_WITHOUT_EXP =
"ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn"
+ "0.eyJpYXQiOjE1MTYyMzkwMjIsInBlcm1pc3Npb25zIjpbIm5ldDpwZWVyQ291bnQiXX0";
private static final String VALID_TOKEN =
"ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0.eyJpYXQiOjE1"
+ "MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWlzc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ";
@Test
public void getJwtTokenFromNullStringShouldReturnNull() {
final String headerValue = null;
@ -71,46 +55,4 @@ public class AuthenticationUtilsTest {
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);
User successKid = handler.getEvent().get();
assertThat(successKid.attributes().getLong("exp")).isEqualTo(4729363200L);
assertThat(successKid.attributes().getLong("iat")).isEqualTo(1516239022L);
assertThat(successKid.principal().getJsonArray("permissions").getString(0))
.isEqualTo("net:peerCount");
}
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;
}
}
}

@ -0,0 +1,120 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.mockito.Mockito.mock;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.jwt.JWTAuth;
import org.junit.Test;
public class EngineAuthServiceTest {
@Test
public void createsEphemeralByDefault() throws IOException {
Vertx vertx = mock(Vertx.class);
Path dataDir = Files.createTempDirectory("besuUnitTest");
EngineAuthService auth = new EngineAuthService(vertx, Optional.empty(), dataDir);
assertThat(auth).isNotNull();
assertThat(dataDir.toFile()).exists();
assertThat(dataDir.toFile()).isDirectory();
boolean defaultFileFound =
Arrays.stream(dataDir.toFile().listFiles())
.anyMatch(
file -> {
return file.getName().equals("jwt.hex");
});
assertThat(defaultFileFound).isTrue();
}
@Test
public void usesSpecified() throws IOException, URISyntaxException {
Vertx vertx = mock(Vertx.class);
final Path userKey =
Paths.get(ClassLoader.getSystemResource("authentication/ee-jwt-secret.hex").toURI());
Path dataDir = Files.createTempDirectory("besuUnitTest");
EngineAuthService auth = new EngineAuthService(vertx, Optional.of(userKey.toFile()), dataDir);
assertThat(auth).isNotNull();
JWTAuth jwtAuth = auth.getJwtAuthProvider();
String token =
jwtAuth.generateToken(new JsonObject().put("iat", System.currentTimeMillis() / 1000));
Handler<Optional<User>> authHandler =
new Handler<Optional<User>>() {
@Override
public void handle(final Optional<User> event) {
assertThat(event).isPresent();
assertThat(event.get()).isNotNull();
}
};
auth.authenticate(token, authHandler);
}
@Test(expected = UnsecurableEngineApiException.class)
public void throwsOnShortKey() throws IOException, URISyntaxException {
Vertx vertx = mock(Vertx.class);
final Path userKey =
Paths.get(
ClassLoader.getSystemResource("authentication/ee-jwt-secret-too-short.hex").toURI());
Path dataDir = Files.createTempDirectory("besuUnitTest");
EngineAuthService auth = new EngineAuthService(vertx, Optional.of(userKey.toFile()), dataDir);
assertThat(auth).isNotNull();
}
@Test(expected = UnsecurableEngineApiException.class)
public void throwsKeyFileMissing() throws IOException, URISyntaxException {
Vertx vertx = mock(Vertx.class);
final Path userKey = Paths.get("no-such-file.hex");
Path dataDir = Files.createTempDirectory("besuUnitTest");
EngineAuthService auth = new EngineAuthService(vertx, Optional.of(userKey.toFile()), dataDir);
assertThat(auth).isNotNull();
}
@Test
public void denyExpired() throws IOException, URISyntaxException {
Vertx vertx = mock(Vertx.class);
final Path userKey =
Paths.get(ClassLoader.getSystemResource("authentication/ee-jwt-secret.hex").toURI());
Path dataDir = Files.createTempDirectory("besuUnitTest");
EngineAuthService auth = new EngineAuthService(vertx, Optional.of(userKey.toFile()), dataDir);
assertThat(auth).isNotNull();
JWTAuth jwtAuth = auth.getJwtAuthProvider();
String token =
jwtAuth.generateToken(new JsonObject().put("iat", (System.currentTimeMillis() / 1000) - 6));
Handler<Optional<User>> authHandler =
new Handler<Optional<User>>() {
@Override
public void handle(final Optional<User> event) {
assertThat(event).isEmpty();
}
};
auth.authenticate(token, authHandler);
}
}

@ -109,7 +109,7 @@ public class JWTAuthOptionsFactoryTest {
try {
final JWTAuthOptions jwtAuthOptions =
jwtAuthOptionsFactory.createForExternalPublicKeyWithAlgorithm(
enclavePublicKeyFile, "ES256");
enclavePublicKeyFile, JwtAlgorithm.ES256);
assertThat(jwtAuthOptions.getPubSecKeys()).hasSize(1);
final PubSecKeyOptions pubSecKeyOptions = jwtAuthOptions.getPubSecKeys().get(0);
assertThat(pubSecKeyOptions.getAlgorithm()).isEqualTo("ES256");
@ -147,4 +147,12 @@ public class JWTAuthOptionsFactoryTest {
.isInstanceOf(IllegalStateException.class)
.hasMessage("Authentication RPC public key file format is invalid");
}
@Test
public void createsEphemeralHmacOptions() {
final JWTAuthOptionsFactory factory = new JWTAuthOptionsFactory();
JWTAuthOptions engineOptions = factory.engineApiJWTOptions(JwtAlgorithm.HS256);
byte[] publicKey = engineOptions.getPubSecKeys().get(0).getBuffer().getBytes();
assertThat(publicKey.length).isEqualTo(32);
}
}

@ -0,0 +1 @@
9465710175a93a3f2d67b0cb98d92d44ead4d1126a12233571884de92a8edc

@ -0,0 +1 @@
9465710175a93a3f2d67b0cb98d92d44ead4d1126a12233571884de92a8edc76
Loading…
Cancel
Save