mirror of https://github.com/hyperledger/besu
jwt auth on websockets (#4039)
* integration test covering websocket subscription without auth * uses authenticated user on websocket handler when auth enabled and successful * sonarlint fixes and copyright correction * moved test specific class to test sources Signed-off-by: Justin Florentine <justin+github@florentine.us> Co-authored-by: garyschulte <garyschulte@gmail.com>pull/4044/head
parent
90f891b78c
commit
043191a407
@ -0,0 +1,97 @@ |
|||||||
|
/* |
||||||
|
* 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.internal.response; |
||||||
|
|
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonGetter; |
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder; |
||||||
|
import com.fasterxml.jackson.annotation.JsonSetter; |
||||||
|
|
||||||
|
@JsonPropertyOrder({"jsonrpc", "id", "result"}) |
||||||
|
public class MutableJsonRpcSuccessResponse { |
||||||
|
|
||||||
|
private Object id; |
||||||
|
private Object result; |
||||||
|
private Object version; |
||||||
|
|
||||||
|
public MutableJsonRpcSuccessResponse() { |
||||||
|
this.id = null; |
||||||
|
this.result = null; |
||||||
|
} |
||||||
|
|
||||||
|
public MutableJsonRpcSuccessResponse(final Object id, final Object result) { |
||||||
|
this.id = id; |
||||||
|
this.result = result; |
||||||
|
} |
||||||
|
|
||||||
|
public MutableJsonRpcSuccessResponse(final Object id) { |
||||||
|
this.id = id; |
||||||
|
this.result = "Success"; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonGetter("id") |
||||||
|
public Object getId() { |
||||||
|
return id; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonGetter("result") |
||||||
|
public Object getResult() { |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonSetter("id") |
||||||
|
public void setId(final Object id) { |
||||||
|
this.id = id; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonSetter("result") |
||||||
|
public void setResult(final Object result) { |
||||||
|
this.result = result; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonGetter("jsonrpc") |
||||||
|
public Object getVersion() { |
||||||
|
return version; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonSetter("jsonrpc") |
||||||
|
public void setVersion(final Object version) { |
||||||
|
this.version = version; |
||||||
|
} |
||||||
|
|
||||||
|
@JsonIgnore |
||||||
|
public JsonRpcResponseType getType() { |
||||||
|
return JsonRpcResponseType.SUCCESS; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(final Object o) { |
||||||
|
if (this == o) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (o == null || getClass() != o.getClass()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
final MutableJsonRpcSuccessResponse that = (MutableJsonRpcSuccessResponse) o; |
||||||
|
return Objects.equals(id, that.id) && Objects.equals(result, that.result); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return Objects.hash(id, result); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,297 @@ |
|||||||
|
/* |
||||||
|
* 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.websocket; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.fail; |
||||||
|
|
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcService; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService; |
||||||
|
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.HealthService.HealthCheck; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService.ParamSource; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.MutableJsonRpcSuccessResponse; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods.WebSocketMethodsFactory; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; |
||||||
|
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; |
||||||
|
import org.hyperledger.besu.nat.NatService; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.nio.file.Files; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import io.vertx.core.Vertx; |
||||||
|
import io.vertx.core.http.HttpClient; |
||||||
|
import io.vertx.core.http.HttpClientOptions; |
||||||
|
import io.vertx.core.http.WebSocket; |
||||||
|
import io.vertx.core.http.WebSocketConnectOptions; |
||||||
|
import io.vertx.core.json.Json; |
||||||
|
import io.vertx.ext.unit.Async; |
||||||
|
import io.vertx.ext.unit.TestContext; |
||||||
|
import io.vertx.ext.unit.junit.VertxUnitRunner; |
||||||
|
import org.junit.After; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.ClassRule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.TemporaryFolder; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
|
||||||
|
@RunWith(VertxUnitRunner.class) |
||||||
|
public class JsonRpcJWTTest { |
||||||
|
|
||||||
|
@ClassRule public static final TemporaryFolder folder = new TemporaryFolder(); |
||||||
|
public static final String HOSTNAME = "127.0.0.1"; |
||||||
|
|
||||||
|
protected static Vertx vertx; |
||||||
|
|
||||||
|
private final JsonRpcConfiguration jsonRpcConfiguration = |
||||||
|
JsonRpcConfiguration.createEngineDefault(); |
||||||
|
private HttpClient httpClient; |
||||||
|
private Optional<AuthenticationService> jwtAuth; |
||||||
|
private HealthService healthy; |
||||||
|
private EthScheduler scheduler; |
||||||
|
private Path bufferDir; |
||||||
|
private Map<String, JsonRpcMethod> websocketMethods; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void initServerAndClient() { |
||||||
|
jsonRpcConfiguration.setPort(0); |
||||||
|
jsonRpcConfiguration.setHostsAllowlist(List.of("*")); |
||||||
|
try { |
||||||
|
jsonRpcConfiguration.setAuthenticationPublicKeyFile( |
||||||
|
new File(this.getClass().getResource("jwt.hex").toURI())); |
||||||
|
} catch (URISyntaxException e) { |
||||||
|
fail("couldn't load jwt key from jwt.hex in classpath"); |
||||||
|
} |
||||||
|
vertx = Vertx.vertx(); |
||||||
|
|
||||||
|
websocketMethods = |
||||||
|
new WebSocketMethodsFactory( |
||||||
|
new SubscriptionManager(new NoOpMetricsSystem()), new HashMap<>()) |
||||||
|
.methods(); |
||||||
|
|
||||||
|
bufferDir = null; |
||||||
|
try { |
||||||
|
bufferDir = Files.createTempDirectory("JsonRpcJWTTest").toAbsolutePath(); |
||||||
|
} catch (IOException e) { |
||||||
|
fail("can't create tempdir", e); |
||||||
|
} |
||||||
|
|
||||||
|
jwtAuth = |
||||||
|
Optional.of( |
||||||
|
new EngineAuthService( |
||||||
|
vertx, |
||||||
|
Optional.ofNullable(jsonRpcConfiguration.getAuthenticationPublicKeyFile()), |
||||||
|
bufferDir)); |
||||||
|
|
||||||
|
healthy = |
||||||
|
new HealthService( |
||||||
|
new HealthCheck() { |
||||||
|
@Override |
||||||
|
public boolean isHealthy(final ParamSource paramSource) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
scheduler = new EthScheduler(1, 1, 1, new NoOpMetricsSystem()); |
||||||
|
} |
||||||
|
|
||||||
|
@After |
||||||
|
public void after() {} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void unauthenticatedWebsocketAllowedWithoutJWTAuth(final TestContext context) { |
||||||
|
|
||||||
|
JsonRpcService jsonRpcService = |
||||||
|
new JsonRpcService( |
||||||
|
vertx, |
||||||
|
bufferDir, |
||||||
|
jsonRpcConfiguration, |
||||||
|
new NoOpMetricsSystem(), |
||||||
|
new NatService(Optional.empty(), true), |
||||||
|
websocketMethods, |
||||||
|
Optional.empty(), |
||||||
|
scheduler, |
||||||
|
Optional.empty(), |
||||||
|
healthy, |
||||||
|
healthy); |
||||||
|
|
||||||
|
jsonRpcService.start().join(); |
||||||
|
|
||||||
|
final InetSocketAddress inetSocketAddress = jsonRpcService.socketAddress(); |
||||||
|
int listenPort = inetSocketAddress.getPort(); |
||||||
|
|
||||||
|
final HttpClientOptions httpClientOptions = |
||||||
|
new HttpClientOptions().setDefaultHost(HOSTNAME).setDefaultPort(listenPort); |
||||||
|
|
||||||
|
httpClient = vertx.createHttpClient(httpClientOptions); |
||||||
|
|
||||||
|
WebSocketConnectOptions wsOpts = new WebSocketConnectOptions(); |
||||||
|
wsOpts.setPort(listenPort); |
||||||
|
wsOpts.setHost(HOSTNAME); |
||||||
|
wsOpts.setURI("/"); |
||||||
|
|
||||||
|
final Async async = context.async(); |
||||||
|
httpClient.webSocket( |
||||||
|
wsOpts, |
||||||
|
connected -> { |
||||||
|
if (connected.failed()) { |
||||||
|
connected.cause().printStackTrace(); |
||||||
|
} |
||||||
|
assertThat(connected.succeeded()).isTrue(); |
||||||
|
WebSocket ws = connected.result(); |
||||||
|
|
||||||
|
JsonRpcRequest req = |
||||||
|
new JsonRpcRequest("2.0", "eth_subscribe", List.of("syncing").toArray()); |
||||||
|
ws.frameHandler( |
||||||
|
resp -> { |
||||||
|
assertThat(resp.isText()).isTrue(); |
||||||
|
MutableJsonRpcSuccessResponse messageReply = |
||||||
|
Json.decodeValue(resp.textData(), MutableJsonRpcSuccessResponse.class); |
||||||
|
assertThat(messageReply.getResult()).isEqualTo("0x1"); |
||||||
|
async.complete(); |
||||||
|
}); |
||||||
|
ws.writeTextMessage(Json.encode(req)); |
||||||
|
}); |
||||||
|
|
||||||
|
async.awaitSuccess(10000); |
||||||
|
jsonRpcService.stop(); |
||||||
|
httpClient.close(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void httpRequestWithDefaultHeaderAndValidJWTIsAccepted(final TestContext context) { |
||||||
|
|
||||||
|
JsonRpcService jsonRpcService = |
||||||
|
new JsonRpcService( |
||||||
|
vertx, |
||||||
|
bufferDir, |
||||||
|
jsonRpcConfiguration, |
||||||
|
new NoOpMetricsSystem(), |
||||||
|
new NatService(Optional.empty(), true), |
||||||
|
websocketMethods, |
||||||
|
Optional.empty(), |
||||||
|
scheduler, |
||||||
|
jwtAuth, |
||||||
|
healthy, |
||||||
|
healthy); |
||||||
|
|
||||||
|
jsonRpcService.start().join(); |
||||||
|
|
||||||
|
final InetSocketAddress inetSocketAddress = jsonRpcService.socketAddress(); |
||||||
|
int listenPort = inetSocketAddress.getPort(); |
||||||
|
|
||||||
|
final HttpClientOptions httpClientOptions = |
||||||
|
new HttpClientOptions().setDefaultHost(HOSTNAME).setDefaultPort(listenPort); |
||||||
|
|
||||||
|
httpClient = vertx.createHttpClient(httpClientOptions); |
||||||
|
|
||||||
|
WebSocketConnectOptions wsOpts = new WebSocketConnectOptions(); |
||||||
|
wsOpts.setPort(listenPort); |
||||||
|
wsOpts.setHost(HOSTNAME); |
||||||
|
wsOpts.setURI("/"); |
||||||
|
wsOpts.addHeader( |
||||||
|
"Authorization", "Bearer " + ((EngineAuthService) jwtAuth.get()).createToken()); |
||||||
|
|
||||||
|
final Async async = context.async(); |
||||||
|
httpClient.webSocket( |
||||||
|
wsOpts, |
||||||
|
connected -> { |
||||||
|
if (connected.failed()) { |
||||||
|
connected.cause().printStackTrace(); |
||||||
|
} |
||||||
|
assertThat(connected.succeeded()).isTrue(); |
||||||
|
WebSocket ws = connected.result(); |
||||||
|
JsonRpcRequest req = |
||||||
|
new JsonRpcRequest("1", "eth_subscribe", List.of("syncing").toArray()); |
||||||
|
ws.frameHandler( |
||||||
|
resp -> { |
||||||
|
assertThat(resp.isText()).isTrue(); |
||||||
|
System.out.println(resp.textData()); |
||||||
|
assertThat(resp.textData()).doesNotContain("error"); |
||||||
|
MutableJsonRpcSuccessResponse messageReply = |
||||||
|
Json.decodeValue(resp.textData(), MutableJsonRpcSuccessResponse.class); |
||||||
|
assertThat(messageReply.getResult()).isEqualTo("0x1"); |
||||||
|
async.complete(); |
||||||
|
}); |
||||||
|
ws.writeTextMessage(Json.encode(req)); |
||||||
|
}); |
||||||
|
|
||||||
|
async.awaitSuccess(10000); |
||||||
|
jsonRpcService.stop(); |
||||||
|
httpClient.close(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void httpRequestWithDefaultHeaderAndInvalidJWTIsDenied(final TestContext context) { |
||||||
|
|
||||||
|
JsonRpcService jsonRpcService = |
||||||
|
new JsonRpcService( |
||||||
|
vertx, |
||||||
|
bufferDir, |
||||||
|
jsonRpcConfiguration, |
||||||
|
new NoOpMetricsSystem(), |
||||||
|
new NatService(Optional.empty(), true), |
||||||
|
websocketMethods, |
||||||
|
Optional.empty(), |
||||||
|
scheduler, |
||||||
|
jwtAuth, |
||||||
|
healthy, |
||||||
|
healthy); |
||||||
|
|
||||||
|
jsonRpcService.start().join(); |
||||||
|
|
||||||
|
final InetSocketAddress inetSocketAddress = jsonRpcService.socketAddress(); |
||||||
|
int listenPort = inetSocketAddress.getPort(); |
||||||
|
|
||||||
|
final HttpClientOptions httpClientOptions = |
||||||
|
new HttpClientOptions().setDefaultHost(HOSTNAME).setDefaultPort(listenPort); |
||||||
|
|
||||||
|
httpClient = vertx.createHttpClient(httpClientOptions); |
||||||
|
|
||||||
|
WebSocketConnectOptions wsOpts = new WebSocketConnectOptions(); |
||||||
|
wsOpts.setPort(listenPort); |
||||||
|
wsOpts.setHost(HOSTNAME); |
||||||
|
wsOpts.setURI("/"); |
||||||
|
wsOpts.addHeader("Authorization", "Bearer totallyunparseablenonsense"); |
||||||
|
|
||||||
|
final Async async = context.async(); |
||||||
|
httpClient.webSocket( |
||||||
|
wsOpts, |
||||||
|
connected -> { |
||||||
|
if (connected.failed()) { |
||||||
|
connected.cause().printStackTrace(); |
||||||
|
} |
||||||
|
assertThat(connected.succeeded()).isFalse(); |
||||||
|
async.complete(); |
||||||
|
}); |
||||||
|
|
||||||
|
async.awaitSuccess(10000); |
||||||
|
jsonRpcService.stop(); |
||||||
|
httpClient.close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
9465710175a93a3f2d67b0cb98d92d44ead4d1126a12233571884de92a8edc76 |
Loading…
Reference in new issue