Skip to content

Commit

Permalink
Extract json-rpc HTTP authentication to a handler [hyperledger#535]
Browse files Browse the repository at this point in the history
Signed-off-by: Diego López León <[email protected]>
  • Loading branch information
diega committed Apr 5, 2022
1 parent 92de455 commit 8c2fe3e
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright contributors to 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.handlers;

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.context.ContextKey;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;

import java.util.Collection;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.Json;
import io.vertx.ext.web.RoutingContext;

public class AuthenticationHandler {

public static Handler<RoutingContext> handler(
final AuthenticationService authenticationService, final Collection<String> noAuthRpcApis) {
return ctx -> {
// first check token if authentication is required
final String token = getAuthToken(ctx);
if (token == null && noAuthRpcApis.isEmpty()) {
// no auth token when auth required
handleJsonRpcUnauthorizedError(ctx);
} else {
authenticationService.authenticate(
token, user -> ctx.put(ContextKey.AUTHENTICATED_USER.name(), user));
ctx.next();
}
};
}

private static String getAuthToken(final RoutingContext routingContext) {
return AuthenticationUtils.getJwtTokenFromAuthorizationHeaderValue(
routingContext.request().getHeader("Authorization"));
}

private static void handleJsonRpcUnauthorizedError(final RoutingContext routingContext) {
final HttpServerResponse response = routingContext.response();
if (!response.closed()) {
response
.setStatusCode(HttpResponseStatus.UNAUTHORIZED.code())
.end(Json.encode(new JsonRpcErrorResponse(null, JsonRpcError.UNAUTHORIZED)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
*/
package org.hyperledger.besu.ethereum.api.handlers;

import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;

import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -42,4 +44,9 @@ public static Handler<RoutingContext> timeout(
.collect(Collectors.toMap(String::new, ignored -> globalOptions)),
decodeJSON));
}

public static Handler<RoutingContext> authentication(
final AuthenticationService authenticationService, final Collection<String> noAuthRpcApis) {
return AuthenticationHandler.handler(authenticationService, noAuthRpcApis);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
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;
Expand Down Expand Up @@ -103,6 +102,7 @@
import io.vertx.core.net.PfxOptions;
import io.vertx.core.net.SocketAddress;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
Expand Down Expand Up @@ -344,14 +344,19 @@ private Router buildRouter() {
.route(HealthService.READINESS_PATH)
.method(HttpMethod.GET)
.handler(readinessService::handleRequest);
router
.route("/")
.method(HttpMethod.POST)
.produces(APPLICATION_JSON)
.handler(
HandlerFactory.timeout(
new TimeoutOptions(config.getHttpTimeoutSec()), rpcMethods, true))
.handler(this::handleJsonRPCRequest);
Route mainRoute =
router
.route("/")
.method(HttpMethod.POST)
.produces(APPLICATION_JSON)
.handler(
HandlerFactory.timeout(
new TimeoutOptions(config.getHttpTimeoutSec()), rpcMethods, true));
if (authenticationService.isPresent()) {
mainRoute.handler(
HandlerFactory.authentication(authenticationService.get(), config.getNoAuthRpcApis()));
}
mainRoute.handler(this::handleJsonRPCRequest);

if (authenticationService.isPresent()) {
router
Expand Down Expand Up @@ -493,11 +498,6 @@ private Handler<RoutingContext> checkAllowlistHostHeader() {
};
}

private String getAuthToken(final RoutingContext routingContext) {
return AuthenticationUtils.getJwtTokenFromAuthorizationHeaderValue(
routingContext.request().getHeader("Authorization"));
}

private Optional<String> getAndValidateHostHeader(final RoutingContext event) {
String hostname =
event.request().getHeader(HttpHeaders.HOST) != null
Expand Down Expand Up @@ -563,47 +563,26 @@ private String getScheme() {
}

private void handleJsonRPCRequest(final RoutingContext routingContext) {
// first check token if authentication is required
final String token = getAuthToken(routingContext);
// we check the no auth api methods actually match what's in the request later on
if (authenticationService.isPresent() && token == null && config.getNoAuthRpcApis().isEmpty()) {
// no auth token when auth required
handleJsonRpcUnauthorizedError(routingContext, null, JsonRpcError.UNAUTHORIZED);
} else {
Optional<User> user =
ContextKey.AUTHENTICATED_USER.extractFrom(routingContext, Optional::empty);
try {
// 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());
}
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));
handleJsonSingleRequest(routingContext, requestBodyJsonObject, user);
} else {
final JsonArray array = new JsonArray(json);
if (array.size() < 1) {
handleJsonRpcError(routingContext, null, INVALID_REQUEST);
return;
}
} catch (final DecodeException | NullPointerException ex) {
handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR);
handleJsonBatchRequest(routingContext, array, user);
}
} catch (final DecodeException | NullPointerException ex) {
handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR);
}
}

Expand Down Expand Up @@ -818,16 +797,6 @@ private void handleJsonRpcError(
}
}

private void handleJsonRpcUnauthorizedError(
final RoutingContext routingContext, final Object id, final JsonRpcError error) {
final HttpServerResponse response = routingContext.response();
if (!response.closed()) {
response
.setStatusCode(HttpResponseStatus.UNAUTHORIZED.code())
.end(Json.encode(new JsonRpcErrorResponse(id, error)));
}
}

private JsonRpcResponse errorResponse(final Object id, final JsonRpcError error) {
return new JsonRpcErrorResponse(id, error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import io.vertx.ext.web.RoutingContext;

public enum ContextKey {
REQUEST_BODY_AS_JSON_OBJECT;
REQUEST_BODY_AS_JSON_OBJECT,
AUTHENTICATED_USER;

public <T> T extractFrom(final RoutingContext ctx, final Supplier<T> defaultSupplier) {
final T value = ctx.get(this.name());
Expand Down

0 comments on commit 8c2fe3e

Please sign in to comment.