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 3 commits
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
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,42 +81,10 @@ 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;

/**
* 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<String, JsonRpcMethod> methods,
final JWTAuthOptions jwtOptions,
final AuthProvider credentialAuthProvider) {
this(
vertx,
dataDir,
config,
metricsSystem,
methods,
Optional.of(jwtOptions),
Optional.of(credentialAuthProvider));
}

/**
* Construct a JsonRpcHttpService handler
*
Expand All @@ -148,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 @@ -199,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 @@ -212,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 @@ -255,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 @@ -402,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
Loading