diff --git a/besu/src/main/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidator.java b/besu/src/main/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidator.java index 53712d8be4..40a9155799 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidator.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidator.java @@ -14,6 +14,8 @@ */ package org.hyperledger.besu.cli.custom; +import static org.hyperledger.besu.ethereum.api.jsonrpc.authentication.TomlAuth.PRIVACY_PUBLIC_KEY; + import org.hyperledger.besu.ethereum.permissioning.TomlConfigFileParser; import java.io.File; @@ -97,7 +99,19 @@ public class RpcAuthFileValidator { .dottedKeySet() .parallelStream() .filter(keySet -> !keySet.contains("password")) - .allMatch(dottedKey -> verifyArray(dottedKey, tomlParseResult)); + .allMatch(dottedKey -> verifyEntry(dottedKey, tomlParseResult)); + } + + private static boolean verifyEntry(final String key, final TomlParseResult tomlParseResult) { + if (key.endsWith(PRIVACY_PUBLIC_KEY)) { + return verifyString(key, tomlParseResult); + } else { + return verifyArray(key, tomlParseResult); + } + } + + private static boolean verifyString(final String key, final TomlParseResult tomlParseResult) { + return tomlParseResult.isString(key) && !tomlParseResult.getString(key, () -> "").isEmpty(); } private static boolean verifyArray(final String key, final TomlParseResult tomlParseResult) { diff --git a/besu/src/test/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidatorTest.java b/besu/src/test/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidatorTest.java index d0298d6173..714086245a 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidatorTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidatorTest.java @@ -27,11 +27,16 @@ import picocli.CommandLine.ParameterException; @RunWith(MockitoJUnitRunner.StrictStubs.class) public class RpcAuthFileValidatorTest { - private static final String CORRECT_TOML = "/auth_correct.toml"; - private static final String DUPLICATE_USER_TOML = "/auth_duplicate_user.toml"; - private static final String INVALID_TOML = "/auth_invalid.toml"; - private static final String INVALID_VALUE_TOML = "/auth_invalid_value.toml"; - private static final String NO_PASSWORD_TOML = "/auth_no_password.toml"; + private static final String CORRECT_TOML = "/rpcauth/auth_correct.toml"; + private static final String DUPLICATE_USER_TOML = "/rpcauth/auth_duplicate_user.toml"; + private static final String INVALID_TOML = "/rpcauth/auth_invalid.toml"; + private static final String INVALID_GROUPS_VALUE_TOML = "/rpcauth/auth_invalid_groups_value.toml"; + private static final String INVALID_PERMISSIONS_VALUE_TOML = + "/rpcauth/auth_invalid_permissions_value.toml"; + private static final String INVALID_PRIVACY_PUBLIC_KEY_VALUE_TOML = + "/rpcauth/auth_invalid_privacy_public_key_value.toml"; + private static final String NO_PASSWORD_TOML = "/rpcauth/auth_no_password.toml"; + @Mock CommandLine commandLine; @Test @@ -67,10 +72,31 @@ public class RpcAuthFileValidatorTest { } @Test - public void shouldFailWhenInvalidKeyValue() { + public void shouldFailWhenInvalidGroupsKeyValue() { + assertThatThrownBy( + () -> + RpcAuthFileValidator.validate( + commandLine, getFilePath(INVALID_GROUPS_VALUE_TOML), "HTTP")) + .isInstanceOf(ParameterException.class) + .hasMessage("RPC authentication configuration file contains invalid values."); + } + + @Test + public void shouldFailWhenInvalidPermissionsKeyValue() { assertThatThrownBy( () -> - RpcAuthFileValidator.validate(commandLine, getFilePath(INVALID_VALUE_TOML), "HTTP")) + RpcAuthFileValidator.validate( + commandLine, getFilePath(INVALID_PERMISSIONS_VALUE_TOML), "HTTP")) + .isInstanceOf(ParameterException.class) + .hasMessage("RPC authentication configuration file contains invalid values."); + } + + @Test + public void shouldFailWhenInvalidEmptyPrivacyPublicKeyValue() { + assertThatThrownBy( + () -> + RpcAuthFileValidator.validate( + commandLine, getFilePath(INVALID_PRIVACY_PUBLIC_KEY_VALUE_TOML), "HTTP")) .isInstanceOf(ParameterException.class) .hasMessage("RPC authentication configuration file contains invalid values."); } diff --git a/besu/src/test/resources/auth_correct.toml b/besu/src/test/resources/rpcauth/auth_correct.toml similarity index 82% rename from besu/src/test/resources/auth_correct.toml rename to besu/src/test/resources/rpcauth/auth_correct.toml index 8159250076..2c3b1a8c45 100644 --- a/besu/src/test/resources/auth_correct.toml +++ b/besu/src/test/resources/rpcauth/auth_correct.toml @@ -3,6 +3,7 @@ password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" groups = ["admin"] permissions = ["eth:*", "perm:*"] roles = ["net"] +privacyPublicKey="A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=" [Users.userB] password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" diff --git a/besu/src/test/resources/auth_duplicate_user.toml b/besu/src/test/resources/rpcauth/auth_duplicate_user.toml similarity index 100% rename from besu/src/test/resources/auth_duplicate_user.toml rename to besu/src/test/resources/rpcauth/auth_duplicate_user.toml diff --git a/besu/src/test/resources/auth_invalid.toml b/besu/src/test/resources/rpcauth/auth_invalid.toml similarity index 100% rename from besu/src/test/resources/auth_invalid.toml rename to besu/src/test/resources/rpcauth/auth_invalid.toml diff --git a/besu/src/test/resources/auth_invalid_value.toml b/besu/src/test/resources/rpcauth/auth_invalid_groups_value.toml similarity index 100% rename from besu/src/test/resources/auth_invalid_value.toml rename to besu/src/test/resources/rpcauth/auth_invalid_groups_value.toml diff --git a/besu/src/test/resources/rpcauth/auth_invalid_permissions_value.toml b/besu/src/test/resources/rpcauth/auth_invalid_permissions_value.toml new file mode 100644 index 0000000000..25a8befd3b --- /dev/null +++ b/besu/src/test/resources/rpcauth/auth_invalid_permissions_value.toml @@ -0,0 +1,21 @@ +[Users.userA] +password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" +groups = ["admin"] +# This line is invalid - should be an array +permissions = "eth:*" +roles = ["net"] + +[Users.userB] +password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" +groups = ["admin"] +permissions = ["eth:*", "perm:*"] +roles = ["net"] + +[Groups.admins] +roles = ["admin"] + +[Roles.admin] +permissions = ["admin:*"] + +[Roles.net] +permissions = ["net:*"] \ No newline at end of file diff --git a/besu/src/test/resources/rpcauth/auth_invalid_privacy_public_key_value.toml b/besu/src/test/resources/rpcauth/auth_invalid_privacy_public_key_value.toml new file mode 100644 index 0000000000..9356db4e22 --- /dev/null +++ b/besu/src/test/resources/rpcauth/auth_invalid_privacy_public_key_value.toml @@ -0,0 +1,22 @@ +[Users.userA] +password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" +groups = ["admin"] +permissions = ["eth:*", "perm:*"] +roles = ["net"] +# This line is invalid - should be a non-empty value +privacyPublicKey = "" + +[Users.userB] +password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" +groups = ["admin"] +permissions = ["eth:*", "perm:*"] +roles = ["net"] + +[Groups.admins] +roles = ["admin"] + +[Roles.admin] +permissions = ["admin:*"] + +[Roles.net] +permissions = ["net:*"] \ No newline at end of file diff --git a/besu/src/test/resources/auth_no_password.toml b/besu/src/test/resources/rpcauth/auth_no_password.toml similarity index 100% rename from besu/src/test/resources/auth_no_password.toml rename to besu/src/test/resources/rpcauth/auth_no_password.toml diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java index 7e132b8e58..ad10010fa1 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java @@ -186,6 +186,11 @@ public class AuthenticationService { new JsonObject() .put("permissions", user.principal().getValue("permissions")) .put("username", user.principal().getValue("username")); + final String privacyPublicKey = user.principal().getString("privacyPublicKey"); + if (privacyPublicKey != null) { + jwtContents.put("privacyPublicKey", privacyPublicKey); + } + final String token = jwtAuthProvider.generateToken(jwtContents, options); final JsonObject responseBody = new JsonObject().put("token", token); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java index 0238b69a3f..cac92cf333 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java @@ -82,7 +82,7 @@ public class AuthenticationUtils { (r) -> { if (r.succeeded()) { final Optional user = Optional.ofNullable(r.result()); - validateExpExists(user); + validateExpiryExists(user); handler.handle(user); } else { LOG.debug("Invalid JWT token", r.cause()); @@ -95,7 +95,7 @@ public class AuthenticationUtils { } } - private static void validateExpExists(final Optional user) { + private static void validateExpiryExists(final Optional user) { if (!user.map(User::principal).map(p -> p.containsKey("exp")).orElse(false)) { throw new IllegalStateException("Invalid JWT doesn't have expiry"); } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuth.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuth.java index 56ab1d4464..38d256c051 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuth.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuth.java @@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import io.vertx.core.AsyncResult; @@ -32,6 +33,7 @@ import org.springframework.security.crypto.bcrypt.BCrypt; public class TomlAuth implements AuthProvider { + public static final String PRIVACY_PUBLIC_KEY = "privacyPublicKey"; private final Vertx vertx; private final TomlAuthOptions options; @@ -124,8 +126,11 @@ public class TomlAuth implements AuthProvider { userData.getArrayOrEmpty("roles").toList().stream() .map(Object::toString) .collect(Collectors.toList()); + final Optional privacyPublicKey = + Optional.ofNullable(userData.getString(PRIVACY_PUBLIC_KEY)); - return new TomlUser(username, saltedAndHashedPassword, groups, permissions, roles); + return new TomlUser( + username, saltedAndHashedPassword, groups, permissions, roles, privacyPublicKey); } private void checkPasswordHash( diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUser.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUser.java index 0a97472bf3..856d8858a5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUser.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUser.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; import java.util.List; +import java.util.Optional; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; @@ -29,28 +30,34 @@ public class TomlUser extends AbstractUser { private final List groups; private final List permissions; private final List roles; + private Optional privacyPublicKey; TomlUser( final String username, final String password, final List groups, final List permissions, - final List roles) { + final List roles, + final Optional privacyPublicKey) { this.username = username; this.password = password; this.groups = groups; this.permissions = permissions; this.roles = roles; + this.privacyPublicKey = privacyPublicKey; } @Override public JsonObject principal() { - return new JsonObject() - .put("username", username) - .put("password", password) - .put("groups", groups) - .put("permissions", permissions) - .put("roles", roles); + final JsonObject principle = + new JsonObject() + .put("username", username) + .put("password", password) + .put("groups", groups) + .put("permissions", permissions) + .put("roles", roles); + privacyPublicKey.ifPresent(pk -> principle.put("privacyPublicKey", pk)); + return principle; } @Override @@ -85,4 +92,8 @@ public class TomlUser extends AbstractUser { public List getRoles() { return roles; } + + public Optional getPrivacyPublicKey() { + return privacyPublicKey; + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java index ca42314764..3691657267 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java @@ -14,7 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.util.Lists.list; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -42,16 +45,14 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.math.BigInteger; import java.nio.file.Paths; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -60,19 +61,13 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import com.google.common.base.Splitter; import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.ext.auth.KeyStoreOptions; -import io.vertx.ext.auth.PubSecKeyOptions; -import io.vertx.ext.auth.SecretOptions; import io.vertx.ext.auth.User; import io.vertx.ext.auth.jwt.JWTAuth; -import io.vertx.ext.auth.jwt.JWTAuthOptions; -import io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl; -import io.vertx.ext.jwt.JWK; -import io.vertx.ext.jwt.JWT; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -272,63 +267,6 @@ public class JsonRpcHttpServiceLoginTest { } } - private JWT makeJwt(final JWTAuthOptions config) - throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { - final KeyStoreOptions keyStoreOptions = config.getKeyStore(); - if (keyStoreOptions != null) { - final KeyStore ks = KeyStore.getInstance(keyStoreOptions.getType()); - - // synchronize on the class to avoid the case where multiple file accesses will overlap - synchronized (JWTAuthProviderImpl.class) { - final Buffer keystore = vertx.fileSystem().readFileBlocking(keyStoreOptions.getPath()); - - try (final InputStream in = new ByteArrayInputStream(keystore.getBytes())) { - ks.load(in, keyStoreOptions.getPassword().toCharArray()); - } - } - - return new JWT(ks, keyStoreOptions.getPassword().toCharArray()); - } else { - // no key file attempt to load pem keys - final JWT jwt = new JWT(); - - final List keys = config.getPubSecKeys(); - - if (keys != null) { - for (final PubSecKeyOptions pubSecKey : config.getPubSecKeys()) { - if (pubSecKey.isSymmetric()) { - jwt.addJWK(new JWK(pubSecKey.getAlgorithm(), pubSecKey.getPublicKey())); - } else { - jwt.addJWK( - new JWK( - pubSecKey.getAlgorithm(), - pubSecKey.isCertificate(), - pubSecKey.getPublicKey(), - pubSecKey.getSecretKey())); - } - } - } - - // TODO: remove once the deprecation ends! - final List secrets = config.getSecrets(); - - if (secrets != null) { - for (final SecretOptions secret : secrets) { - jwt.addSecret(secret.getType(), secret.getSecret()); - } - } - - final List jwks = config.getJwks(); - - if (jwks != null) { - for (final JsonObject jwk : jwks) { - jwt.addJWK(new JWK(jwk)); - } - } - return jwt; - } - } - @Test public void loginDoesntPopulateJWTPayloadWithPassword() throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { @@ -348,15 +286,48 @@ public class JsonRpcHttpServiceLoginTest { final JsonObject respBody = new JsonObject(bodyString); final String token = respBody.getString("token"); assertThat(token).isNotNull(); - final JWT jwt = makeJwt(service.authenticationService.get().jwtAuthOptions); - final JsonObject jwtPayload = jwt.decode(token); + final JsonObject jwtPayload = decodeJwtPayload(token); final String jwtPayloadString = jwtPayload.encode(); assertThat(jwtPayloadString.contains("password")).isFalse(); assertThat(jwtPayloadString.contains("pegasys")).isFalse(); } } + @Test + public void loginPopulatesJWTPayloadWithRequiredValues() + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + final RequestBody body = + RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pegasys\"}"); + 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 JsonObject jwtPayload = decodeJwtPayload(token); + assertThat(jwtPayload.getString("username")).isEqualTo("user"); + assertThat(jwtPayload.getJsonArray("permissions")) + .isEqualTo( + new JsonArray(list("fakePermission", "eth:blockNumber", "eth:subscribe", "web3:*"))); + assertThat(jwtPayload.getString("privacyPublicKey")) + .isEqualTo("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); + assertThat(jwtPayload.containsKey("iat")).isTrue(); + assertThat(jwtPayload.containsKey("exp")).isTrue(); + final long tokenExpiry = jwtPayload.getLong("exp") - jwtPayload.getLong("iat"); + assertThat(tokenExpiry).isEqualTo(MINUTES.toSeconds(5)); + } + } + private String login(final String username, final String password) throws IOException { final RequestBody loginBody = RequestBody.create( @@ -534,4 +505,10 @@ public class JsonRpcHttpServiceLoginTest { token.ifPresent(t -> request.addHeader("Authorization", "Bearer " + t)); return request.build(); } + + private JsonObject decodeJwtPayload(final String token) { + final List tokenParts = Splitter.on('.').splitToList(token); + final String payload = tokenParts.get(1); + return new JsonObject(new String(Base64.getUrlDecoder().decode(payload), UTF_8)); + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuthTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuthTest.java index f251d53a73..b3d22cc3e3 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuthTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuthTest.java @@ -105,14 +105,16 @@ public class TomlAuthTest { } @Test - public void validPasswordShouldAuthenticateSuccessfully(final TestContext context) { + public void validPasswordWithAllValuesShouldAuthenticateAndCreateUserSuccessfully( + final TestContext context) { JsonObject expectedPrincipal = new JsonObject() .put("username", "userA") .put("password", "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC") .put("groups", new JsonArray().add("admin")) .put("permissions", new JsonArray().add("eth:*").add("perm:*")) - .put("roles", new JsonArray().add("net")); + .put("roles", new JsonArray().add("net")) + .put("privacyPublicKey", "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); JsonObject authInfo = new JsonObject().put("username", "userA").put("password", "pegasys"); @@ -122,6 +124,25 @@ public class TomlAuthTest { res -> context.assertEquals(expectedPrincipal, res.principal()))); } + @Test + public void validPasswordWithOptionalValuesShouldAuthenticateAndCreateUserSuccessfully( + final TestContext context) { + JsonObject expectedPrincipal = + new JsonObject() + .put("username", "userB") + .put("password", "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC") + .put("groups", new JsonArray()) + .put("permissions", new JsonArray().add("net:*")) + .put("roles", new JsonArray()); + + JsonObject authInfo = new JsonObject().put("username", "userB").put("password", "pegasys"); + + tomlAuth.authenticate( + authInfo, + context.asyncAssertSuccess( + res -> context.assertEquals(expectedPrincipal, res.principal()))); + } + private String getTomlPath(final String tomlFileName) throws URISyntaxException { return Paths.get(ClassLoader.getSystemResource(tomlFileName).toURI()) .toAbsolutePath() diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUserTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUserTest.java new file mode 100644 index 0000000000..f96e0e0f60 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUserTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.util.Lists.list; + +import java.util.Optional; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.junit.Test; + +public class TomlUserTest { + + @Test + public void createsPrincipleWithAllValues() { + final TomlUser tomlUser = + new TomlUser( + "user", + "password", + list("admin"), + list("eth:*", "perm:*"), + list("net"), + Optional.of("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=")); + + final JsonObject principal = tomlUser.principal(); + assertThat(principal.getString("username")).isEqualTo("user"); + assertThat(principal.getString("password")).isEqualTo("password"); + assertThat(principal.getJsonArray("groups")).isEqualTo(new JsonArray(list("admin"))); + assertThat(principal.getJsonArray("permissions")) + .isEqualTo(new JsonArray(list("eth:*", "perm:*"))); + assertThat(principal.getJsonArray("roles")).isEqualTo(new JsonArray(list("net"))); + assertThat(principal.getString("privacyPublicKey")) + .isEqualTo("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); + } + + @Test + public void createsPrincipleWithOnlyRequiredValues() { + final TomlUser tomlUser = + new TomlUser("user", "password", list(), list(), list(), Optional.empty()); + + final JsonObject principal = tomlUser.principal(); + assertThat(principal.getString("username")).isEqualTo("user"); + assertThat(principal.getString("password")).isEqualTo("password"); + assertThat(principal.getJsonArray("groups")).isEqualTo(new JsonArray()); + assertThat(principal.getJsonArray("permissions")).isEqualTo(new JsonArray()); + assertThat(principal.getJsonArray("roles")).isEqualTo(new JsonArray()); + assertThat(principal.containsKey("privacyPublicKey")).isFalse(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java index 03ad8b58d6..70ba99b786 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java @@ -14,7 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.util.Lists.list; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; @@ -25,10 +28,13 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import java.net.URISyntaxException; import java.nio.file.Paths; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.google.common.base.Splitter; import com.google.common.collect.Lists; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; @@ -38,6 +44,7 @@ import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.RequestOptions; import io.vertx.core.http.impl.headers.VertxHttpHeaders; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.User; import io.vertx.ext.auth.jwt.JWTAuth; @@ -250,4 +257,53 @@ public class WebSocketServiceLoginTest { async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); } + + @Test + public void loginPopulatesJWTPayloadWithRequiredValues(final TestContext context) { + final Async async = context.async(); + final HttpClientRequest request = + httpClient.post( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/login", + response -> { + response.bodyHandler( + buffer -> { + final String body = buffer.toString(); + assertThat(body).isNotBlank(); + + final JsonObject respBody = new JsonObject(body); + final String token = respBody.getString("token"); + + final JsonObject jwtPayload = decodeJwtPayload(token); + assertThat(jwtPayload.getString("username")).isEqualTo("user"); + assertThat(jwtPayload.getJsonArray("permissions")) + .isEqualTo( + new JsonArray( + list( + "fakePermission", + "eth:blockNumber", + "eth:subscribe", + "web3:*"))); + assertThat(jwtPayload.getString("privacyPublicKey")) + .isEqualTo("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); + assertThat(jwtPayload.containsKey("iat")).isTrue(); + assertThat(jwtPayload.containsKey("exp")).isTrue(); + final long tokenExpiry = jwtPayload.getLong("exp") - jwtPayload.getLong("iat"); + assertThat(tokenExpiry).isEqualTo(MINUTES.toSeconds(5)); + + async.complete(); + }); + }); + request.putHeader("Content-Type", "application/json; charset=utf-8"); + request.end("{\"username\":\"user\",\"password\":\"pegasys\"}"); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } + + private JsonObject decodeJwtPayload(final String token) { + final List tokenParts = Splitter.on('.').splitToList(token); + final String payload = tokenParts.get(1); + return new JsonObject(new String(Base64.getUrlDecoder().decode(payload), UTF_8)); + } } diff --git a/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml b/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml index 13b19700ee..48e7bf1e5c 100644 --- a/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml +++ b/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml @@ -1,3 +1,4 @@ [Users.user] password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" permissions = ["fakePermission","eth:blockNumber","eth:subscribe","web3:*"] +privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=" diff --git a/ethereum/api/src/test/resources/authentication/auth.toml b/ethereum/api/src/test/resources/authentication/auth.toml index 0c31c70e8a..3ab994ceef 100644 --- a/ethereum/api/src/test/resources/authentication/auth.toml +++ b/ethereum/api/src/test/resources/authentication/auth.toml @@ -3,6 +3,11 @@ password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" groups = ["admin"] permissions = ["eth:*", "perm:*"] roles = ["net"] +privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=" + +[Users.userB] +password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" +permissions = ["net:*"] [Groups.admins] roles = ["admin"] diff --git a/ethereum/referencetests/src/test/resources b/ethereum/referencetests/src/test/resources index cfbcd15f91..0327d9f76c 160000 --- a/ethereum/referencetests/src/test/resources +++ b/ethereum/referencetests/src/test/resources @@ -1 +1 @@ -Subproject commit cfbcd15f91d4d6e1785d9cae5c5c37f47e8bad46 +Subproject commit 0327d9f76ce2a292a99e7a9dfc93627368ce589e