mirror of https://github.com/hyperledger/besu
[NC-2044] jsonrpc authentication login (#753)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
61bdcc879a
commit
562251638e
@ -0,0 +1,287 @@ |
||||
/* |
||||
* 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; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.spy; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import tech.pegasys.pantheon.config.StubGenesisConfigOptions; |
||||
import tech.pegasys.pantheon.ethereum.blockcreation.EthHashMiningCoordinator; |
||||
import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; |
||||
import tech.pegasys.pantheon.ethereum.core.Synchronizer; |
||||
import tech.pegasys.pantheon.ethereum.core.TransactionPool; |
||||
import tech.pegasys.pantheon.ethereum.eth.EthProtocol; |
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; |
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; |
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; |
||||
import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; |
||||
import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; |
||||
import tech.pegasys.pantheon.ethereum.privacy.PrivateTransactionHandler; |
||||
import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.HashSet; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.Set; |
||||
|
||||
import io.vertx.core.AsyncResult; |
||||
import io.vertx.core.Future; |
||||
import io.vertx.core.Handler; |
||||
import io.vertx.core.Vertx; |
||||
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 okhttp3.MediaType; |
||||
import okhttp3.OkHttpClient; |
||||
import okhttp3.Request; |
||||
import okhttp3.RequestBody; |
||||
import okhttp3.Response; |
||||
import org.junit.AfterClass; |
||||
import org.junit.BeforeClass; |
||||
import org.junit.ClassRule; |
||||
import org.junit.Test; |
||||
import org.junit.rules.TemporaryFolder; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.StrictStubs.class) |
||||
public class JsonRpcHttpServiceLoginTest { |
||||
@ClassRule public static final TemporaryFolder folder = new TemporaryFolder(); |
||||
|
||||
private static final Vertx vertx = Vertx.vertx(); |
||||
|
||||
protected static Map<String, JsonRpcMethod> rpcMethods; |
||||
protected static JsonRpcHttpService service; |
||||
protected static OkHttpClient client; |
||||
protected static String baseUrl; |
||||
protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); |
||||
protected static final String CLIENT_VERSION = "TestClientVersion/0.1.0"; |
||||
protected static final int CHAIN_ID = 123; |
||||
protected static P2PNetwork peerDiscoveryMock; |
||||
protected static BlockchainQueries blockchainQueries; |
||||
protected static Synchronizer synchronizer; |
||||
protected static final Collection<RpcApi> JSON_RPC_APIS = |
||||
Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3, RpcApis.ADMIN); |
||||
private static StubAuthProvider stubCredentialProvider; |
||||
private static final JWTAuthOptions jwtOptions = |
||||
new JWTAuthOptions() |
||||
.setPermissionsClaimKey("permissions") |
||||
.addPubSecKey( |
||||
new PubSecKeyOptions() |
||||
.setAlgorithm("HS256") |
||||
.setPublicKey("keyboard cat") |
||||
.setSymmetric(true)); |
||||
|
||||
private static class StubAuthProvider implements AuthProvider { |
||||
private Optional<User> respondUser = Optional.empty(); |
||||
private Optional<String> respondError = Optional.empty(); |
||||
|
||||
@Override |
||||
public void authenticate( |
||||
final JsonObject authInfo, final Handler<AsyncResult<User>> resultHandler) { |
||||
if (respondUser.isPresent()) { |
||||
resultHandler.handle(Future.succeededFuture(respondUser.get())); |
||||
} else if (respondError.isPresent()) { |
||||
resultHandler.handle(Future.failedFuture(respondError.get())); |
||||
} else { |
||||
throw new IllegalStateException("Setup your auth provider stub"); |
||||
} |
||||
} |
||||
|
||||
public void setRespondUser(final User respondUser) { |
||||
this.respondError = Optional.empty(); |
||||
this.respondUser = Optional.of(respondUser); |
||||
} |
||||
|
||||
public void setRespondError(final String respondError) { |
||||
this.respondUser = Optional.empty(); |
||||
this.respondError = Optional.of(respondError); |
||||
} |
||||
} |
||||
|
||||
@BeforeClass |
||||
public static void initServerAndClient() throws Exception { |
||||
stubCredentialProvider = new StubAuthProvider(); |
||||
peerDiscoveryMock = mock(P2PNetwork.class); |
||||
blockchainQueries = mock(BlockchainQueries.class); |
||||
synchronizer = mock(Synchronizer.class); |
||||
|
||||
final Set<Capability> supportedCapabilities = new HashSet<>(); |
||||
supportedCapabilities.add(EthProtocol.ETH62); |
||||
supportedCapabilities.add(EthProtocol.ETH63); |
||||
|
||||
rpcMethods = |
||||
spy( |
||||
new JsonRpcMethodsFactory() |
||||
.methods( |
||||
CLIENT_VERSION, |
||||
peerDiscoveryMock, |
||||
blockchainQueries, |
||||
synchronizer, |
||||
MainnetProtocolSchedule.fromConfig( |
||||
new StubGenesisConfigOptions().constantinopleBlock(0).chainId(CHAIN_ID), |
||||
PrivacyParameters.noPrivacy()), |
||||
mock(FilterManager.class), |
||||
mock(TransactionPool.class), |
||||
mock(EthHashMiningCoordinator.class), |
||||
new NoOpMetricsSystem(), |
||||
supportedCapabilities, |
||||
mock(AccountWhitelistController.class), |
||||
JSON_RPC_APIS, |
||||
mock(PrivateTransactionHandler.class))); |
||||
service = createJsonRpcHttpService(); |
||||
service.start().join(); |
||||
|
||||
// Build an OkHttp client.
|
||||
client = new OkHttpClient(); |
||||
baseUrl = service.url(); |
||||
} |
||||
|
||||
private static JsonRpcHttpService createJsonRpcHttpService() throws Exception { |
||||
return new JsonRpcHttpService( |
||||
vertx, |
||||
folder.newFolder().toPath(), |
||||
createJsonRpcConfig(), |
||||
new NoOpMetricsSystem(), |
||||
rpcMethods, |
||||
jwtOptions, |
||||
stubCredentialProvider); |
||||
} |
||||
|
||||
private static JsonRpcConfiguration createJsonRpcConfig() { |
||||
final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); |
||||
config.setPort(0); |
||||
config.setHostsWhitelist(Collections.singletonList("*")); |
||||
return config; |
||||
} |
||||
|
||||
/** Tears down the HTTP server. */ |
||||
@AfterClass |
||||
public static void shutdownServer() { |
||||
service.stop().join(); |
||||
} |
||||
|
||||
@Test |
||||
public void loginWithBadCredentials() throws IOException { |
||||
stubCredentialProvider.setRespondError("Invalid password"); |
||||
|
||||
final RequestBody body = |
||||
RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pass\"}"); |
||||
final Request request = new Request.Builder().post(body).url(baseUrl + "/login").build(); |
||||
try (final Response resp = client.newCall(request).execute()) { |
||||
assertThat(resp.code()).isEqualTo(401); |
||||
assertThat(resp.message()).isEqualTo("Unauthorized"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void loginWithGoodCredentials() throws IOException { |
||||
final User mockUser = mock(User.class); |
||||
stubCredentialProvider.setRespondUser(mockUser); |
||||
when(mockUser.principal()).thenReturn(new JsonObject()); |
||||
|
||||
final RequestBody body = |
||||
RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pass\"}"); |
||||
final Request request = new Request.Builder().post(body).url(baseUrl + "/login").build(); |
||||
try (final Response resp = client.newCall(request).execute()) { |
||||
assertThat(resp.code()).isEqualTo(200); |
||||
assertThat(resp.message()).isEqualTo("OK"); |
||||
assertThat(resp.body().contentType()).isNotNull(); |
||||
assertThat(resp.body().contentType().type()).isEqualTo("application"); |
||||
assertThat(resp.body().contentType().subtype()).isEqualTo("json"); |
||||
final String bodyString = resp.body().string(); |
||||
assertThat(bodyString).isNotNull(); |
||||
assertThat(bodyString).isNotBlank(); |
||||
|
||||
final JsonObject respBody = new JsonObject(bodyString); |
||||
final String token = respBody.getString("token"); |
||||
assertThat(token).isNotNull(); |
||||
|
||||
final JWTAuth auth = JWTAuth.create(vertx, jwtOptions); |
||||
|
||||
auth.authenticate( |
||||
new JsonObject().put("jwt", token), |
||||
(r) -> { |
||||
assertThat(r.succeeded()).isTrue(); |
||||
final User user = r.result(); |
||||
user.isAuthorized( |
||||
"noauths", |
||||
(authed) -> { |
||||
assertThat(authed.succeeded()).isTrue(); |
||||
assertThat(authed.result()).isFalse(); |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void loginWithGoodCredentialsAndPermissions() throws IOException { |
||||
final User mockUser = mock(User.class); |
||||
stubCredentialProvider.setRespondUser(mockUser); |
||||
when(mockUser.principal()) |
||||
.thenReturn( |
||||
new JsonObject() |
||||
.put("permissions", new JsonArray(Collections.singletonList("fakePermission")))); |
||||
|
||||
final RequestBody body = |
||||
RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pass\"}"); |
||||
final Request request = new Request.Builder().post(body).url(baseUrl + "/login").build(); |
||||
try (final Response resp = client.newCall(request).execute()) { |
||||
assertThat(resp.code()).isEqualTo(200); |
||||
assertThat(resp.message()).isEqualTo("OK"); |
||||
assertThat(resp.body().contentType()).isNotNull(); |
||||
assertThat(resp.body().contentType().type()).isEqualTo("application"); |
||||
assertThat(resp.body().contentType().subtype()).isEqualTo("json"); |
||||
final String bodyString = resp.body().string(); |
||||
assertThat(bodyString).isNotNull(); |
||||
assertThat(bodyString).isNotBlank(); |
||||
|
||||
final JsonObject respBody = new JsonObject(bodyString); |
||||
final String token = respBody.getString("token"); |
||||
assertThat(token).isNotNull(); |
||||
|
||||
final JWTAuth auth = JWTAuth.create(vertx, jwtOptions); |
||||
|
||||
auth.authenticate( |
||||
new JsonObject().put("jwt", token), |
||||
(r) -> { |
||||
assertThat(r.succeeded()).isTrue(); |
||||
final User user = r.result(); |
||||
user.isAuthorized( |
||||
"noauths", |
||||
(authed) -> { |
||||
assertThat(authed.succeeded()).isTrue(); |
||||
assertThat(authed.result()).isFalse(); |
||||
}); |
||||
user.isAuthorized( |
||||
"fakePermission", |
||||
(authed) -> { |
||||
assertThat(authed.succeeded()).isTrue(); |
||||
assertThat(authed.result()).isTrue(); |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue