Allow use of privacy public key in the credentials file (#196)

Signed-off-by: Jason Frame <jasonwframe@gmail.com>
pull/205/head
Jason Frame 5 years ago committed by GitHub
parent a449e81b2d
commit c1ddab52fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      besu/src/main/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidator.java
  2. 40
      besu/src/test/java/org/hyperledger/besu/cli/custom/RpcAuthFileValidatorTest.java
  3. 1
      besu/src/test/resources/rpcauth/auth_correct.toml
  4. 0
      besu/src/test/resources/rpcauth/auth_duplicate_user.toml
  5. 0
      besu/src/test/resources/rpcauth/auth_invalid.toml
  6. 0
      besu/src/test/resources/rpcauth/auth_invalid_groups_value.toml
  7. 21
      besu/src/test/resources/rpcauth/auth_invalid_permissions_value.toml
  8. 22
      besu/src/test/resources/rpcauth/auth_invalid_privacy_public_key_value.toml
  9. 0
      besu/src/test/resources/rpcauth/auth_no_password.toml
  10. 5
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java
  11. 4
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
  12. 7
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuth.java
  13. 15
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUser.java
  14. 115
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
  15. 25
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlAuthTest.java
  16. 63
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/TomlUserTest.java
  17. 56
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java
  18. 1
      ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml
  19. 5
      ethereum/api/src/test/resources/authentication/auth.toml
  20. 2
      ethereum/referencetests/src/test/resources

@ -14,6 +14,8 @@
*/ */
package org.hyperledger.besu.cli.custom; 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 org.hyperledger.besu.ethereum.permissioning.TomlConfigFileParser;
import java.io.File; import java.io.File;
@ -97,7 +99,19 @@ public class RpcAuthFileValidator {
.dottedKeySet() .dottedKeySet()
.parallelStream() .parallelStream()
.filter(keySet -> !keySet.contains("password")) .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) { private static boolean verifyArray(final String key, final TomlParseResult tomlParseResult) {

@ -27,11 +27,16 @@ import picocli.CommandLine.ParameterException;
@RunWith(MockitoJUnitRunner.StrictStubs.class) @RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RpcAuthFileValidatorTest { public class RpcAuthFileValidatorTest {
private static final String CORRECT_TOML = "/auth_correct.toml"; private static final String CORRECT_TOML = "/rpcauth/auth_correct.toml";
private static final String DUPLICATE_USER_TOML = "/auth_duplicate_user.toml"; private static final String DUPLICATE_USER_TOML = "/rpcauth/auth_duplicate_user.toml";
private static final String INVALID_TOML = "/auth_invalid.toml"; private static final String INVALID_TOML = "/rpcauth/auth_invalid.toml";
private static final String INVALID_VALUE_TOML = "/auth_invalid_value.toml"; private static final String INVALID_GROUPS_VALUE_TOML = "/rpcauth/auth_invalid_groups_value.toml";
private static final String NO_PASSWORD_TOML = "/auth_no_password.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; @Mock CommandLine commandLine;
@Test @Test
@ -67,10 +72,31 @@ public class RpcAuthFileValidatorTest {
} }
@Test @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( 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) .isInstanceOf(ParameterException.class)
.hasMessage("RPC authentication configuration file contains invalid values."); .hasMessage("RPC authentication configuration file contains invalid values.");
} }

@ -3,6 +3,7 @@ password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"] groups = ["admin"]
permissions = ["eth:*", "perm:*"] permissions = ["eth:*", "perm:*"]
roles = ["net"] roles = ["net"]
privacyPublicKey="A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="
[Users.userB] [Users.userB]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"

@ -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:*"]

@ -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:*"]

@ -186,6 +186,11 @@ public class AuthenticationService {
new JsonObject() new JsonObject()
.put("permissions", user.principal().getValue("permissions")) .put("permissions", user.principal().getValue("permissions"))
.put("username", user.principal().getValue("username")); .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 String token = jwtAuthProvider.generateToken(jwtContents, options);
final JsonObject responseBody = new JsonObject().put("token", token); final JsonObject responseBody = new JsonObject().put("token", token);

@ -82,7 +82,7 @@ public class AuthenticationUtils {
(r) -> { (r) -> {
if (r.succeeded()) { if (r.succeeded()) {
final Optional<User> user = Optional.ofNullable(r.result()); final Optional<User> user = Optional.ofNullable(r.result());
validateExpExists(user); validateExpiryExists(user);
handler.handle(user); handler.handle(user);
} else { } else {
LOG.debug("Invalid JWT token", r.cause()); LOG.debug("Invalid JWT token", r.cause());
@ -95,7 +95,7 @@ public class AuthenticationUtils {
} }
} }
private static void validateExpExists(final Optional<User> user) { private static void validateExpiryExists(final Optional<User> user) {
if (!user.map(User::principal).map(p -> p.containsKey("exp")).orElse(false)) { if (!user.map(User::principal).map(p -> p.containsKey("exp")).orElse(false)) {
throw new IllegalStateException("Invalid JWT doesn't have expiry"); throw new IllegalStateException("Invalid JWT doesn't have expiry");
} }

@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.vertx.core.AsyncResult; import io.vertx.core.AsyncResult;
@ -32,6 +33,7 @@ import org.springframework.security.crypto.bcrypt.BCrypt;
public class TomlAuth implements AuthProvider { public class TomlAuth implements AuthProvider {
public static final String PRIVACY_PUBLIC_KEY = "privacyPublicKey";
private final Vertx vertx; private final Vertx vertx;
private final TomlAuthOptions options; private final TomlAuthOptions options;
@ -124,8 +126,11 @@ public class TomlAuth implements AuthProvider {
userData.getArrayOrEmpty("roles").toList().stream() userData.getArrayOrEmpty("roles").toList().stream()
.map(Object::toString) .map(Object::toString)
.collect(Collectors.toList()); .collect(Collectors.toList());
final Optional<String> 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( private void checkPasswordHash(

@ -15,6 +15,7 @@
package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
import java.util.List; import java.util.List;
import java.util.Optional;
import io.vertx.core.AsyncResult; import io.vertx.core.AsyncResult;
import io.vertx.core.Handler; import io.vertx.core.Handler;
@ -29,28 +30,34 @@ public class TomlUser extends AbstractUser {
private final List<String> groups; private final List<String> groups;
private final List<String> permissions; private final List<String> permissions;
private final List<String> roles; private final List<String> roles;
private Optional<String> privacyPublicKey;
TomlUser( TomlUser(
final String username, final String username,
final String password, final String password,
final List<String> groups, final List<String> groups,
final List<String> permissions, final List<String> permissions,
final List<String> roles) { final List<String> roles,
final Optional<String> privacyPublicKey) {
this.username = username; this.username = username;
this.password = password; this.password = password;
this.groups = groups; this.groups = groups;
this.permissions = permissions; this.permissions = permissions;
this.roles = roles; this.roles = roles;
this.privacyPublicKey = privacyPublicKey;
} }
@Override @Override
public JsonObject principal() { public JsonObject principal() {
return new JsonObject() final JsonObject principle =
new JsonObject()
.put("username", username) .put("username", username)
.put("password", password) .put("password", password)
.put("groups", groups) .put("groups", groups)
.put("permissions", permissions) .put("permissions", permissions)
.put("roles", roles); .put("roles", roles);
privacyPublicKey.ifPresent(pk -> principle.put("privacyPublicKey", pk));
return principle;
} }
@Override @Override
@ -85,4 +92,8 @@ public class TomlUser extends AbstractUser {
public List<String> getRoles() { public List<String> getRoles() {
return roles; return roles;
} }
public Optional<String> getPrivacyPublicKey() {
return privacyPublicKey;
}
} }

@ -14,7 +14,10 @@
*/ */
package org.hyperledger.besu.ethereum.api.jsonrpc; 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.api.Assertions.assertThat;
import static org.assertj.core.util.Lists.list;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy; 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.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -60,19 +61,13 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import com.google.common.base.Splitter;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.Json; import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; 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.User;
import io.vertx.ext.auth.jwt.JWTAuth; 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.MediaType;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
@ -272,65 +267,35 @@ public class JsonRpcHttpServiceLoginTest {
} }
} }
private JWT makeJwt(final JWTAuthOptions config) @Test
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { public void loginDoesntPopulateJWTPayloadWithPassword()
final KeyStoreOptions keyStoreOptions = config.getKeyStore(); throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
if (keyStoreOptions != null) { final RequestBody body =
final KeyStore ks = KeyStore.getInstance(keyStoreOptions.getType()); RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pegasys\"}");
final Request request = new Request.Builder().post(body).url(baseUrl + "/login").build();
// synchronize on the class to avoid the case where multiple file accesses will overlap try (final Response resp = client.newCall(request).execute()) {
synchronized (JWTAuthProviderImpl.class) { assertThat(resp.code()).isEqualTo(200);
final Buffer keystore = vertx.fileSystem().readFileBlocking(keyStoreOptions.getPath()); assertThat(resp.message()).isEqualTo("OK");
assertThat(resp.body().contentType()).isNotNull();
try (final InputStream in = new ByteArrayInputStream(keystore.getBytes())) { assertThat(resp.body().contentType().type()).isEqualTo("application");
ks.load(in, keyStoreOptions.getPassword().toCharArray()); assertThat(resp.body().contentType().subtype()).isEqualTo("json");
} final String bodyString = resp.body().string();
} assertThat(bodyString).isNotNull();
assertThat(bodyString).isNotBlank();
return new JWT(ks, keyStoreOptions.getPassword().toCharArray());
} else {
// no key file attempt to load pem keys
final JWT jwt = new JWT();
final List<PubSecKeyOptions> 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<SecretOptions> secrets = config.getSecrets();
if (secrets != null) {
for (final SecretOptions secret : secrets) {
jwt.addSecret(secret.getType(), secret.getSecret());
}
}
final List<JsonObject> jwks = config.getJwks(); final JsonObject respBody = new JsonObject(bodyString);
final String token = respBody.getString("token");
assertThat(token).isNotNull();
if (jwks != null) { final JsonObject jwtPayload = decodeJwtPayload(token);
for (final JsonObject jwk : jwks) { final String jwtPayloadString = jwtPayload.encode();
jwt.addJWK(new JWK(jwk)); assertThat(jwtPayloadString.contains("password")).isFalse();
} assertThat(jwtPayloadString.contains("pegasys")).isFalse();
}
return jwt;
} }
} }
@Test @Test
public void loginDoesntPopulateJWTPayloadWithPassword() public void loginPopulatesJWTPayloadWithRequiredValues()
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
final RequestBody body = final RequestBody body =
RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pegasys\"}"); RequestBody.create(JSON, "{\"username\":\"user\",\"password\":\"pegasys\"}");
@ -348,12 +313,18 @@ public class JsonRpcHttpServiceLoginTest {
final JsonObject respBody = new JsonObject(bodyString); final JsonObject respBody = new JsonObject(bodyString);
final String token = respBody.getString("token"); final String token = respBody.getString("token");
assertThat(token).isNotNull(); 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(jwtPayload.getString("username")).isEqualTo("user");
assertThat(jwtPayloadString.contains("password")).isFalse(); assertThat(jwtPayload.getJsonArray("permissions"))
assertThat(jwtPayloadString.contains("pegasys")).isFalse(); .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));
} }
} }
@ -534,4 +505,10 @@ public class JsonRpcHttpServiceLoginTest {
token.ifPresent(t -> request.addHeader("Authorization", "Bearer " + t)); token.ifPresent(t -> request.addHeader("Authorization", "Bearer " + t));
return request.build(); return request.build();
} }
private JsonObject decodeJwtPayload(final String token) {
final List<String> tokenParts = Splitter.on('.').splitToList(token);
final String payload = tokenParts.get(1);
return new JsonObject(new String(Base64.getUrlDecoder().decode(payload), UTF_8));
}
} }

