Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

[NC-2044] refactor json rpc authentication to be provided as a service #825

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[NC-2044] refactor authentication operations to be in a seperate service
  • Loading branch information
Errorific committed Feb 11, 2019
commit 1dc43e741e658bae83ad7ef0cf72cd21eb285a6c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -91,9 +81,7 @@ public class JsonRpcHttpService {
private final Path dataDir;
private final LabelledMetric<OperationTimer> requestTimer;

@VisibleForTesting public final Optional<JWTAuth> jwtAuthProvider;
@VisibleForTesting public final Optional<JWTAuthOptions> jwtAuthOptions;
private final Optional<AuthProvider> credentialAuthProvider;
@VisibleForTesting public final Optional<AuthenticationService> authenticationService;

private HttpServer httpServer;

Expand All @@ -118,49 +106,7 @@ public JsonRpcHttpService(
config,
metricsSystem,
methods,
makeJwtAuthOptions(config),
makeCredentialAuthProvider(vertx, config));
}

private static Optional<JWTAuthOptions> 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<AuthProvider> 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(
Expand All @@ -169,8 +115,7 @@ private JsonRpcHttpService(
final JsonRpcConfiguration config,
final MetricsSystem metricsSystem,
final Map<String, JsonRpcMethod> methods,
final Optional<JWTAuthOptions> jwtOptions,
final Optional<AuthProvider> credentialAuthProvider) {
final Optional<AuthenticationService> authenticationService) {
this.dataDir = dataDir;
requestTimer =
metricsSystem.createLabelledTimer(
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AuthenticationService> create(
final Vertx vertx, final JsonRpcConfiguration config) {
final Optional<JWTAuthOptions> jwtAuthOptions = makeJwtAuthOptions(config);
if (!jwtAuthOptions.isPresent()) {
return Optional.empty();
}

final Optional<AuthProvider> 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<JWTAuthOptions> 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<AuthProvider> 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;
}
}
Loading