From 587b831e46736c5606f2a410352227b994ee8dfa Mon Sep 17 00:00:00 2001 From: Chris Mckay Date: Mon, 11 Feb 2019 11:11:30 +1000 Subject: [PATCH 1/3] delete dead constructor for http service --- .../ethereum/jsonrpc/JsonRpcHttpService.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java index fe5335e720..73a3638da2 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java @@ -97,36 +97,6 @@ public class JsonRpcHttpService { private HttpServer httpServer; - /** - * Construct a JsonRpcHttpService handler that has authentication provided from outside the - * constructor - * - * @param vertx The vertx process that will be running this service - * @param dataDir The data directory where requests can be buffered - * @param config Configuration for the rpc methods being loaded - * @param metricsSystem The metrics service that activities should be reported to - * @param methods The json rpc methods that should be enabled - * @param jwtOptions The configuration for the jwt auth provider - * @param credentialAuthProvider An auth provider that is backed by a credentials store - */ - public JsonRpcHttpService( - final Vertx vertx, - final Path dataDir, - final JsonRpcConfiguration config, - final MetricsSystem metricsSystem, - final Map methods, - final JWTAuthOptions jwtOptions, - final AuthProvider credentialAuthProvider) { - this( - vertx, - dataDir, - config, - metricsSystem, - methods, - Optional.of(jwtOptions), - Optional.of(credentialAuthProvider)); - } - /** * Construct a JsonRpcHttpService handler * From 1dc43e741e658bae83ad7ef0cf72cd21eb285a6c Mon Sep 17 00:00:00 2001 From: Chris Mckay Date: Mon, 11 Feb 2019 12:17:59 +1000 Subject: [PATCH 2/3] [NC-2044] refactor authentication operations to be in a seperate service --- .../ethereum/jsonrpc/JsonRpcHttpService.java | 142 ++----------- .../authentication/AuthenticationService.java | 188 ++++++++++++++++++ .../jsonrpc/JsonRpcHttpServiceLoginTest.java | 4 +- 3 files changed, 209 insertions(+), 125 deletions(-) create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java index 73a3638da2..ade407e6ec 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java @@ -17,7 +17,7 @@ import static java.util.stream.Collectors.toList; import static tech.pegasys.pantheon.util.NetworkUtility.urlForSocketAddress; -import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.TomlAuthOptions; +import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationService; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequestId; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; @@ -37,10 +37,6 @@ import java.net.InetSocketAddress; import java.net.SocketException; import java.nio.file.Path; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Optional; @@ -63,12 +59,6 @@ import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; 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; -import io.vertx.ext.jwt.JWTOptions; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; @@ -91,9 +81,7 @@ public class JsonRpcHttpService { private final Path dataDir; private final LabelledMetric requestTimer; - @VisibleForTesting public final Optional jwtAuthProvider; - @VisibleForTesting public final Optional jwtAuthOptions; - private final Optional credentialAuthProvider; + @VisibleForTesting public final Optional authenticationService; private HttpServer httpServer; @@ -118,49 +106,7 @@ public JsonRpcHttpService( config, metricsSystem, methods, - makeJwtAuthOptions(config), - makeCredentialAuthProvider(vertx, config)); - } - - private static Optional makeJwtAuthOptions(final JsonRpcConfiguration config) { - if (config.isAuthenticationEnabled() && config.getAuthenticationCredentialsFile() != 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(); - } - } - - private static Optional makeCredentialAuthProvider( - final Vertx vertx, final JsonRpcConfiguration config) { - if (config.isAuthenticationEnabled() && config.getAuthenticationCredentialsFile() != null) { - return Optional.of( - new TomlAuthOptions() - .setTomlPath(config.getAuthenticationCredentialsFile()) - .createProvider(vertx)); - } else { - return Optional.empty(); - } + AuthenticationService.create(vertx, config)); } private JsonRpcHttpService( @@ -169,8 +115,7 @@ private JsonRpcHttpService( final JsonRpcConfiguration config, final MetricsSystem metricsSystem, final Map methods, - final Optional jwtOptions, - final Optional credentialAuthProvider) { + final Optional authenticationService) { this.dataDir = dataDir; requestTimer = metricsSystem.createLabelledTimer( @@ -182,9 +127,7 @@ private JsonRpcHttpService( this.config = config; this.vertx = vertx; this.jsonRpcMethods = methods; - this.credentialAuthProvider = credentialAuthProvider; - this.jwtAuthOptions = jwtOptions; - jwtAuthProvider = jwtOptions.map(options -> JWTAuth.create(vertx, options)); + this.authenticationService = authenticationService; } private void validateConfig(final JsonRpcConfiguration config) { @@ -225,11 +168,20 @@ public CompletableFuture start() { .method(HttpMethod.POST) .produces(APPLICATION_JSON) .handler(this::handleJsonRPCRequest); - router - .route("/login") - .method(HttpMethod.POST) - .produces(APPLICATION_JSON) - .handler(this::handleLogin); + + if (authenticationService.isPresent()) { + router + .route("/login") + .method(HttpMethod.POST) + .produces(APPLICATION_JSON) + .handler(authenticationService.get()::handleLogin); + } else { + router + .route("/login") + .method(HttpMethod.POST) + .produces(APPLICATION_JSON) + .handler(AuthenticationService::handleDisabledLogin); + } final CompletableFuture resultFuture = new CompletableFuture<>(); httpServer @@ -372,62 +324,6 @@ private void handleJsonSingleRequest( }); } - private void handleLogin(final RoutingContext routingContext) { - if (!jwtAuthProvider.isPresent() || !credentialAuthProvider.isPresent()) { - routingContext - .response() - .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) - .setStatusMessage("Authentication not enabled") - .end(); - return; - } - - 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 - .get() - .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 token = jwtAuthProvider.get().generateToken(jwtContents, options); - - final JsonObject responseBody = new JsonObject().put("token", token); - final HttpServerResponse response = routingContext.response(); - response.setStatusCode(200); - response.putHeader("Content-Type", APPLICATION_JSON); - response.end(responseBody.encode()); - } - }); - } - private HttpResponseStatus status(final JsonRpcResponse response) { switch (response.getType()) { diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java new file mode 100644 index 0000000000..51da8ae9cf --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java @@ -0,0 +1,188 @@ +/* + * Copyright 2019 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. + */ +package tech.pegasys.pantheon.ethereum.jsonrpc.authentication; + +import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Optional; + +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.PubSecKeyOptions; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.jwt.JWTAuth; +import io.vertx.ext.auth.jwt.JWTAuthOptions; +import io.vertx.ext.jwt.JWTOptions; +import io.vertx.ext.web.RoutingContext; + +/** Provides authentication handlers for use in the http & websocket services */ +public class AuthenticationService { + private final JWTAuth jwtAuthProvider; + @VisibleForTesting public final JWTAuthOptions jwtAuthOptions; + private final AuthProvider credentialAuthProvider; + + private AuthenticationService( + final JWTAuth jwtAuthProvider, + final JWTAuthOptions jwtAuthOptions, + final 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 create( + final Vertx vertx, final JsonRpcConfiguration config) { + final Optional jwtAuthOptions = makeJwtAuthOptions(config); + if (!jwtAuthOptions.isPresent()) { + return Optional.empty(); + } + + final Optional credentialAuthProvider = makeCredentialAuthProvider(vertx, config); + if (!credentialAuthProvider.isPresent()) { + return Optional.empty(); + } + + return Optional.of( + new AuthenticationService( + jwtAuthOptions.map(o -> JWTAuth.create(vertx, o)).get(), + jwtAuthOptions.get(), + credentialAuthProvider.get())); + } + + private static Optional makeJwtAuthOptions(final JsonRpcConfiguration config) { + if (config.isAuthenticationEnabled() && config.getAuthenticationCredentialsFile() != 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(); + } + } + + private static Optional makeCredentialAuthProvider( + final Vertx vertx, final JsonRpcConfiguration config) { + if (config.isAuthenticationEnabled() && config.getAuthenticationCredentialsFile() != null) { + return Optional.of( + new TomlAuthOptions() + .setTomlPath(config.getAuthenticationCredentialsFile()) + .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) { + 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 token = jwtAuthProvider.generateToken(jwtContents, options); + + final JsonObject responseBody = new JsonObject().put("token", token); + final HttpServerResponse response = routingContext.response(); + response.setStatusCode(200); + response.putHeader("Content-Type", "application/json"); + response.end(responseBody.encode()); + } + }); + } + + public JWTAuth getJwtAuthProvider() { + return jwtAuthProvider; + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceLoginTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceLoginTest.java index 47b872d672..4d942f15f8 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceLoginTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceLoginTest.java @@ -123,7 +123,7 @@ public static void initServerAndClient() throws Exception { JSON_RPC_APIS, mock(PrivateTransactionHandler.class))); service = createJsonRpcHttpService(); - jwtAuth = service.jwtAuthProvider.get(); + jwtAuth = service.authenticationService.get().getJwtAuthProvider(); service.start().join(); // Build an OkHttp client. @@ -319,7 +319,7 @@ public void loginDoesntPopulateJWTPayloadWithPassword() final JsonObject respBody = new JsonObject(bodyString); final String token = respBody.getString("token"); assertThat(token).isNotNull(); - final JWT jwt = makeJwt(service.jwtAuthOptions.get()); + final JWT jwt = makeJwt(service.authenticationService.get().jwtAuthOptions); final JsonObject jwtPayload = jwt.decode(token); final String jwtPayloadString = jwtPayload.encode(); From 2be1b5a26a89bdf09ba3884cc8fb1437cafe06cb Mon Sep 17 00:00:00 2001 From: Chris Mckay Date: Mon, 11 Feb 2019 12:34:53 +1000 Subject: [PATCH 3/3] [NC-2044] javadoc fix --- .../ethereum/jsonrpc/authentication/AuthenticationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java index 51da8ae9cf..170227c718 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java @@ -33,7 +33,7 @@ import io.vertx.ext.jwt.JWTOptions; import io.vertx.ext.web.RoutingContext; -/** Provides authentication handlers for use in the http & websocket services */ +/** Provides authentication handlers for use in the http and websocket services */ public class AuthenticationService { private final JWTAuth jwtAuthProvider; @VisibleForTesting public final JWTAuthOptions jwtAuthOptions;