@ -105,14 +105,16 @@ public class TomlAuthTest {
} }
@Test @Test
public void validPasswordShouldAuthenticateSuccessfully(final TestContext context) { public void validPasswordWithAllValuesShouldAuthenticateAndCreateUserSuccessfully(
final TestContext context) {
JsonObject expectedPrincipal = JsonObject expectedPrincipal =
new JsonObject() new JsonObject()
.put("username", "userA") .put("username", "userA")
.put("password", "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC") .put("password", "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC")
.put("groups", new JsonArray().add("admin")) .put("groups", new JsonArray().add("admin"))
.put("permissions", new JsonArray().add("eth:*").add("perm:*")) .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"); JsonObject authInfo = new JsonObject().put("username", "userA").put("password", "pegasys");
@ -122,6 +124,25 @@ public class TomlAuthTest {
res -> context.assertEquals(expectedPrincipal, res.principal()))); 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 { private String getTomlPath(final String tomlFileName) throws URISyntaxException {
return Paths.get(ClassLoader.getSystemResource(tomlFileName).toURI()) return Paths.get(ClassLoader.getSystemResource(tomlFileName).toURI())
.toAbsolutePath() .toAbsolutePath()

@ -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();
}
}

@ -14,7 +14,10 @@
*/ */
package org.hyperledger.besu.ethereum.api.jsonrpc.websocket; 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.api.Assertions.assertThat;
import static org.assertj.core.util.Lists.list;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
@ -25,10 +28,13 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import io.vertx.core.MultiMap; import io.vertx.core.MultiMap;
import io.vertx.core.Vertx; 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.HttpClientRequest;
import io.vertx.core.http.RequestOptions; import io.vertx.core.http.RequestOptions;
import io.vertx.core.http.impl.headers.VertxHttpHeaders; import io.vertx.core.http.impl.headers.VertxHttpHeaders;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User; import io.vertx.ext.auth.User;
import io.vertx.ext.auth.jwt.JWTAuth; import io.vertx.ext.auth.jwt.JWTAuth;
@ -250,4 +257,53 @@ public class WebSocketServiceLoginTest {
async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); 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<String> tokenParts = Splitter.on('.').splitToList(token);
final String payload = tokenParts.get(1);
return new JsonObject(new String(Base64.getUrlDecoder().decode(payload), UTF_8));
}
} }

@ -1,3 +1,4 @@
[Users.user] [Users.user]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
permissions = ["fakePermission","eth:blockNumber","eth:subscribe","web3:*"] permissions = ["fakePermission","eth:blockNumber","eth:subscribe","web3:*"]
privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="

@ -3,6 +3,11 @@ password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"] groups = ["admin"]
permissions = ["eth:*", "perm:*"] permissions = ["eth:*", "perm:*"]
roles = ["net"] roles = ["net"]
privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="
[Users.userB]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
permissions = ["net:*"]
[Groups.admins] [Groups.admins]
roles = ["admin"] roles = ["admin"]

@ -1 +1 @@
Subproject commit cfbcd15f91d4d6e1785d9cae5c5c37f47e8bad46 Subproject commit 0327d9f76ce2a292a99e7a9dfc93627368ce589e
Loading…
Cancel
Save