From e7499375b10c8eec88dc5f99f8a3abda56bf0004 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sat, 1 Feb 2025 11:37:43 +0200 Subject: [PATCH 1/9] Updated Vault --- .../vault/VaultServiceTokenSupplier.java | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java index 1804b0d..4161fae 100644 --- a/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java @@ -9,6 +9,7 @@ import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,7 +21,7 @@ public class VaultServiceTokenSupplier { private final String vaultAddress; private final String serviceRole; - private final CompletableFuture vaultTokenSupplier; + private final Supplier> vaultTokenSupplier; private final BiFunction, String> serviceTokenNameBuilder; private VaultServiceTokenSupplier(Builder builder) { @@ -39,16 +40,17 @@ private VaultServiceTokenSupplier(Builder builder) { * serviceTokenNameBuilder} * @return vault service token */ - public String getToken(Map tags) { - try { - final String vaultToken = vaultTokenSupplier.get(); - final String uri = toServiceTokenUri(tags); - final String token = rpcGetToken(uri, vaultToken); - LOGGER.debug("[getToken][success] uri={}, tags={}, result={}", uri, tags, mask(token)); - return token; - } catch (Exception ex) { - throw new RuntimeException(ex); - } + public CompletableFuture getToken(Map tags) { + return vaultTokenSupplier + .get() + .thenApplyAsync( + vaultToken -> { + final String uri = toServiceTokenUri(tags); + final String token = rpcGetToken(uri, vaultToken); + LOGGER.debug( + "[getToken][success] uri={}, tags={}, result={}", uri, tags, mask(token)); + return token; + }); } private static String rpcGetToken(String uri, String vaultToken) { @@ -93,26 +95,52 @@ public static class Builder { private String vaultAddress; private String serviceRole; - private CompletableFuture vaultTokenSupplier; + private Supplier> vaultTokenSupplier; private BiFunction, String> serviceTokenNameBuilder; public Builder() {} + /** + * Setter for {@code vaultAddress}. + * + * @param vaultAddress vaultAddress + * @return this + */ public Builder vaultAddress(String vaultAddress) { this.vaultAddress = vaultAddress; return this; } + /** + * Setter for {@code serviceRole}. + * + * @param serviceRole serviceRole + * @return this + */ public Builder serviceRole(String serviceRole) { this.serviceRole = serviceRole; return this; } - public Builder vaultTokenSupplier(CompletableFuture vaultTokenSupplier) { + /** + * Setter for {@code vaultTokenSupplier}. + * + * @param vaultTokenSupplier vaultTokenSupplier + * @return this + */ + public Builder vaultTokenSupplier(Supplier> vaultTokenSupplier) { this.vaultTokenSupplier = vaultTokenSupplier; return this; } + /** + * Setter for {@code serviceTokenNameBuilder}. + * + * @param serviceTokenNameBuilder {@link BiFunction} where first parameter is service-role, and + * second parameter is map of attributes, and result will be fully qualified service-token + * name - a combination of service-role and attributes. + * @return this + */ public Builder serviceTokenNameBuilder( BiFunction, String> serviceTokenNameBuilder) { this.serviceTokenNameBuilder = serviceTokenNameBuilder; From 0f7f4a420c6f9684b37872df3bb73802c568fc01 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 14:01:08 +0200 Subject: [PATCH 2/9] WIP --- jwt/pom.xml | 36 --- .../security/acl/DefaultAccessControl.java | 50 --- .../scalecube/security/api/AccessControl.java | 14 - .../scalecube/security/api/Authenticator.java | 14 - .../io/scalecube/security/api/Authorizer.java | 8 - .../io/scalecube/security/api/Profile.java | 139 --------- .../security/jwt/AsyncJwtKeyResolver.java | 10 - .../security/jwt/AuthenticationException.java | 16 - .../security/jwt/DefaultJwtAuthenticator.java | 37 --- .../security/jwt/JwtAuthenticator.java | 36 --- .../security/jwt/JwtKeyResolver.java | 11 - .../security/JwtAuthenticatorTests.java | 287 ------------------ .../security/acl/AccessControlTest.java | 75 ----- .../scalecube/security/acl/Permissions.java | 82 ----- pom.xml | 35 ++- .../tokens/jwt/JsonwebtokenResolver.java | 36 +++ .../security/tokens/jwt/JwkInfoList.java | 11 +- .../security/tokens/jwt/JwksKeyLocator.java | 185 +++++++++++ .../security/tokens/jwt/JwksKeyProvider.java | 147 --------- .../security/tokens/jwt/JwtToken.java | 21 +- .../tokens/jwt/JwtTokenException.java | 25 ++ .../security/tokens/jwt/JwtTokenParser.java | 10 - .../tokens/jwt/JwtTokenParserFactory.java | 6 - .../security/tokens/jwt/JwtTokenResolver.java | 13 +- .../tokens/jwt/JwtTokenResolverImpl.java | 163 ---------- .../tokens/jwt/KeyNotFoundException.java | 18 -- .../security/tokens/jwt/KeyProvider.java | 16 - .../tokens/jwt/KeyProviderException.java | 22 -- .../jwt/jsonwebtoken/JsonwebtokenParser.java | 45 --- .../JsonwebtokenParserFactory.java | 16 - .../tokens/jwt/JwtTokenResolverTests.java | 164 ---------- .../security/tokens/jwt/JwtTokenWithKey.java | 20 -- .../security/tokens/jwt/VaultEnvironment.java | 64 ++-- .../tokens/jwt/VaultIdentityTokenTests.java | 78 +++++ .../tokens/jwt/VaultJwksKeyProviderTests.java | 101 ------ ...token-and-pubkey.after-rotation.properties | 4 - .../resources/token-and-pubkey.properties | 4 - .../token-and-wrong-pubkey.properties | 4 - 38 files changed, 398 insertions(+), 1625 deletions(-) delete mode 100644 jwt/pom.xml delete mode 100644 jwt/src/main/java/io/scalecube/security/acl/DefaultAccessControl.java delete mode 100644 jwt/src/main/java/io/scalecube/security/api/AccessControl.java delete mode 100644 jwt/src/main/java/io/scalecube/security/api/Authenticator.java delete mode 100644 jwt/src/main/java/io/scalecube/security/api/Authorizer.java delete mode 100644 jwt/src/main/java/io/scalecube/security/api/Profile.java delete mode 100644 jwt/src/main/java/io/scalecube/security/jwt/AsyncJwtKeyResolver.java delete mode 100644 jwt/src/main/java/io/scalecube/security/jwt/AuthenticationException.java delete mode 100644 jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java delete mode 100644 jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java delete mode 100644 jwt/src/main/java/io/scalecube/security/jwt/JwtKeyResolver.java delete mode 100644 jwt/src/test/java/io/scalecube/security/JwtAuthenticatorTests.java delete mode 100644 jwt/src/test/java/io/scalecube/security/acl/AccessControlTest.java delete mode 100644 jwt/src/test/java/io/scalecube/security/acl/Permissions.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyNotFoundException.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java delete mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java delete mode 100644 tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java delete mode 100644 tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenWithKey.java create mode 100644 tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java delete mode 100644 tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultJwksKeyProviderTests.java delete mode 100644 tokens/src/test/resources/token-and-pubkey.after-rotation.properties delete mode 100644 tokens/src/test/resources/token-and-pubkey.properties delete mode 100644 tokens/src/test/resources/token-and-wrong-pubkey.properties diff --git a/jwt/pom.xml b/jwt/pom.xml deleted file mode 100644 index d9e7e63..0000000 --- a/jwt/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - - io.scalecube - scalecube-security-parent - 1.1.0-SNAPSHOT - - - scalecube-security-jwt - - - - io.projectreactor - reactor-core - true - - - io.jsonwebtoken - jjwt-api - - - io.jsonwebtoken - jjwt-impl - - - io.jsonwebtoken - jjwt-jackson - - - org.slf4j - slf4j-api - - - - diff --git a/jwt/src/main/java/io/scalecube/security/acl/DefaultAccessControl.java b/jwt/src/main/java/io/scalecube/security/acl/DefaultAccessControl.java deleted file mode 100644 index aee39f8..0000000 --- a/jwt/src/main/java/io/scalecube/security/acl/DefaultAccessControl.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.scalecube.security.acl; - -import io.scalecube.security.api.AccessControl; -import io.scalecube.security.api.Authenticator; -import io.scalecube.security.api.Authorizer; -import io.scalecube.security.api.Profile; -import io.scalecube.security.jwt.AuthenticationException; -import reactor.core.publisher.Mono; - -public class DefaultAccessControl implements AccessControl { - private final Authenticator authenticator; - private final Authorizer authorizator; - - public static class Builder { - private Authenticator authenticator; - private Authorizer authorizer; - - public Builder authorizer(Authorizer authorizer) { - this.authorizer = authorizer; - return this; - } - - public Builder authenticator(Authenticator authenticator) { - this.authenticator = authenticator; - return this; - } - - public DefaultAccessControl build() { - return new DefaultAccessControl(this); - } - } - - public DefaultAccessControl(Builder builder) { - this.authenticator = builder.authenticator; - this.authorizator = builder.authorizer; - } - - public static DefaultAccessControl.Builder builder() { - return new DefaultAccessControl.Builder(); - } - - @Override - public Mono check(String token, String resource) { - return authenticator - .authenticate(token) - .switchIfEmpty( - Mono.error(() -> new AuthenticationException("Authentication Failure", null))) - .flatMap(profile -> authorizator.authorize(profile, resource)); - } -} diff --git a/jwt/src/main/java/io/scalecube/security/api/AccessControl.java b/jwt/src/main/java/io/scalecube/security/api/AccessControl.java deleted file mode 100644 index 9185fa1..0000000 --- a/jwt/src/main/java/io/scalecube/security/api/AccessControl.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.scalecube.security.api; - -import reactor.core.publisher.Mono; - -public interface AccessControl { - /** - * Request for an action to be made. - * - * @param token or any kind of identifying string - * @param resource the action name - * @return A mono with active profile or with an error. - */ - Mono check(String token, String resource); -} diff --git a/jwt/src/main/java/io/scalecube/security/api/Authenticator.java b/jwt/src/main/java/io/scalecube/security/api/Authenticator.java deleted file mode 100644 index 09e4bc4..0000000 --- a/jwt/src/main/java/io/scalecube/security/api/Authenticator.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.scalecube.security.api; - -import reactor.core.publisher.Mono; - -@FunctionalInterface -public interface Authenticator { - /** - * Authenticate the identity of one. - * - * @param token a string of identity, can be a token, a user and password etc,. - * @return a mono with profile identifying the one, or mono with an error. - */ - Mono authenticate(String token); -} diff --git a/jwt/src/main/java/io/scalecube/security/api/Authorizer.java b/jwt/src/main/java/io/scalecube/security/api/Authorizer.java deleted file mode 100644 index 30b922c..0000000 --- a/jwt/src/main/java/io/scalecube/security/api/Authorizer.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.scalecube.security.api; - -import reactor.core.publisher.Mono; - -@FunctionalInterface -public interface Authorizer { - Mono authorize(Profile profile, String action); -} diff --git a/jwt/src/main/java/io/scalecube/security/api/Profile.java b/jwt/src/main/java/io/scalecube/security/api/Profile.java deleted file mode 100644 index 3dd18db..0000000 --- a/jwt/src/main/java/io/scalecube/security/api/Profile.java +++ /dev/null @@ -1,139 +0,0 @@ -package io.scalecube.security.api; - -import java.util.Map; - -public class Profile { - - private final String userId; - private final String tenant; - private final String email; - private final boolean isEmailVerified; - private final String name; - private final String familyName; - private final String givenName; - private final Map claims; - - private Profile(Builder builder) { - this.userId = builder.userId; - this.tenant = builder.tenant; - this.email = builder.email; - this.isEmailVerified = builder.isEmailVerified; - this.name = builder.name; - this.familyName = builder.familyName; - this.givenName = builder.givenName; - this.claims = builder.claims; - } - - public String userId() { - return userId; - } - - public String tenant() { - return tenant; - } - - public String email() { - return email; - } - - public boolean isEmailVerified() { - return isEmailVerified; - } - - public String name() { - return name; - } - - public String familyName() { - return familyName; - } - - public String givenName() { - return givenName; - } - - public Object claim(String name) { - return this.claims.getOrDefault(name, null); - } - - public Map claims() { - return claims; - } - - @Override - public String toString() { - return super.toString() - + String.format( - " [" - + "userId=%s, " - + "tenant=%s, " - + "email=%s, " - + "isEmailVerified=%s, " - + "name=%s, " - + "familyName=%s, " - + "givenName=%s, " - + "claims=%s]", - userId, tenant, email, isEmailVerified, name, familyName, givenName, claims); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private String userId; - private String tenant; - private String email; - private boolean isEmailVerified; - private String name; - private String familyName; - private String givenName; - private Map claims; - - private Builder() {} - - public Builder userId(String userId) { - this.userId = userId; - return this; - } - - public Builder tenant(String tenant) { - this.tenant = tenant; - return this; - } - - public Builder email(String email) { - this.email = email; - return this; - } - - public Builder emailVerified(Boolean emailVerified) { - isEmailVerified = emailVerified != null ? emailVerified.booleanValue() : Boolean.FALSE; - return this; - } - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder familyName(String familyName) { - this.familyName = familyName; - return this; - } - - public Builder givenName(String givenName) { - this.givenName = givenName; - return this; - } - - public Builder claims(Map claims) { - this.claims = claims; - return this; - } - - public Profile build() { - return new Profile(this); - } - } -} diff --git a/jwt/src/main/java/io/scalecube/security/jwt/AsyncJwtKeyResolver.java b/jwt/src/main/java/io/scalecube/security/jwt/AsyncJwtKeyResolver.java deleted file mode 100644 index 0a62118..0000000 --- a/jwt/src/main/java/io/scalecube/security/jwt/AsyncJwtKeyResolver.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.scalecube.security.jwt; - -import java.security.Key; -import reactor.core.publisher.Mono; - -@FunctionalInterface -public interface AsyncJwtKeyResolver { - - Mono resolve(String keyId); -} diff --git a/jwt/src/main/java/io/scalecube/security/jwt/AuthenticationException.java b/jwt/src/main/java/io/scalecube/security/jwt/AuthenticationException.java deleted file mode 100644 index 96589a5..0000000 --- a/jwt/src/main/java/io/scalecube/security/jwt/AuthenticationException.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.scalecube.security.jwt; - -/** - * This class encapsulates all authentication exceptions. This will allow us to replace the internal - * mechanism of authentication without changing client code. - */ -public class AuthenticationException extends RuntimeException { - - public AuthenticationException(String message, Throwable cause) { - super(message, cause); - } - - public AuthenticationException(Throwable cause) { - super(cause.getMessage(), cause); - } -} diff --git a/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java b/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java deleted file mode 100644 index 98fc2c0..0000000 --- a/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.scalecube.security.jwt; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.scalecube.security.api.Profile; -import java.util.Map; -import reactor.core.publisher.Mono; - -public final class DefaultJwtAuthenticator implements JwtAuthenticator { - - private final JwtKeyResolver jwtKeyResolver; - - public DefaultJwtAuthenticator(JwtKeyResolver jwtKeyResolver) { - this.jwtKeyResolver = jwtKeyResolver; - } - - @Override - public Mono authenticate(String token) { - return Mono.defer(() -> authenticate0(token)).onErrorMap(AuthenticationException::new); - } - - private Mono authenticate0(String token) { - String tokenWithoutSignature = token.substring(0, token.lastIndexOf(".") + 1); - - JwtParser parser = Jwts.parser(); - - Jwt claims = parser.parseClaimsJwt(tokenWithoutSignature); - - return jwtKeyResolver - .resolve((Map) claims.getHeader()) - .map(key -> parser.setSigningKey(key).parseClaimsJws(token).getBody()) - .map(this::profileFromClaims); - } -} diff --git a/jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java b/jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java deleted file mode 100644 index eae5d3f..0000000 --- a/jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.scalecube.security.jwt; - -import io.jsonwebtoken.Claims; -import io.scalecube.security.api.Authenticator; -import io.scalecube.security.api.Profile; -import reactor.core.publisher.Mono; - -public interface JwtAuthenticator extends Authenticator { - - /** - * Authenticate a JWT token. - * - * @param token jwt token. - * @return security profile. - */ - Mono authenticate(String token); - - /** - * Create a profile from claims. - * - * @param tokenClaims the claims to parse - * @return a profile from the claims - */ - default Profile profileFromClaims(Claims tokenClaims) { - return Profile.builder() - .userId(tokenClaims.get("sub", String.class)) - .tenant(tokenClaims.get("aud", String.class)) - .email(tokenClaims.get("email", String.class)) - .emailVerified(tokenClaims.get("email_verified", Boolean.class)) - .name(tokenClaims.get("name", String.class)) - .familyName(tokenClaims.get("family_name", String.class)) - .givenName(tokenClaims.get("given_name", String.class)) - .claims(tokenClaims) - .build(); - } -} diff --git a/jwt/src/main/java/io/scalecube/security/jwt/JwtKeyResolver.java b/jwt/src/main/java/io/scalecube/security/jwt/JwtKeyResolver.java deleted file mode 100644 index 216d200..0000000 --- a/jwt/src/main/java/io/scalecube/security/jwt/JwtKeyResolver.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.scalecube.security.jwt; - -import java.security.Key; -import java.util.Map; -import reactor.core.publisher.Mono; - -@FunctionalInterface -public interface JwtKeyResolver { - - Mono resolve(Map jtwHeaders); -} diff --git a/jwt/src/test/java/io/scalecube/security/JwtAuthenticatorTests.java b/jwt/src/test/java/io/scalecube/security/JwtAuthenticatorTests.java deleted file mode 100644 index 4706542..0000000 --- a/jwt/src/test/java/io/scalecube/security/JwtAuthenticatorTests.java +++ /dev/null @@ -1,287 +0,0 @@ -package io.scalecube.security; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.security.SignatureException; -import io.scalecube.security.jwt.DefaultJwtAuthenticator; -import io.scalecube.security.jwt.JwtAuthenticator; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.time.Instant; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import javax.crypto.spec.SecretKeySpec; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class JwtAuthenticatorTests { - - private static final Key hmacSecretKey = - new SecretKeySpec( - UUID.randomUUID().toString().getBytes(), SignatureAlgorithm.HS256.getJcaName()); - // private static final Mono hmacSecretKeyMono = Mono.just(hmacSecretKey).cache(); - private static final KeyPair keys = generateRSAKeys(); - - @Test - void authenticateAuthenticateUsingKidHeaderPropertyAuthenticationSuccess() { - Map customClaims = new HashMap<>(); - customClaims.put("name", "Trader1"); - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .addClaims(customClaims) - .signWith(hmacSecretKey) - .compact(); - - JwtAuthenticator sut = new DefaultJwtAuthenticator(map -> Mono.just(hmacSecretKey)); - - StepVerifier.create(sut.authenticate(token)) - .assertNext( - profile -> { - assertEquals("Tenant1", profile.tenant()); - assertEquals("Trader1", profile.name()); - assertEquals("1", profile.userId()); - }) - .verifyComplete(); - } - - @Test - void authenticateCreateTokenAndAuthenticateHmacAuthenticationSuccess() { - Map customClaims = new HashMap<>(); - customClaims.put("name", "Trader1"); - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .addClaims(customClaims) - .signWith(hmacSecretKey) - .compact(); - - JwtAuthenticator sut = new DefaultJwtAuthenticator(map -> Mono.just(hmacSecretKey)); - - StepVerifier.create(sut.authenticate(token)) - .assertNext( - profile -> { - assertEquals("Tenant1", profile.tenant()); - assertEquals("Trader1", profile.name()); - assertEquals("1", profile.userId()); - }) - .verifyComplete(); - } - - @Test - void authenticateValidTokenInvalidHmacSecretAuthenticationFailedExceptionThrown() { - Map customClaims = new HashMap<>(); - customClaims.put("name", "trader1"); - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .addClaims(customClaims) - .signWith(hmacSecretKey) - .compact(); - - JwtAuthenticator sut = - new DefaultJwtAuthenticator( - map -> - Mono.just( - new SecretKeySpec( - UUID.randomUUID().toString().getBytes(), - SignatureAlgorithm.HS256.getJcaName()))); - StepVerifier.create(sut.authenticate(token)) - .expectErrorSatisfies( - actualException -> - assertEquals(SignatureException.class, actualException.getCause().getClass())); - } - - @Test - void authenticateUsingKidHeaderPropertyKidIsMissingAuthenticationFailsExceptionThrown() { - - Map customClaims = new HashMap<>(); - customClaims.put("name", "Trader1"); - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .addClaims(customClaims) - .signWith(hmacSecretKey) - .compact(); - - JwtAuthenticator sut = - new DefaultJwtAuthenticator( - map -> - Optional.ofNullable(map.get("kid")) - .filter(String.class::isInstance) - .map(s -> Mono.just(hmacSecretKey)) - .orElse(Mono.empty())); - - StepVerifier.create(sut.authenticate(token)) - .expectErrorSatisfies( - actualException -> - assertEquals( - IllegalArgumentException.class, actualException.getCause().getClass())); - } - - @Test - void authenticateCreateTokenAndValidateRsaAuthenticationSuccess() { - Map customClaims = new HashMap<>(); - customClaims.put("name", "Trader1"); - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .addClaims(customClaims) - .signWith(keys.getPrivate()) - .compact(); - - JwtAuthenticator sut = new DefaultJwtAuthenticator(map -> Mono.just(keys.getPublic())); - - StepVerifier.create(sut.authenticate(token)) - .assertNext( - profile -> { - assertEquals("Tenant1", profile.tenant()); - assertEquals("Trader1", profile.name()); - assertEquals("1", profile.userId()); - }) - .verifyComplete(); - } - - @Test - void authenticateCreateTokenAndValidateWrongKeyForAlgorithmAuthenticationFailsExceptionThrown() { - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .signWith(keys.getPrivate()) - .compact(); - - JwtAuthenticator sut = - new DefaultJwtAuthenticator( - map -> - Optional.ofNullable(map.get("kid")) - .filter(String.class::isInstance) - .map(s -> Mono.just(hmacSecretKey)) - .orElse(Mono.empty())); - - StepVerifier.create(sut.authenticate(token)) - .expectErrorSatisfies( - actualException -> - assertEquals(UnsupportedJwtException.class, actualException.getCause().getClass())); - } - - @Test - void authenticateMissingClaimsInTokenAuthenticationSuccessProfilePropertyIsMissing() { - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .signWith(keys.getPrivate()) - .compact(); - - JwtAuthenticator sut = new DefaultJwtAuthenticator(map -> Mono.just(keys.getPublic())); - - StepVerifier.create(sut.authenticate(token)) - .assertNext( - profile -> { - assertEquals("Tenant1", profile.tenant()); - assertNull(profile.name()); - assertEquals("1", profile.userId()); - }) - .verifyComplete(); - } - - @Test - void authenticateUnsignedTokenAuthenticationFailsExceptionThrown() { - String token = Jwts.builder().setAudience("Tenant1").setSubject("1").compact(); - - JwtAuthenticator sut = - new DefaultJwtAuthenticator( - map -> - Optional.ofNullable(map.get("kid")) - .filter(String.class::isInstance) - .map(s -> Mono.just(hmacSecretKey)) - .orElse(Mono.empty())); - - StepVerifier.create(sut.authenticate(token)) - .expectErrorSatisfies( - actualException -> - assertEquals(UnsupportedJwtException.class, actualException.getCause().getClass())); - } - - @Test - void authenticateKeyResolverReturnNullsAuthenticationFailsExceptionThrown() { - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .signWith(keys.getPrivate()) - .compact(); - - JwtAuthenticator sut = new DefaultJwtAuthenticator(map -> Mono.empty()); - StepVerifier.create(sut.authenticate(token)) - .expectErrorSatisfies( - actualException -> - assertEquals( - IllegalArgumentException.class, actualException.getCause().getClass())); - } - - @Test - void authenticateAuthenticateExpiredTokenFails() { - - Map customClaims = new HashMap<>(); - customClaims.put("name", "Trader1"); - - String token = - Jwts.builder() - .setAudience("Tenant1") - .setSubject("1") - .setHeaderParam("kid", "5") - .setExpiration(Date.from(Instant.ofEpochMilli(0))) - .addClaims(customClaims) - .signWith(hmacSecretKey) - .compact(); - - JwtAuthenticator sut = new DefaultJwtAuthenticator(map -> Mono.empty()); - StepVerifier.create(sut.authenticate(token)) - .expectErrorSatisfies( - actualException -> - assertEquals(ExpiredJwtException.class, actualException.getCause().getClass())); - } - - private static KeyPair generateRSAKeys() { - KeyPairGenerator kpg; - try { - kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(2048); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException impossibleException) { - return Assertions.fail("This should not happen", impossibleException); - } - } -} diff --git a/jwt/src/test/java/io/scalecube/security/acl/AccessControlTest.java b/jwt/src/test/java/io/scalecube/security/acl/AccessControlTest.java deleted file mode 100644 index f16b079..0000000 --- a/jwt/src/test/java/io/scalecube/security/acl/AccessControlTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.scalecube.security.acl; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.jsonwebtoken.Jwts; -import io.scalecube.security.api.Authenticator; -import io.scalecube.security.jwt.DefaultJwtAuthenticator; -import java.security.AccessControlException; -import java.security.NoSuchAlgorithmException; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class AccessControlTest { - - // user permissions - private static final String RESOURCE_READ = "resource/read"; - private static final String RESOURCE_CREATE = "resource/create"; - private static final String RESOURCE_DELETE = "resource/delete"; - - // user roles - private static final String OWNER = "owner"; - private static final String ADMIN = "admin"; - private static final String MEMBER = "member"; - - private static SecretKey key; - private static DefaultAccessControl accessControl; - - @BeforeAll - static void setUp() throws Exception { - key = KeyGenerator.getInstance("HmacSHA256").generateKey(); - - Authenticator authenticator = - new DefaultJwtAuthenticator(m -> "1".equals(m.get("kid")) ? Mono.just(key) : Mono.empty()); - - accessControl = - DefaultAccessControl.builder() - .authenticator(authenticator) - .authorizer( - Permissions.builder() - .grant(RESOURCE_DELETE, OWNER) - .grant(RESOURCE_CREATE, OWNER, ADMIN) - .grant(RESOURCE_READ, OWNER, ADMIN, MEMBER) - .build()) - .build(); - } - - @Test - void shouldGrantAccess() throws NoSuchAlgorithmException { - - String token = - Jwts.builder().setHeaderParam("kid", "1").claim("roles", OWNER).signWith(key).compact(); - - StepVerifier.create(accessControl.check(token, RESOURCE_CREATE)) - .assertNext( - profile -> { - assertEquals(profile.claim("roles"), OWNER); - }) - .verifyComplete(); - } - - @Test - void shouldDenyAccess() throws NoSuchAlgorithmException { - - String token = - Jwts.builder().setHeaderParam("kid", "1").claim("roles", MEMBER).signWith(key).compact(); - - StepVerifier.create(accessControl.check(token, RESOURCE_DELETE)) - .expectError(AccessControlException.class) - .verify(); - } -} diff --git a/jwt/src/test/java/io/scalecube/security/acl/Permissions.java b/jwt/src/test/java/io/scalecube/security/acl/Permissions.java deleted file mode 100644 index 4581ded..0000000 --- a/jwt/src/test/java/io/scalecube/security/acl/Permissions.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.scalecube.security.acl; - -import io.scalecube.security.api.Authorizer; -import io.scalecube.security.api.Profile; -import java.security.AccessControlException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import reactor.core.publisher.Mono; - -/** - * This is a helper class for setting up an immutable Permissions object. - * - *
- * Permissions:
- *   name: resourceName
- *   permissions:
- *     allowed:
- *     - role1
- *     - role2
- * 
- */ -public class Permissions implements Authorizer { - - private final Map> rolesForAllResources; - - public static class Builder { - - private final Map> permissions = new HashMap<>(); - - /** - * grant access to list of roles for a certain action. - * - * @param resourceName name or topic of granting access. - * @param roles or roles allowed to access or do an action names are trimmed of whitespace and - * lowercased. - * @return builder. - */ - public Permissions.Builder grant(String resourceName, String... roles) { - for (String subject : roles) { - permissions - .computeIfAbsent(resourceName, newAction -> new HashSet<>()) - .add(subject.trim().toLowerCase()); - } - return this; - } - - public Authorizer build() { - return new Permissions(this); - } - } - - private Permissions(Builder builder) { - this.rolesForAllResources = new HashMap<>(builder.permissions.size()); - builder.permissions.forEach( - (action, subjects) -> { - this.rolesForAllResources.put(action, new HashSet<>(subjects)); - }); - } - - public static Permissions.Builder builder() { - return new Builder(); - } - - private static Set rolesByResource( - final Map> permissionsByAction, String resource) { - return permissionsByAction.getOrDefault(resource, Collections.emptySet()); - } - - private static boolean isInRole(Profile profile, Set roles) { - return roles.contains(profile.claim("roles")); - } - - @Override - public Mono authorize(Profile profile, String resource) { - return Mono.just(profile) - .filter(p -> isInRole(p, rolesByResource(rolesForAllResources, resource))) - .switchIfEmpty(Mono.error(() -> new AccessControlException("Permission denied"))); - } -} diff --git a/pom.xml b/pom.xml index 517af3d..af61e98 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,6 @@ scalecube-security-parent 1.1.0-SNAPSHOT pom - ScaleCube Security @@ -35,7 +34,6 @@ - jwt tokens vault @@ -45,11 +43,12 @@ 5.1.0 2.18.2 1.7.36 - 0.11.2 + 0.12.6 4.6.1 5.8.2 1.3 + 2.17.2 1.20.1 https://maven.pkg.github.com/scalecube/scalecube-security @@ -103,12 +102,19 @@ pom import + + + org.apache.logging.log4j + log4j-bom + ${log4j.version} + pom + import + org.testcontainers vault ${testcontainers.version} - test @@ -133,6 +139,12 @@ ${mockito-junit.version} test + + org.mockito + mockito-inline + ${mockito-junit.version} + test + org.hamcrest hamcrest-all @@ -144,6 +156,21 @@ reactor-test test + + org.testcontainers + vault + test + + + org.apache.logging.log4j + log4j-slf4j-impl + test + + + org.apache.logging.log4j + log4j-core + test + diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java new file mode 100644 index 0000000..7f1e9d1 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java @@ -0,0 +1,36 @@ +package io.scalecube.security.tokens.jwt; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Locator; +import java.security.Key; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class JsonwebtokenResolver implements JwtTokenResolver { + + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenResolver.class); + + private final Locator keyLocator; + + public JsonwebtokenResolver(Locator keyLocator) { + this.keyLocator = keyLocator; + } + + @Override + public Mono resolve(String token) { + return Mono.fromCallable( + () -> { + final var claimsJws = + Jwts.parser().keyLocator(keyLocator).build().parseSignedClaims(token); + return new JwtToken(claimsJws.getHeader(), claimsJws.getPayload()); + }); + } + + private static String mask(String data) { + if (data == null || data.length() < 5) { + return "*****"; + } + return data.replace(data.substring(2, data.length() - 2), "***"); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java index d0c33bb..3ccee0a 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java @@ -1,18 +1,19 @@ package io.scalecube.security.tokens.jwt; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.StringJoiner; public class JwkInfoList { - private List keys = Collections.emptyList(); + private final List keys; - public JwkInfoList() {} + public JwkInfoList() { + this(null); + } public JwkInfoList(List keys) { - this.keys = new ArrayList<>(keys); + this.keys = keys != null ? new ArrayList<>(keys) : null; } public List keys() { @@ -22,7 +23,7 @@ public List keys() { @Override public String toString() { return new StringJoiner(", ", JwkInfoList.class.getSimpleName() + "[", "]") - .add("keys=" + keys) + .add("keys=" + (keys != null ? "[" + keys.size() + "]" : null)) .toString(); } } diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java new file mode 100644 index 0000000..2934573 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java @@ -0,0 +1,185 @@ +package io.scalecube.security.tokens.jwt; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.LocatorAdapter; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.time.Duration; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +public class JwksKeyLocator extends LocatorAdapter { + + private static final ObjectMapper OBJECT_MAPPER = newObjectMapper(); + + private final URI jwksUri; + private final Duration connectTimeout; + private final Duration requestTimeout; + private final int keyTtl; + + private final Map keyResolutions = new ConcurrentHashMap<>(); + private final ReentrantLock cleanupLock = new ReentrantLock(); + + private JwksKeyLocator(Builder builder) { + this.jwksUri = builder.jwksUri; + this.connectTimeout = builder.connectTimeout; + this.requestTimeout = builder.requestTimeout; + this.keyTtl = builder.keyTtl; + } + + @Override + protected Key locate(JwsHeader header) { + try { + return keyResolutions + .computeIfAbsent( + header.getKeyId(), + kid -> { + final var key = findKeyById(computeKeyList(), kid); + if (key == null) { + throw new JwtTokenException("Cannot find key by kid: " + kid); + } + return new CachedKey(key, System.currentTimeMillis() + keyTtl); + }) + .key(); + } catch (JwtTokenException ex) { + throw ex; + } catch (Exception ex) { + throw new JwtTokenException(ex); + } finally { + tryCleanup(); + } + } + + private JwkInfoList computeKeyList() { + final HttpResponse httpResponse; + try { + httpResponse = + HttpClient.newBuilder() + .connectTimeout(connectTimeout) + .build() + .send( + HttpRequest.newBuilder(jwksUri).GET().timeout(requestTimeout).build(), + BodyHandlers.ofInputStream()); + } catch (Exception e) { + throw new JwtTokenException("Failed to retrive jwk keys", e); + } + + final var statusCode = httpResponse.statusCode(); + if (statusCode != 200) { + throw new JwtTokenException("Failed to retrive jwk keys, status: " + statusCode); + } + + return toJwkInfoList(httpResponse.body()); + } + + private static JwkInfoList toJwkInfoList(InputStream stream) { + try (var inputStream = new BufferedInputStream(stream)) { + return OBJECT_MAPPER.readValue(inputStream, JwkInfoList.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static PublicKey findKeyById(JwkInfoList jwkInfoList, String kid) { + if (jwkInfoList.keys() != null) { + return jwkInfoList.keys().stream() + .filter(jwkInfo -> kid.equals(jwkInfo.kid())) + .map(jwkInfo -> toRsaPublicKey(jwkInfo.modulus(), jwkInfo.exponent())) + .findFirst() + .orElse(null); + } + return null; + } + + private static PublicKey toRsaPublicKey(String n, String e) { + final var decoder = Base64.getUrlDecoder(); + final var modulus = new BigInteger(1, decoder.decode(n)); + final var exponent = new BigInteger(1, decoder.decode(e)); + final var keySpec = new RSAPublicKeySpec(modulus, exponent); + try { + return KeyFactory.getInstance("RSA").generatePublic(keySpec); + } catch (Exception ex) { + throw new RuntimeException(e); + } + } + + private static ObjectMapper newObjectMapper() { + final var mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper; + } + + private void tryCleanup() { + if (cleanupLock.tryLock()) { + final var now = System.currentTimeMillis(); + try { + keyResolutions.entrySet().removeIf(entry -> entry.getValue().hasExpired(now)); + } finally { + cleanupLock.unlock(); + } + } + } + + private record CachedKey(Key key, long expirationDeadline) { + + boolean hasExpired(long now) { + return now >= expirationDeadline; + } + } + + public static class Builder { + + private URI jwksUri; + private Duration connectTimeout = Duration.ofSeconds(10); + private Duration requestTimeout = Duration.ofSeconds(10); + private int keyTtl = 60 * 1000; + + public Builder jwksUri(String jwksUri) { + this.jwksUri = URI.create(jwksUri); + return this; + } + + public Builder connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder requestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + public Builder keyTtl(int keyTtl) { + this.keyTtl = keyTtl; + return this; + } + + public JwksKeyLocator build() { + return new JwksKeyLocator(this); + } + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java deleted file mode 100644 index 02016a8..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java +++ /dev/null @@ -1,147 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.Key; -import java.security.KeyFactory; -import java.security.spec.KeySpec; -import java.security.spec.RSAPublicKeySpec; -import java.time.Duration; -import java.util.Base64; -import java.util.Base64.Decoder; -import java.util.Optional; -import reactor.core.Exceptions; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - -public final class JwksKeyProvider implements KeyProvider { - - private static final ObjectMapper OBJECT_MAPPER = newObjectMapper(); - - private String jwksUri; - private Duration connectTimeout = Duration.ofSeconds(10); - private Duration readTimeout = Duration.ofSeconds(10); - - public JwksKeyProvider() {} - - private JwksKeyProvider(JwksKeyProvider other) { - this.jwksUri = other.jwksUri; - this.connectTimeout = other.connectTimeout; - this.readTimeout = other.readTimeout; - } - - /** - * Setter for jwksUri. - * - * @param jwksUri jwksUri - * @return new instance with applied setting - */ - public JwksKeyProvider jwksUri(String jwksUri) { - final JwksKeyProvider c = copy(); - c.jwksUri = jwksUri; - return c; - } - - /** - * Setter for connectTimeout. - * - * @param connectTimeout connectTimeout - * @return new instance with applied setting - */ - public JwksKeyProvider connectTimeout(Duration connectTimeout) { - final JwksKeyProvider c = copy(); - c.connectTimeout = connectTimeout; - return c; - } - - /** - * Setter for readTimeout. - * - * @param readTimeout readTimeout - * @return new instance with applied setting - */ - public JwksKeyProvider readTimeout(Duration readTimeout) { - final JwksKeyProvider c = copy(); - c.readTimeout = readTimeout; - return c; - } - - @Override - public Mono findKey(String kid) { - return computeKey(kid) - .switchIfEmpty(Mono.error(new KeyNotFoundException("Key was not found, kid: " + kid))) - .subscribeOn(Schedulers.boundedElastic()) - .publishOn(Schedulers.boundedElastic()); - } - - private Mono computeKey(String kid) { - return Mono.fromCallable(this::computeKeyList) - .flatMap(list -> Mono.justOrEmpty(findRsaKey(list, kid))) - .onErrorMap(th -> th instanceof KeyProviderException ? th : new KeyProviderException(th)); - } - - private JwkInfoList computeKeyList() throws IOException { - HttpURLConnection httpClient = (HttpURLConnection) new URL(jwksUri).openConnection(); - httpClient.setConnectTimeout((int) connectTimeout.toMillis()); - httpClient.setReadTimeout((int) readTimeout.toMillis()); - - int responseCode = httpClient.getResponseCode(); - if (responseCode != 200) { - throw new KeyProviderException("Not expected response code: " + responseCode); - } - - return toKeyList(httpClient.getInputStream()); - } - - private static JwkInfoList toKeyList(InputStream stream) { - try (InputStream inputStream = new BufferedInputStream(stream)) { - return OBJECT_MAPPER.readValue(inputStream, JwkInfoList.class); - } catch (IOException e) { - throw Exceptions.propagate(e); - } - } - - private Optional findRsaKey(JwkInfoList list, String kid) { - return list.keys().stream() - .filter(k -> kid.equals(k.kid())) - .findFirst() - .map(info -> toRsaPublicKey(info.modulus(), info.exponent())); - } - - static Key toRsaPublicKey(String n, String e) { - Decoder b64Decoder = Base64.getUrlDecoder(); - BigInteger modulus = new BigInteger(1, b64Decoder.decode(n)); - BigInteger exponent = new BigInteger(1, b64Decoder.decode(e)); - KeySpec keySpec = new RSAPublicKeySpec(modulus, exponent); - try { - return KeyFactory.getInstance("RSA").generatePublic(keySpec); - } catch (Exception ex) { - throw Exceptions.propagate(ex); - } - } - - private static ObjectMapper newObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - return mapper; - } - - private JwksKeyProvider copy() { - return new JwksKeyProvider(this); - } -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java index 3346751..163095b 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java @@ -1,24 +1,5 @@ package io.scalecube.security.tokens.jwt; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -public class JwtToken { - - private final Map header; - private final Map body; - - public JwtToken(Map header, Map body) { - this.header = Collections.unmodifiableMap(new HashMap<>(header)); - this.body = Collections.unmodifiableMap(new HashMap<>(body)); - } - - public Map header() { - return header; - } - - public Map body() { - return body; - } -} +public record JwtToken(Map header, Map payload) {} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java new file mode 100644 index 0000000..1db6f82 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java @@ -0,0 +1,25 @@ +package io.scalecube.security.tokens.jwt; + +import java.util.StringJoiner; + +public class JwtTokenException extends RuntimeException { + + public JwtTokenException(String message) { + super(message); + } + + public JwtTokenException(String message, Throwable cause) { + super(message, cause); + } + + public JwtTokenException(Throwable cause) { + super(cause); + } + + @Override + public String toString() { + return new StringJoiner(", ", getClass().getSimpleName() + "[", "]") + .add("errorMessage='" + getMessage() + "'") + .toString(); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java deleted file mode 100644 index 6b3600f..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import java.security.Key; - -public interface JwtTokenParser { - - JwtToken parseToken(); - - JwtToken verifyToken(Key key); -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java deleted file mode 100644 index f5ef270..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -public interface JwtTokenParserFactory { - - JwtTokenParser newParser(String token); -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java index bacce61..fdc8209 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java @@ -1,23 +1,14 @@ package io.scalecube.security.tokens.jwt; -import java.util.Map; import reactor.core.publisher.Mono; public interface JwtTokenResolver { /** - * Parses and returns token claims without verification. - * - * @param token jwt token - * @return parsed claims - */ - Map parseBody(String token); - - /** - * Verifies and returns token claims if everything went ok. + * Verifies and returns token claims. * * @param token jwt token * @return mono result with parsed claims (or error) */ - Mono> resolve(String token); + Mono resolve(String token); } diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java deleted file mode 100644 index 72c679e..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java +++ /dev/null @@ -1,163 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import io.scalecube.security.tokens.jwt.jsonwebtoken.JsonwebtokenParserFactory; -import java.security.Key; -import java.time.Duration; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; - -public final class JwtTokenResolverImpl implements JwtTokenResolver { - - private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenResolver.class); - - private KeyProvider keyProvider; - private JwtTokenParserFactory tokenParserFactory = new JsonwebtokenParserFactory(); - private Scheduler scheduler = Schedulers.boundedElastic(); - private Duration cleanupInterval = Duration.ofSeconds(60); - - private final Map> keyResolutions = new ConcurrentHashMap<>(); - - public JwtTokenResolverImpl() {} - - private JwtTokenResolverImpl(JwtTokenResolverImpl other) { - this.keyProvider = other.keyProvider; - this.tokenParserFactory = other.tokenParserFactory; - this.scheduler = other.scheduler; - this.cleanupInterval = other.cleanupInterval; - } - - /** - * Setter for keyProvider. - * - * @param keyProvider keyProvider - * @return new instance with applied setting - */ - public JwtTokenResolverImpl keyProvider(KeyProvider keyProvider) { - final JwtTokenResolverImpl c = copy(); - c.keyProvider = keyProvider; - return c; - } - - /** - * Setter for tokenParserFactory. - * - * @param tokenParserFactory tokenParserFactory - * @return new instance with applied setting - */ - public JwtTokenResolverImpl tokenParserFactory(JwtTokenParserFactory tokenParserFactory) { - final JwtTokenResolverImpl c = copy(); - c.tokenParserFactory = tokenParserFactory; - return c; - } - - /** - * Setter for scheduler. - * - * @param scheduler scheduler - * @return new instance with applied setting - */ - public JwtTokenResolverImpl scheduler(Scheduler scheduler) { - final JwtTokenResolverImpl c = copy(); - c.scheduler = scheduler; - return c; - } - - /** - * Setter for cleanupInterval. - * - * @param cleanupInterval cleanupInterval - * @return new instance with applied setting - */ - public JwtTokenResolverImpl cleanupInterval(Duration cleanupInterval) { - final JwtTokenResolverImpl c = copy(); - c.cleanupInterval = cleanupInterval; - return c; - } - - @Override - public Map parseBody(String token) { - JwtTokenParser tokenParser = tokenParserFactory.newParser(token); - JwtToken jwtToken = tokenParser.parseToken(); - return jwtToken.body(); - } - - @Override - public Mono> resolve(String token) { - return Mono.defer( - () -> { - JwtTokenParser tokenParser = tokenParserFactory.newParser(token); - JwtToken jwtToken = tokenParser.parseToken(); - - Map header = jwtToken.header(); - String kid = (String) header.get("kid"); - Objects.requireNonNull(kid, "kid is missing"); - - LOGGER.debug("[resolveToken][kid:{}] Resolving token {}", kid, mask(token)); - - // workaround to remove safely on errors - AtomicReference> computedValueHolder = new AtomicReference<>(); - - return findKey(kid, computedValueHolder) - .map(key -> tokenParser.verifyToken(key).body()) - .doOnError(throwable -> cleanup(kid, computedValueHolder)) - .doOnError( - throwable -> - LOGGER.error( - "[resolveToken][kid:{}][{}] Exception occurred: {}", - kid, - mask(token), - throwable.toString())) - .doOnSuccess( - s -> LOGGER.debug("[resolveToken][kid:{}] Resolved token {}", kid, mask(token))); - }); - } - - private Mono findKey(String kid, AtomicReference> computedValueHolder) { - if (cleanupInterval.isZero()) { - return Mono.defer(() -> keyProvider.findKey(kid)).cache(); - } - - return keyResolutions.computeIfAbsent( - kid, - (kid1) -> { - Mono result = - computedValueHolder.updateAndGet( - mono -> - Mono.defer(() -> keyProvider.findKey(kid1)) - .cache() - .doOnError(ex -> keyResolutions.remove(kid1))); - scheduleCleanup(kid, computedValueHolder); - return result; - }); - } - - private void scheduleCleanup(String kid, AtomicReference> computedValueHolder) { - scheduler.schedule( - () -> cleanup(kid, computedValueHolder), cleanupInterval.toMillis(), TimeUnit.MILLISECONDS); - } - - private void cleanup(String kid, AtomicReference> computedValueHolder) { - if (computedValueHolder.get() != null) { - keyResolutions.remove(kid, computedValueHolder.get()); - } - } - - private static String mask(String data) { - if (data == null || data.isEmpty() || data.length() < 5) { - return "*****"; - } - return data.replace(data.substring(2, data.length() - 2), "***"); - } - - private JwtTokenResolverImpl copy() { - return new JwtTokenResolverImpl(this); - } -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyNotFoundException.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyNotFoundException.java deleted file mode 100644 index 8e683ed..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyNotFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -public final class KeyNotFoundException extends RuntimeException { - - public KeyNotFoundException(String s) { - super(s); - } - - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{errorMessage=" + getMessage() + '}'; - } -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java deleted file mode 100644 index 2a3f22d..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import java.security.Key; -import reactor.core.publisher.Mono; - -@FunctionalInterface -public interface KeyProvider { - - /** - * Finds key for jwt token verification. - * - * @param kid key id token attribute - * @return mono result with key (or error) - */ - Mono findKey(String kid); -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java deleted file mode 100644 index 2bb298b..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -public final class KeyProviderException extends RuntimeException { - - public KeyProviderException(String s) { - super(s); - } - - public KeyProviderException(Throwable cause) { - super(cause); - } - - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{errorMessage=" + getMessage() + '}'; - } -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java deleted file mode 100644 index d97764d..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.scalecube.security.tokens.jwt.jsonwebtoken; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.JwtParserBuilder; -import io.scalecube.security.tokens.jwt.JwtToken; -import io.scalecube.security.tokens.jwt.JwtTokenParser; -import java.security.Key; - -public class JsonwebtokenParser implements JwtTokenParser { - - private final String token; - private final String justClaims; - private final JwtParserBuilder parserBuilder; - - /** - * Constructor. - * - * @param token jwt token - * @param justClaims just claims - * @param parserBuilder parser builder - */ - public JsonwebtokenParser(String token, String justClaims, JwtParserBuilder parserBuilder) { - this.token = token; - this.justClaims = justClaims; - this.parserBuilder = parserBuilder; - } - - @Override - public JwtToken parseToken() { - //noinspection rawtypes - Jwt jwt = parserBuilder.build().parseClaimsJwt(justClaims); - //noinspection unchecked - return new JwtToken(jwt.getHeader(), jwt.getBody()); - } - - @Override - public JwtToken verifyToken(Key key) { - Jws jws = parserBuilder.setSigningKey(key).build().parseClaimsJws(token); - //noinspection unchecked - return new JwtToken(jws.getHeader(), jws.getBody()); - } -} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java deleted file mode 100644 index e3ef719..0000000 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.scalecube.security.tokens.jwt.jsonwebtoken; - -import io.jsonwebtoken.JwtParserBuilder; -import io.jsonwebtoken.Jwts; -import io.scalecube.security.tokens.jwt.JwtTokenParser; -import io.scalecube.security.tokens.jwt.JwtTokenParserFactory; - -public class JsonwebtokenParserFactory implements JwtTokenParserFactory { - - @Override - public JwtTokenParser newParser(String token) { - String justClaims = token.substring(0, token.lastIndexOf(".") + 1); - JwtParserBuilder parserBuilder = Jwts.parserBuilder(); - return new JsonwebtokenParser(token, justClaims, parserBuilder); - } -} diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java b/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java deleted file mode 100644 index 51e9412..0000000 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java +++ /dev/null @@ -1,164 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import java.time.Duration; -import java.util.Collections; -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; -import reactor.test.scheduler.VirtualTimeScheduler; - -class JwtTokenResolverTests { - - private static final Map BODY = Collections.singletonMap("aud", "aud"); - - @Test - void testTokenResolver() throws Exception { - JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties"); - - JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); - Mockito.when(tokenParser.parseToken()) - .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithKey.kid), BODY)); - Mockito.when(tokenParser.verifyToken(tokenWithKey.key)) - .thenReturn(Mockito.mock(JwtToken.class)); - - JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); - Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) - .thenReturn(tokenParser); - - KeyProvider keyProvider = Mockito.mock(KeyProvider.class); - Mockito.when(keyProvider.findKey(tokenWithKey.kid)).thenReturn(Mono.just(tokenWithKey.key)); - - JwtTokenResolverImpl tokenResolver = - new JwtTokenResolverImpl() - .keyProvider(keyProvider) - .tokenParserFactory(tokenParserFactory) - .scheduler(VirtualTimeScheduler.create()) - .cleanupInterval(Duration.ofSeconds(3)); - - // N times call resolve - StepVerifier.create(tokenResolver.resolve(tokenWithKey.token).repeat(3)) - .expectNextCount(3) - .thenCancel() - .verify(); - - // check caching, must have been called 1 time - Mockito.verify(keyProvider, Mockito.times(1)).findKey(tokenWithKey.kid); - } - - @Test - void testTokenResolverWithRotatingKey() throws Exception { - JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties"); - JwtTokenWithKey tokenWithKeyAfterRotation = - new JwtTokenWithKey("token-and-pubkey.after-rotation.properties"); - - JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); - Mockito.when(tokenParser.parseToken()) - .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithKey.kid), BODY)) - .thenReturn( - new JwtToken(Collections.singletonMap("kid", tokenWithKeyAfterRotation.kid), BODY)); - - Mockito.when(tokenParser.verifyToken(tokenWithKey.key)) - .thenReturn(Mockito.mock(JwtToken.class)); - Mockito.when(tokenParser.verifyToken(tokenWithKeyAfterRotation.key)) - .thenReturn(Mockito.mock(JwtToken.class)); - - JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); - Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) - .thenReturn(tokenParser); - - KeyProvider keyProvider = Mockito.mock(KeyProvider.class); - Mockito.when(keyProvider.findKey(tokenWithKey.kid)).thenReturn(Mono.just(tokenWithKey.key)); - Mockito.when(keyProvider.findKey(tokenWithKeyAfterRotation.kid)) - .thenReturn(Mono.just(tokenWithKeyAfterRotation.key)); - - JwtTokenResolverImpl tokenResolver = - new JwtTokenResolverImpl() - .keyProvider(keyProvider) - .tokenParserFactory(tokenParserFactory) - .scheduler(VirtualTimeScheduler.create()) - .cleanupInterval(Duration.ofSeconds(3)); - - // Call normal token first - StepVerifier.create(tokenResolver.resolve(tokenWithKey.token)) - .expectNextCount(1) - .expectComplete() - .verify(); - - // Call token after rotation (call N times) - StepVerifier.create(tokenResolver.resolve(tokenWithKeyAfterRotation.token).repeat(3)) - .expectNextCount(3) - .thenCancel() - .verify(); - - // in total must have been called 2 times - Mockito.verify(keyProvider, Mockito.times(1)).findKey(tokenWithKey.kid); - Mockito.verify(keyProvider, Mockito.times(1)).findKey(tokenWithKeyAfterRotation.kid); - } - - @Test - void testTokenResolverWithWrongKey() throws Exception { - JwtTokenWithKey tokenWithWrongKey = new JwtTokenWithKey("token-and-wrong-pubkey.properties"); - - JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); - Mockito.when(tokenParser.parseToken()) - .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithWrongKey.kid), BODY)); - Mockito.when(tokenParser.verifyToken(tokenWithWrongKey.key)).thenThrow(RuntimeException.class); - - JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); - Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) - .thenReturn(tokenParser); - - KeyProvider keyProvider = Mockito.mock(KeyProvider.class); - Mockito.when(keyProvider.findKey(tokenWithWrongKey.kid)) - .thenReturn(Mono.just(tokenWithWrongKey.key)); - - JwtTokenResolverImpl tokenResolver = - new JwtTokenResolverImpl() - .keyProvider(keyProvider) - .tokenParserFactory(tokenParserFactory) - .scheduler(VirtualTimeScheduler.create()) - .cleanupInterval(Duration.ofSeconds(3)); - - // Must fail (retry N times) - StepVerifier.create(tokenResolver.resolve(tokenWithWrongKey.token).retry(1)) - .expectError() - .verify(); - - // failed resolution not stored => keyProvider must have been called 2 times - Mockito.verify(keyProvider, Mockito.times(2)).findKey(tokenWithWrongKey.kid); - } - - @Test - void testTokenResolverWhenKeyProviderFailing() throws Exception { - JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties"); - - JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); - Mockito.when(tokenParser.parseToken()) - .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithKey.kid), BODY)); - Mockito.when(tokenParser.verifyToken(tokenWithKey.key)) - .thenReturn(Mockito.mock(JwtToken.class)); - - JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); - Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) - .thenReturn(tokenParser); - - KeyProvider keyProvider = Mockito.mock(KeyProvider.class); - Mockito.when(keyProvider.findKey(tokenWithKey.kid)).thenThrow(RuntimeException.class); - - JwtTokenResolverImpl tokenResolver = - new JwtTokenResolverImpl() - .keyProvider(keyProvider) - .tokenParserFactory(tokenParserFactory) - .scheduler(VirtualTimeScheduler.create()) - .cleanupInterval(Duration.ofSeconds(3)); - - // Must fail with "hola" (retry N times) - StepVerifier.create(tokenResolver.resolve(tokenWithKey.token).retry(1)).expectError().verify(); - - // failed resolution not stored => keyProvider must have been called 2 times - Mockito.verify(keyProvider, Mockito.times(2)).findKey(tokenWithKey.kid); - } -} diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenWithKey.java b/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenWithKey.java deleted file mode 100644 index 1bfe4a9..0000000 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenWithKey.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import java.security.Key; -import java.util.Properties; - -class JwtTokenWithKey { - - final String token; - final Key key; - final String kid; - - JwtTokenWithKey(String s) throws Exception { - Properties props = new Properties(); - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - props.load(classLoader.getResourceAsStream(s)); - this.token = props.getProperty("token"); - this.kid = props.getProperty("kid"); - this.key = JwksKeyProvider.toRsaPublicKey(props.getProperty("n"), props.getProperty("e")); - } -} diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java index f2b9dab..dd4888e 100644 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java +++ b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java @@ -4,36 +4,40 @@ import com.bettercloud.vault.rest.Rest; import com.bettercloud.vault.rest.RestException; import com.bettercloud.vault.rest.RestResponse; -import java.io.IOException; import java.util.UUID; import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.vault.VaultContainer; -public class VaultEnvironment { +public class VaultEnvironment implements AutoCloseable { private static final String VAULT_TOKEN = "test"; private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; + private static final int PORT = 8200; - private static final GenericContainer VAULT_CONTAINER = + private final GenericContainer vault = new VaultContainer("vault:1.4.0") .withVaultToken(VAULT_TOKEN) .waitingFor(new LogMessageWaitStrategy().withRegEx("^.*Vault server started!.*$")); - private static String vaultAddr; - - public static void start() { - VAULT_CONTAINER.start(); - vaultAddr = "http://localhost:" + VAULT_CONTAINER.getMappedPort(8200); - } - - public static void stop() { - VAULT_CONTAINER.stop(); + private String vaultAddr; + + public static VaultEnvironment start() { + final var environment = new VaultEnvironment(); + try { + final var vault = environment.vault; + vault.start(); + environment.vaultAddr = "http://localhost:" + vault.getMappedPort(PORT); + checkSuccess(vault.execInContainer("vault auth enable userpass".split("\\s")).getExitCode()); + } catch (Exception ex) { + environment.close(); + throw new RuntimeException(ex); + } + return environment; } - public static String generateIdentityToken(String clientToken, String roleName) - throws RestException { + public String generateIdentityToken(String clientToken, String roleName) throws RestException { RestResponse restResponse = new Rest().header(VAULT_TOKEN_HEADER, clientToken).url(oidcToken(roleName)).get(); int status = restResponse.getStatus(); @@ -51,7 +55,7 @@ public static String generateIdentityToken(String clientToken, String roleName) .asString(); } - public static void createIdentityTokenPolicy(String roleName) throws RestException { + public void createIdentityTokenPolicy(String roleName) throws RestException { int status = new Rest() .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) @@ -70,20 +74,15 @@ public static void createIdentityTokenPolicy(String roleName) throws RestExcepti } } - public static String createEntity(final String roleName) - throws IOException, InterruptedException { - + public String createEntity(final String roleName) throws Exception { checkSuccess( - VAULT_CONTAINER.execInContainer("vault auth enable userpass".split("\\s")).getExitCode()); - checkSuccess( - VAULT_CONTAINER + vault .execInContainer( ("vault write auth/userpass/users/abc password=abc policies=" + roleName) .split("\\s")) .getExitCode()); - ExecResult loginExecResult = - VAULT_CONTAINER.execInContainer( + vault.execInContainer( "vault login -format json -method=userpass username=abc password=abc".split("\\s")); checkSuccess(loginExecResult.getExitCode()); return Json.parse(loginExecResult.getStdout().replaceAll("\\r?\\n", "")) @@ -100,7 +99,7 @@ public static void checkSuccess(int exitCode) { } } - public static String createIdentityKey() throws RestException { + public String createIdentityKey() throws RestException { String keyName = UUID.randomUUID().toString(); int status = new Rest() @@ -125,7 +124,7 @@ public static String createIdentityKey() throws RestException { return keyName; } - public static String createIdentityRole(String keyName) throws RestException { + public String createIdentityRole(String keyName) throws RestException { String roleName = UUID.randomUUID().toString(); int status = new Rest() @@ -141,23 +140,28 @@ public static String createIdentityRole(String keyName) throws RestException { return roleName; } - public static String oidcKeyUrl(String keyName) { + public String oidcKeyUrl(String keyName) { return vaultAddr + "/v1/identity/oidc/key/" + keyName; } - public static String oidcRoleUrl(String roleName) { + public String oidcRoleUrl(String roleName) { return vaultAddr + "/v1/identity/oidc/role/" + roleName; } - public static String oidcToken(String roleName) { + public String oidcToken(String roleName) { return vaultAddr + "/v1/identity/oidc/token/" + roleName; } - public static String jwksUri() { + public String jwksUri() { return vaultAddr + "/v1/identity/oidc/.well-known/keys"; } - public static String policiesAclUri(String roleName) { + public String policiesAclUri(String roleName) { return vaultAddr + "/v1/sys/policies/acl/" + roleName; } + + @Override + public void close() { + vault.stop(); + } } diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java new file mode 100644 index 0000000..2f9a022 --- /dev/null +++ b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java @@ -0,0 +1,78 @@ +package io.scalecube.security.tokens.jwt; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.jsonwebtoken.Locator; +import java.security.Key; +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class VaultIdentityTokenTests { + + private static final Duration TIMEOUT = Duration.ofSeconds(3); + + private static VaultEnvironment vaultEnvironment; + + @BeforeAll + static void beforeAll() { + vaultEnvironment = VaultEnvironment.start(); + } + + @AfterAll + static void afterAll() { + if (vaultEnvironment != null) { + vaultEnvironment.close(); + } + } + + @Test + void testResolveTokenSuccessfully() throws Exception { + final var token = generateToken(); + + final var jwtToken = + new JsonwebtokenResolver( + new JwksKeyLocator.Builder() + .jwksUri(vaultEnvironment.jwksUri()) + .connectTimeout(Duration.ofSeconds(3)) + .requestTimeout(Duration.ofSeconds(3)) + .keyTtl(1000) + .build()) + .resolve(token) + .block(TIMEOUT); + + assertNotNull(jwtToken, "jwtToken"); + assertTrue(jwtToken.header().size() > 0, "jwtToken.header: " + jwtToken.header()); + assertTrue(jwtToken.payload().size() > 0, "jwtToken.payload: " + jwtToken.payload()); + } + + @Test + void testJwksKeyLocatorThrowsError() throws Exception { + final var token = generateToken(); + + Locator keyLocator = mock(Locator.class); + when(keyLocator.locate(any())).thenThrow(new RuntimeException("Cannot get key")); + + try { + new JsonwebtokenResolver(keyLocator).resolve(token).block(TIMEOUT); + fail("Expected exception"); + } catch (Exception ex) { + assertNotNull(ex.getMessage()); + assertTrue(ex.getMessage().startsWith("Cannot get key")); + } + } + + private static String generateToken() throws Exception { + String keyName = vaultEnvironment.createIdentityKey(); // oidc/key + String roleName = vaultEnvironment.createIdentityRole(keyName); // oidc/role + vaultEnvironment.createIdentityTokenPolicy(roleName); // policy policyfile.hcl + String clientToken = vaultEnvironment.createEntity(roleName); // onboard entity with policy + return vaultEnvironment.generateIdentityToken(clientToken, roleName); + } +} diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultJwksKeyProviderTests.java b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultJwksKeyProviderTests.java deleted file mode 100644 index b7d508a..0000000 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultJwksKeyProviderTests.java +++ /dev/null @@ -1,101 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import static io.scalecube.security.tokens.jwt.VaultEnvironment.createEntity; -import static io.scalecube.security.tokens.jwt.VaultEnvironment.createIdentityKey; -import static io.scalecube.security.tokens.jwt.VaultEnvironment.createIdentityRole; -import static io.scalecube.security.tokens.jwt.VaultEnvironment.createIdentityTokenPolicy; -import static io.scalecube.security.tokens.jwt.VaultEnvironment.generateIdentityToken; -import static io.scalecube.security.tokens.jwt.VaultEnvironment.jwksUri; -import static org.hamcrest.CoreMatchers.startsWith; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.JwtParserBuilder; -import io.jsonwebtoken.Jwts; -import java.time.Duration; -import java.util.UUID; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.test.StepVerifier; - -class VaultJwksKeyProviderTests { - - private static final Duration TIMEOUT = Duration.ofSeconds(3); - - @BeforeEach - void setup() { - VaultEnvironment.start(); - } - - @AfterEach - void cleanup() { - VaultEnvironment.stop(); - } - - @Test - @DisplayName("Find key successfully") - void testFindKey() throws Exception { - String keyName = createIdentityKey(); // oidc/key - String roleName = createIdentityRole(keyName); // oidc/role - createIdentityTokenPolicy(roleName); // write policy policyfile.hcl - String clientToken = createEntity(roleName); // onboard some entity with policy line above - String token = generateIdentityToken(clientToken, roleName); // oidc/token - String kid = getKid(token); - - JwksKeyProvider keyProvider = new JwksKeyProvider().jwksUri(jwksUri()); - - StepVerifier.create(keyProvider.findKey(kid)) - .expectNextCount(1) - .expectComplete() - .verify(TIMEOUT); - } - - @Test - @DisplayName("Fails to find non-existent key") - void testFindNonExistentKey() throws Exception { - String keyName = createIdentityKey(); // oidc/key - String roleName = createIdentityRole(keyName); // oidc/role - createIdentityTokenPolicy(roleName); // write policy policyfile.hcl - String clientToken = createEntity(roleName); // onboard some entity with policy line above - generateIdentityToken(clientToken, roleName); // oidc/token - - JwksKeyProvider keyProvider = new JwksKeyProvider().jwksUri(jwksUri()); - - StepVerifier.create(keyProvider.findKey(UUID.randomUUID().toString())) - .expectErrorSatisfies( - throwable -> { - Assertions.assertEquals(KeyNotFoundException.class, throwable.getClass()); - MatcherAssert.assertThat(throwable.getMessage(), startsWith("Key was not found")); - }) - .verify(TIMEOUT); - } - - @Test - @DisplayName("Fails to find key on empty environment") - void testKeyNotFoundOnEmptyEnvironment() { - JwksKeyProvider keyProvider = new JwksKeyProvider().jwksUri(jwksUri()); - - StepVerifier.create(keyProvider.findKey(UUID.randomUUID().toString())) - .expectErrorSatisfies( - throwable -> { - Assertions.assertEquals(KeyNotFoundException.class, throwable.getClass()); - MatcherAssert.assertThat(throwable.getMessage(), startsWith("Key was not found")); - }) - .verify(TIMEOUT); - } - - private static String getKid(String token) { - String justClaims = token.substring(0, token.lastIndexOf(".") + 1); - JwtParserBuilder parserBuilder = Jwts.parserBuilder(); - //noinspection rawtypes - Jwt claims = parserBuilder.build().parseClaimsJwt(justClaims); - //noinspection rawtypes - Header header = claims.getHeader(); - return (String) header.get("kid"); - } -} diff --git a/tokens/src/test/resources/token-and-pubkey.after-rotation.properties b/tokens/src/test/resources/token-and-pubkey.after-rotation.properties deleted file mode 100644 index 8668fd6..0000000 --- a/tokens/src/test/resources/token-and-pubkey.after-rotation.properties +++ /dev/null @@ -1,4 +0,0 @@ -kid=bae2ad9c-6f11-a20c-993a-171221826b80 -token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImJhZTJhZDljLTZmMTEtYTIwYy05OTNhLTE3MTIyMTgyNmI4MCJ9.eyJhdWQiOiJ0Q0dRRDFkdUJUZjdST2ZucE1NQ0lDU01xRCIsImV4Y2hhbmdlX2lkIjoxLCJleHAiOjE1ODkxMjUzMjYsImlhdCI6MTU4OTAzODkyNiwiaXNzIjoiaHR0cDovLzAuMC4wLjA6ODIwMC92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInBlcm1pc3Npb25zIjpbIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpuZXdfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjp1cGRhdGVfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjphc3NpZ25fY2FsZW5kYXIiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246YXNzaWduX2NiciIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpyZW1vdmVfY2JyIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOnRyYWRpbmdfYWN0aW9uOmhhbHQiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246dHJhZGluZ19hY3Rpb246cmVzdW1lIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOm9wZXJhdGlvbl9ldmVudHMiXSwic3ViIjoiMDY4OTdiNTAtOGQ5Zi0xZDRkLWEwZjUtOGMxNmQ1ZGU4MzIwIn0.FEEB3cL3k2cFacdBFKCewW5M0BjKwp2uhz_CuLBus7aTqfSG1rtIrViPi_Y9a889kaG5j38FhRkG1fzgvEv7BeE6ar3XfrUtDMY7fvDDIr1LKs58KtK6I-ZTI6MlYVvMNJzOplwuvq45D1BLk9_jSKPG_szSSmbveF6zRloB4Nkm_I5VC2kZ126zhiEBn7SELloFGCti7IUuKG80wlcVFQe9upVr4yaCctTOfPs2NyWngCIp55eVGDBNtVqSgZbDoMpvmq4tJmEQ9in0HZkPAEyuN3Zu8IOZMey0jEIDOtds3Lo5xi65t3-1AapY0JyEI08kB-a8pVppl8o6H9DPhQ -n=yab6iEkwcbS0BBGV3gD_M5IW7cEgSZV5yFkASIcA1Ip53nU98yxivhS1ouCQpbyZ1QMv8W4Xol0S9LJfmbAtfxJ3FY4yp1OQQYXA9KMXW4SDkXpZ7mZWR9DWBrX6U-pmGbYtIUDgjlyzAoSu9HKRQjWnV9MEPn3aaDN8wGNON3BbIkCspF-WMJm2FA_-ZqsuGM-0U2WtMTlJjL5a2BqbYWFBRmMvwJ2akqoScIa2RB50kGCPCGO9L-rXcikGTs6yG3mIVmMBFALkGmHXps9JCT33ky4k5QN1aTuCrxzyL9hID5HWrdvhaBAETStH9zWSgvA1-ovGRjxuApiSg9EFtw -e=AQAB diff --git a/tokens/src/test/resources/token-and-pubkey.properties b/tokens/src/test/resources/token-and-pubkey.properties deleted file mode 100644 index 36ccc16..0000000 --- a/tokens/src/test/resources/token-and-pubkey.properties +++ /dev/null @@ -1,4 +0,0 @@ -kid=c09fef96-6af3-62f7-8c73-a8c4813f1bb5 -token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImMwOWZlZjk2LTZhZjMtNjJmNy04YzczLWE4YzQ4MTNmMWJiNSJ9.eyJhdWQiOiJ0Q0dRRDFkdUJUZjdST2ZucE1NQ0lDU01xRCIsImV4Y2hhbmdlX2lkIjoxLCJleHAiOjE1ODkxMjUyMDUsImlhdCI6MTU4OTAzODgwNSwiaXNzIjoiaHR0cDovLzAuMC4wLjA6ODIwMC92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInBlcm1pc3Npb25zIjpbIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpuZXdfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjp1cGRhdGVfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjphc3NpZ25fY2FsZW5kYXIiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246YXNzaWduX2NiciIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpyZW1vdmVfY2JyIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOnRyYWRpbmdfYWN0aW9uOmhhbHQiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246dHJhZGluZ19hY3Rpb246cmVzdW1lIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOm9wZXJhdGlvbl9ldmVudHMiXSwic3ViIjoiMDY4OTdiNTAtOGQ5Zi0xZDRkLWEwZjUtOGMxNmQ1ZGU4MzIwIn0.gNCXOnjEcq-38oDqWVQcrQ146OfqUAChcQu1vF1UkywZGlVAPGWq1Cvayh2LdbVcCLQK5z-ixGfnAzuvAlTn1rrrUb31bgc39DM8-DLjrngvo37n66OI15PKN0aBptnIfpBzwOIxhyUuFKbkupvfEEdEE6k1ClZ3K-4cCzzG8Ec3A80D8F-ntNPv_5aGECSXr8lccoCF0-k_5YyW2r4NX9klAgIIfIbi8UVAU6ikReh8PxSpn8JtlMt7v9CW4gI4uNZfywzC7BXUWQIvf8R9K6OkeoZ3jCFPsrocqtokOCixND2rp1OVe9_7g-FI4XY5GTg06lp832Gvwa7-C2nb_Q -n=zD_WVTbF_bOnYaoGJcJdcZOVZzJTWIXoU4aY_2orS5mcOLC519oU-Pa-i51O_q2l7JHJmYA4ZisH_NMrAPblDFXYB4OIgec8IvHvuS7kn66EgsoLmZu_Xzs4VM_610WwgPIIo3jpEqgBI8dIZEbGUkWKBwlV2G-uY16ftk_sKD84_2cZTVsnBReC_b8JFdkDvmC6KDTDoQsStehvSnOLcby6acbdnoaka0M5V2pXElReDhNKx-NXF4E26-HedGKmys5a0IknF70-Jp5rK5x9Y3vCVcuh7Eyz6zAMB2ovzTCqHTLHuN0BY6SCgy5zZ-S-C-ytNinv41Qkf80BEDyaww -e=AQAB diff --git a/tokens/src/test/resources/token-and-wrong-pubkey.properties b/tokens/src/test/resources/token-and-wrong-pubkey.properties deleted file mode 100644 index e90d768..0000000 --- a/tokens/src/test/resources/token-and-wrong-pubkey.properties +++ /dev/null @@ -1,4 +0,0 @@ -kid=c09fef96-6af3-62f7-8c73-a8c4813f1bb5 -token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImMwOWZlZjk2LTZhZjMtNjJmNy04YzczLWE4YzQ4MTNmMWJiNSJ9.eyJhdWQiOiJ0Q0dRRDFkdUJUZjdST2ZucE1NQ0lDU01xRCIsImV4Y2hhbmdlX2lkIjoxLCJleHAiOjE1ODkxMjUyMDUsImlhdCI6MTU4OTAzODgwNSwiaXNzIjoiaHR0cDovLzAuMC4wLjA6ODIwMC92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInBlcm1pc3Npb25zIjpbIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpuZXdfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjp1cGRhdGVfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjphc3NpZ25fY2FsZW5kYXIiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246YXNzaWduX2NiciIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpyZW1vdmVfY2JyIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOnRyYWRpbmdfYWN0aW9uOmhhbHQiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246dHJhZGluZ19hY3Rpb246cmVzdW1lIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOm9wZXJhdGlvbl9ldmVudHMiXSwic3ViIjoiMDY4OTdiNTAtOGQ5Zi0xZDRkLWEwZjUtOGMxNmQ1ZGU4MzIwIn0.gNCXOnjEcq-38oDqWVQcrQ146OfqUAChcQu1vF1UkywZGlVAPGWq1Cvayh2LdbVcCLQK5z-ixGfnAzuvAlTn1rrrUb31bgc39DM8-DLjrngvo37n66OI15PKN0aBptnIfpBzwOIxhyUuFKbkupvfEEdEE6k1ClZ3K-4cCzzG8Ec3A80D8F-ntNPv_5aGECSXr8lccoCF0-k_5YyW2r4NX9klAgIIfIbi8UVAU6ikReh8PxSpn8JtlMt7v9CW4gI4uNZfywzC7BXUWQIvf8R9K6OkeoZ3jCFPsrocqtokOCixND2rp1OVe9_7g-FI4XY5GTg06lp832Gvwa7-C2nb_Q -n=4aegHcKayVNuDtTR3XPQ5FwXI7MzBMYg4jS_Yden_r0LqaRiPeetTk9sqEhbBVAf8dLTBk1J9LtlQYStNisui0y-arK4lIPWs2wYWtIrZ3UYBoe8EkdDEiWzAVAhSgwA3jIHQrDsfyVjsUTeIgL7hkvIAXf3BIVOHtQqe12vvjHeruOvoea_wk5rftpsFyMDg_40XcgcwPWMhfMI3Uq6kbo-uC1D2WT12k5AJp_qJXRl_qQqWj3swmEgRfeAxrRx8iCWhG59YgIAwf0PUYEV5b8ni4ofnfu3_pLQ7TiDG16BoNBPn_GVcH-zJJtoSups9MY3N2--VmHhREV1Om_Q9Q -e=AQAB From 40938c33a4d5c7fa31e1c277f81241f105897106 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 14:24:23 +0200 Subject: [PATCH 3/9] WIP --- pom.xml | 20 +++-------- tokens/pom.xml | 8 ++--- .../tokens/jwt/JsonwebtokenResolver.java | 36 ++++++++++++++----- .../security/tokens/jwt/JwtTokenResolver.java | 4 +-- .../tokens/jwt/VaultIdentityTokenTests.java | 12 ++++--- tokens/src/test/resources/log4j2-test.xml | 23 ++++++++++++ 6 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 tokens/src/test/resources/log4j2-test.xml diff --git a/pom.xml b/pom.xml index af61e98..6bbf915 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,6 @@ - 2020.0.32 5.1.0 2.18.2 1.7.36 @@ -49,7 +48,7 @@ 5.8.2 1.3 2.17.2 - 1.20.1 + 1.20.4 https://maven.pkg.github.com/scalecube/scalecube-security @@ -64,14 +63,6 @@ vault-java-driver ${vault-java-driver.version} - - - io.projectreactor - reactor-bom - ${reactor.version} - pom - import - org.slf4j @@ -113,8 +104,10 @@ org.testcontainers - vault + testcontainers-bom ${testcontainers.version} + pom + import @@ -151,11 +144,6 @@ ${hamcrest.version} test - - io.projectreactor - reactor-test - test - org.testcontainers vault diff --git a/tokens/pom.xml b/tokens/pom.xml index 5a6caf5..43aee44 100644 --- a/tokens/pom.xml +++ b/tokens/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -11,10 +13,6 @@ scalecube-security-tokens - - io.projectreactor - reactor-core - io.jsonwebtoken jjwt-api diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java index 7f1e9d1..606e592 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java @@ -3,13 +3,13 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Locator; import java.security.Key; +import java.util.concurrent.CompletableFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; public class JsonwebtokenResolver implements JwtTokenResolver { - private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenResolver.class); + private static final Logger LOGGER = LoggerFactory.getLogger(JsonwebtokenResolver.class); private final Locator keyLocator; @@ -18,13 +18,31 @@ public JsonwebtokenResolver(Locator keyLocator) { } @Override - public Mono resolve(String token) { - return Mono.fromCallable( - () -> { - final var claimsJws = - Jwts.parser().keyLocator(keyLocator).build().parseSignedClaims(token); - return new JwtToken(claimsJws.getHeader(), claimsJws.getPayload()); - }); + public CompletableFuture resolve(String token) { + return CompletableFuture.supplyAsync( + () -> { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Resolve token: {}", mask(token)); + } + + final var claimsJws = + Jwts.parser().keyLocator(keyLocator).build().parseSignedClaims(token); + + return new JwtToken(claimsJws.getHeader(), claimsJws.getPayload()); + }) + .whenComplete( + (jwtToken, ex) -> { + if (jwtToken != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Resolved token: {}", mask(token)); + } + } + if (ex != null) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Failed to resolve token: {}, cause: {}", mask(token), ex.toString()); + } + } + }); } private static String mask(String data) { diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java index fdc8209..4c5a61a 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java @@ -1,6 +1,6 @@ package io.scalecube.security.tokens.jwt; -import reactor.core.publisher.Mono; +import java.util.concurrent.CompletableFuture; public interface JwtTokenResolver { @@ -10,5 +10,5 @@ public interface JwtTokenResolver { * @param token jwt token * @return mono result with parsed claims (or error) */ - Mono resolve(String token); + CompletableFuture resolve(String token); } diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java index 2f9a022..70d8067 100644 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java +++ b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java @@ -10,14 +10,14 @@ import io.jsonwebtoken.Locator; import java.security.Key; import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class VaultIdentityTokenTests { - private static final Duration TIMEOUT = Duration.ofSeconds(3); - private static VaultEnvironment vaultEnvironment; @BeforeAll @@ -45,7 +45,7 @@ void testResolveTokenSuccessfully() throws Exception { .keyTtl(1000) .build()) .resolve(token) - .block(TIMEOUT); + .get(3, TimeUnit.SECONDS); assertNotNull(jwtToken, "jwtToken"); assertTrue(jwtToken.header().size() > 0, "jwtToken.header: " + jwtToken.header()); @@ -60,9 +60,11 @@ void testJwksKeyLocatorThrowsError() throws Exception { when(keyLocator.locate(any())).thenThrow(new RuntimeException("Cannot get key")); try { - new JsonwebtokenResolver(keyLocator).resolve(token).block(TIMEOUT); + new JsonwebtokenResolver(keyLocator).resolve(token).get(3, TimeUnit.SECONDS); fail("Expected exception"); - } catch (Exception ex) { + } catch (ExecutionException e) { + final var ex = e.getCause(); + assertNotNull(ex); assertNotNull(ex.getMessage()); assertTrue(ex.getMessage().startsWith("Cannot get key")); } diff --git a/tokens/src/test/resources/log4j2-test.xml b/tokens/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..67e98bb --- /dev/null +++ b/tokens/src/test/resources/log4j2-test.xml @@ -0,0 +1,23 @@ + + + + + %level{length=1} %d{ISO8601} %c{1.} %m [%t]%n + + + + + + + + + + + + + + + + + + From 8e64587b695aab12e136cc75059e297c77890c54 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 14:36:16 +0200 Subject: [PATCH 4/9] WIP --- pom.xml | 1 + tests/pom.xml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/pom.xml diff --git a/pom.xml b/pom.xml index 6bbf915..07afe36 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ tokens vault + tests diff --git a/tests/pom.xml b/tests/pom.xml new file mode 100644 index 0000000..d2745cc --- /dev/null +++ b/tests/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + io.scalecube + scalecube-security-parent + 1.1.0-SNAPSHOT + + + scalecube-security-tests + + + + From ad1117ee51cd7a89b738c93899cc73f00e7c29e8 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 14:51:34 +0200 Subject: [PATCH 5/9] WIP --- tests/pom.xml | 19 ++++++++++++++++++- .../environment}/VaultEnvironment.java | 2 +- .../tokens/jwt/VaultIdentityTokenTests.java | 6 ++++-- .../src/test/resources/log4j2-test.xml | 0 tokens/pom.xml | 11 ----------- 5 files changed, 23 insertions(+), 15 deletions(-) rename {tokens/src/test/java/io/scalecube/security/tokens/jwt => tests/src/test/java/io/scalecube/security/environment}/VaultEnvironment.java (99%) rename {tokens => tests}/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java (89%) rename {tokens => tests}/src/test/resources/log4j2-test.xml (100%) diff --git a/tests/pom.xml b/tests/pom.xml index d2745cc..624a34a 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -12,6 +12,23 @@ scalecube-security-tests - + + + io.scalecube + scalecube-security-tokens + ${project.parent.version} + + + + org.testcontainers + vault + test + + + com.bettercloud + vault-java-driver + test + + diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java similarity index 99% rename from tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java rename to tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java index dd4888e..06781d3 100644 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java +++ b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java @@ -1,4 +1,4 @@ -package io.scalecube.security.tokens.jwt; +package io.scalecube.security.environment; import com.bettercloud.vault.json.Json; import com.bettercloud.vault.rest.Rest; diff --git a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java b/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java similarity index 89% rename from tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java rename to tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java index 70d8067..6f187af 100644 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java +++ b/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java @@ -8,11 +8,13 @@ import static org.mockito.Mockito.when; import io.jsonwebtoken.Locator; +import io.scalecube.security.environment.VaultEnvironment; import java.security.Key; import java.time.Duration; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -48,8 +50,8 @@ void testResolveTokenSuccessfully() throws Exception { .get(3, TimeUnit.SECONDS); assertNotNull(jwtToken, "jwtToken"); - assertTrue(jwtToken.header().size() > 0, "jwtToken.header: " + jwtToken.header()); - assertTrue(jwtToken.payload().size() > 0, "jwtToken.payload: " + jwtToken.payload()); + Assertions.assertTrue(jwtToken.header().size() > 0, "jwtToken.header: " + jwtToken.header()); + Assertions.assertTrue(jwtToken.payload().size() > 0, "jwtToken.payload: " + jwtToken.payload()); } @Test diff --git a/tokens/src/test/resources/log4j2-test.xml b/tests/src/test/resources/log4j2-test.xml similarity index 100% rename from tokens/src/test/resources/log4j2-test.xml rename to tests/src/test/resources/log4j2-test.xml diff --git a/tokens/pom.xml b/tokens/pom.xml index 43aee44..b24af2d 100644 --- a/tokens/pom.xml +++ b/tokens/pom.xml @@ -29,17 +29,6 @@ org.slf4j slf4j-api - - - org.testcontainers - vault - test - - - com.bettercloud - vault-java-driver - test - From e14272511a7779a10b4d790403fec80cd3ea9916 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 17:12:43 +0200 Subject: [PATCH 6/9] Added more ITs --- tests/pom.xml | 5 + .../environment/VaultEnvironment.java | 165 ++++++++++------ .../tokens/jwt/VaultIdentityTokenTests.java | 5 +- .../vault/VaultServiceTokenTests.java | 180 ++++++++++++++++++ tests/src/test/resources/log4j2-test.xml | 2 + .../vault/VaultServiceRolesInstaller.java | 20 +- .../vault/VaultServiceTokenSupplier.java | 11 +- 7 files changed, 304 insertions(+), 84 deletions(-) create mode 100644 tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java diff --git a/tests/pom.xml b/tests/pom.xml index 624a34a..cd237ce 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -18,6 +18,11 @@ scalecube-security-tokens ${project.parent.version} + + io.scalecube + scalecube-security-vault + ${project.parent.version} + org.testcontainers diff --git a/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java index 06781d3..7f28a92 100644 --- a/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java +++ b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java @@ -1,5 +1,8 @@ package io.scalecube.security.environment; +import static org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; + import com.bettercloud.vault.json.Json; import com.bettercloud.vault.rest.Rest; import com.bettercloud.vault.rest.RestException; @@ -12,7 +15,7 @@ public class VaultEnvironment implements AutoCloseable { - private static final String VAULT_TOKEN = "test"; + private static final String VAULT_TOKEN = UUID.randomUUID().toString(); private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; private static final int PORT = 8200; @@ -37,11 +40,20 @@ public static VaultEnvironment start() { return environment; } - public String generateIdentityToken(String clientToken, String roleName) throws RestException { - RestResponse restResponse = - new Rest().header(VAULT_TOKEN_HEADER, clientToken).url(oidcToken(roleName)).get(); - int status = restResponse.getStatus(); + public String vaultAddr() { + return vaultAddr; + } + + public String generateIdentityToken(String clientToken, String roleName) { + RestResponse restResponse; + try { + restResponse = + new Rest().header(VAULT_TOKEN_HEADER, clientToken).url(oidcToken(roleName)).get(); + } catch (RestException e) { + throw new RuntimeException(e); + } + int status = restResponse.getStatus(); if (status != 200 && status != 204) { throw new IllegalStateException( "Unexpected status code on identity token creation: " + status); @@ -55,18 +67,22 @@ public String generateIdentityToken(String clientToken, String roleName) throws .asString(); } - public void createIdentityTokenPolicy(String roleName) throws RestException { - int status = - new Rest() - .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) - .url(policiesAclUri(roleName)) - .body( - ("{\"policy\":\"path \\\"identity/oidc/token/" - + roleName - + "\\\" {capabilities=[\\\"create\\\", \\\"read\\\"]}\"}") - .getBytes()) - .post() - .getStatus(); + public void createIdentityTokenPolicy(String roleName) { + int status; + try { + status = + new Rest() + .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) + .url(policiesAclUri(roleName)) + .body( + ("{\"policy\":\"path \\\"identity/oidc/*" + + "\\\" {capabilities=[\\\"create\\\", \\\"read\\\"]}\"}") + .getBytes()) + .post() + .getStatus(); + } catch (RestException e) { + throw new RuntimeException(e); + } if (status != 200 && status != 204) { throw new IllegalStateException( @@ -74,23 +90,38 @@ public void createIdentityTokenPolicy(String roleName) throws RestException { } } - public String createEntity(final String roleName) throws Exception { - checkSuccess( - vault - .execInContainer( - ("vault write auth/userpass/users/abc password=abc policies=" + roleName) - .split("\\s")) - .getExitCode()); - ExecResult loginExecResult = - vault.execInContainer( - "vault login -format json -method=userpass username=abc password=abc".split("\\s")); - checkSuccess(loginExecResult.getExitCode()); - return Json.parse(loginExecResult.getStdout().replaceAll("\\r?\\n", "")) - .asObject() - .get("auth") - .asObject() - .get("client_token") - .asString(); + public String login() { + try { + String username = randomAlphabetic(5); + String policy = randomAlphanumeric(10); + + // add policy + createIdentityTokenPolicy(policy); + + // create user and login + checkSuccess( + vault + .execInContainer( + ("vault write auth/userpass/users/" + + username + + " password=abc policies=" + + policy) + .split("\\s")) + .getExitCode()); + ExecResult loginExecResult = + vault.execInContainer( + ("vault login -format json -method=userpass username=" + username + " password=abc") + .split("\\s")); + checkSuccess(loginExecResult.getExitCode()); + return Json.parse(loginExecResult.getStdout().replaceAll("\\r?\\n", "")) + .asObject() + .get("auth") + .asObject() + .get("client_token") + .asString(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } } public static void checkSuccess(int exitCode) { @@ -99,24 +130,30 @@ public static void checkSuccess(int exitCode) { } } - public String createIdentityKey() throws RestException { - String keyName = UUID.randomUUID().toString(); - int status = - new Rest() - .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) - .url(oidcKeyUrl(keyName)) - .body( - ("{\"rotation_period\":\"" - + "1m" - + "\", " - + "\"verification_ttl\": \"" - + "1m" - + "\", " - + "\"allowed_client_ids\": \"*\", " - + "\"algorithm\": \"RS256\"}") - .getBytes()) - .post() - .getStatus(); + public String createIdentityKey() { + String keyName = randomAlphanumeric(10); + + int status; + try { + status = + new Rest() + .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) + .url(oidcKeyUrl(keyName)) + .body( + ("{\"rotation_period\":\"" + + "1m" + + "\", " + + "\"verification_ttl\": \"" + + "1m" + + "\", " + + "\"allowed_client_ids\": \"*\", " + + "\"algorithm\": \"RS256\"}") + .getBytes()) + .post() + .getStatus(); + } catch (RestException e) { + throw new RuntimeException(e); + } if (status != 200 && status != 204) { throw new IllegalStateException("Unexpected status code on oidc/key creation: " + status); @@ -124,15 +161,21 @@ public String createIdentityKey() throws RestException { return keyName; } - public String createIdentityRole(String keyName) throws RestException { - String roleName = UUID.randomUUID().toString(); - int status = - new Rest() - .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) - .url(oidcRoleUrl(roleName)) - .body(("{\"key\":\"" + keyName + "\",\"ttl\": \"" + "1h" + "\"}").getBytes()) - .post() - .getStatus(); + public String createIdentityRole(String keyName) { + String roleName = randomAlphanumeric(10); + + int status; + try { + status = + new Rest() + .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) + .url(oidcRoleUrl(roleName)) + .body(("{\"key\":\"" + keyName + "\",\"ttl\": \"" + "1h" + "\"}").getBytes()) + .post() + .getStatus(); + } catch (RestException e) { + throw new RuntimeException(e); + } if (status != 200 && status != 204) { throw new IllegalStateException("Unexpected status code on oidc/role creation: " + status); diff --git a/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java b/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java index 6f187af..b37fee8 100644 --- a/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java +++ b/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java @@ -72,11 +72,10 @@ void testJwksKeyLocatorThrowsError() throws Exception { } } - private static String generateToken() throws Exception { + private static String generateToken() { String keyName = vaultEnvironment.createIdentityKey(); // oidc/key String roleName = vaultEnvironment.createIdentityRole(keyName); // oidc/role - vaultEnvironment.createIdentityTokenPolicy(roleName); // policy policyfile.hcl - String clientToken = vaultEnvironment.createEntity(roleName); // onboard entity with policy + String clientToken = vaultEnvironment.login(); // onboard entity with policy return vaultEnvironment.generateIdentityToken(clientToken, roleName); } } diff --git a/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java new file mode 100644 index 0000000..35a2f0d --- /dev/null +++ b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java @@ -0,0 +1,180 @@ +package io.scalecube.security.vault; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import io.scalecube.security.environment.VaultEnvironment; +import io.scalecube.security.tokens.jwt.JsonwebtokenResolver; +import io.scalecube.security.tokens.jwt.JwksKeyLocator; +import io.scalecube.security.vault.VaultServiceRolesInstaller.ServiceRoles; +import io.scalecube.security.vault.VaultServiceRolesInstaller.ServiceRoles.Role; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class VaultServiceTokenTests { + + private static VaultEnvironment vaultEnvironment; + + @BeforeAll + static void beforeAll() { + vaultEnvironment = VaultEnvironment.start(); + } + + @AfterAll + static void afterAll() { + if (vaultEnvironment != null) { + vaultEnvironment.close(); + } + } + + @Test + void testGetServiceTokenUsingWrongCredentials() throws Exception { + final var serviceTokenSupplier = + new VaultServiceTokenSupplier.Builder() + .vaultAddress(vaultEnvironment.vaultAddr()) + .vaultTokenSupplier(() -> completedFuture(randomAlphabetic(16))) + .serviceRole(randomAlphabetic(16)) + .serviceTokenNameBuilder((role, attributes) -> role) + .build(); + + try { + serviceTokenSupplier.getToken(Collections.emptyMap()).get(3, TimeUnit.SECONDS); + fail("Exception expected"); + } catch (ExecutionException e) { + final var ex = e.getCause(); + assertNotNull(ex); + assertNotNull(ex.getMessage()); + assertTrue( + ex.getMessage().contains("Failed to get service token, status=403"), "Exception: " + ex); + } + } + + @Test + void testGetNonExistingServiceToken() throws Exception { + final var nonExistingServiceRole = "non-existing-role-" + System.currentTimeMillis(); + + final var serviceTokenSupplier = + new VaultServiceTokenSupplier.Builder() + .vaultAddress(vaultEnvironment.vaultAddr()) + .vaultTokenSupplier(() -> completedFuture(vaultEnvironment.login())) + .serviceRole(nonExistingServiceRole) + .serviceTokenNameBuilder((role, attributes) -> role) + .build(); + + try { + serviceTokenSupplier.getToken(Collections.emptyMap()).get(3, TimeUnit.SECONDS); + fail("Exception expected"); + } catch (ExecutionException e) { + final var ex = e.getCause(); + assertNotNull(ex); + assertNotNull(ex.getMessage()); + assertTrue( + ex.getMessage().contains("Failed to get service token, status=400"), "Exception: " + ex); + } + } + + @Test + void testGetServiceTokenByWrongServiceRole() throws Exception { + final var now = System.currentTimeMillis(); + final var serviceRole1 = "role1-" + now; + final var serviceRole2 = "role2-" + now; + final var serviceRole3 = "role3-" + now; + + new VaultServiceRolesInstaller.Builder() + .vaultAddress(vaultEnvironment.vaultAddr()) + .vaultTokenSupplier(() -> completedFuture(vaultEnvironment.login())) + .keyNameSupplier(() -> "key-" + now) + .roleNameBuilder(roleName -> roleName + "-" + now) + .serviceRolesSources( + List.of( + () -> + new ServiceRoles() + .roles( + List.of( + newServiceRole(serviceRole1), + newServiceRole(serviceRole2), + newServiceRole(serviceRole3))))) + .build() + .install(); + + final var serviceTokenSupplier = + new VaultServiceTokenSupplier.Builder() + .vaultAddress(vaultEnvironment.vaultAddr()) + .vaultTokenSupplier(() -> completedFuture(vaultEnvironment.login())) + .serviceRole(serviceRole1) + .serviceTokenNameBuilder((role, attributes) -> role) + .build(); + + try { + serviceTokenSupplier.getToken(Collections.emptyMap()).get(3, TimeUnit.SECONDS); + fail("Exception expected"); + } catch (ExecutionException e) { + final var ex = e.getCause(); + assertNotNull(ex); + assertNotNull(ex.getMessage()); + assertTrue( + ex.getMessage().contains("Failed to get service token, status=400"), "Exception: " + ex); + } + } + + @Test + void testGetServiceTokenSuccessfully() throws Exception { + final var now = System.currentTimeMillis(); + final var serviceRole = "role-" + now; + final var tags = Map.of("type", "ops", "ns", "develop"); + + new VaultServiceRolesInstaller.Builder() + .vaultAddress(vaultEnvironment.vaultAddr()) + .vaultTokenSupplier(() -> completedFuture(vaultEnvironment.login())) + .keyNameSupplier(() -> "key-" + now) + .roleNameBuilder(role -> toQualifiedName(role, tags)) + .serviceRolesSources( + List.of(() -> new ServiceRoles().roles(List.of(newServiceRole(serviceRole))))) + .build() + .install(); + + final var serviceTokenSupplier = + new VaultServiceTokenSupplier.Builder() + .vaultAddress(vaultEnvironment.vaultAddr()) + .vaultTokenSupplier(() -> completedFuture(vaultEnvironment.login())) + .serviceRole(serviceRole) + .serviceTokenNameBuilder(VaultServiceTokenTests::toQualifiedName) + .build(); + + final var serviceToken = serviceTokenSupplier.getToken(tags).get(3, TimeUnit.SECONDS); + assertNotNull(serviceToken, "serviceToken"); + + // Verify serviceToken + + final var jwtToken = + new JsonwebtokenResolver( + new JwksKeyLocator.Builder().jwksUri(vaultEnvironment.jwksUri()).build()) + .resolve(serviceToken) + .get(3, TimeUnit.SECONDS); + + assertNotNull(jwtToken, "jwtToken"); + Assertions.assertTrue(jwtToken.header().size() > 0, "jwtToken.header: " + jwtToken.header()); + Assertions.assertTrue(jwtToken.payload().size() > 0, "jwtToken.payload: " + jwtToken.payload()); + } + + private static String toQualifiedName(String role, Map tags) { + return role + "-" + tags.get("type") + "-" + tags.get("ns"); + } + + private static Role newServiceRole(String name) { + final var role = new ServiceRoles.Role(); + role.role(name); + role.permissions(List.of("read", "write")); + return role; + } +} diff --git a/tests/src/test/resources/log4j2-test.xml b/tests/src/test/resources/log4j2-test.xml index 67e98bb..0dbe306 100644 --- a/tests/src/test/resources/log4j2-test.xml +++ b/tests/src/test/resources/log4j2-test.xml @@ -14,6 +14,8 @@ + + diff --git a/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java index 2af6bb1..09150dd 100644 --- a/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java @@ -7,13 +7,11 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import io.scalecube.security.vault.VaultServiceRolesInstaller.ServiceRoles.Role; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; -import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; @@ -39,7 +37,7 @@ public class VaultServiceRolesInstaller { new ObjectMapper(new YAMLFactory()).setVisibility(PropertyAccessor.FIELD, Visibility.ANY); private final String vaultAddress; - private final CompletableFuture vaultTokenSupplier; + private final Supplier> vaultTokenSupplier; private final Supplier keyNameSupplier; private final Function roleNameBuilder; private final List> serviceRolesSources; @@ -82,6 +80,7 @@ public void install() { try { vaultTokenSupplier + .get() .thenAcceptAsync( token -> { final var rest = new Rest().header(VAULT_TOKEN_HEADER, token); @@ -121,7 +120,7 @@ private ServiceRoles loadServiceRoles() { return null; } - private static void verifyOk(int status) { + private static void awaitSuccess(int status) { if (status != 200 && status != 204) { throw new IllegalStateException("Not expected status returned, status=" + status); } @@ -140,7 +139,7 @@ private void createVaultIdentityKey(Rest rest, String keyName) { .getBytes(); try { - verifyOk(rest.body(body).post().getStatus()); + awaitSuccess(rest.body(body).post().getStatus()); } catch (RestException e) { throw new RuntimeException(e); } @@ -159,7 +158,7 @@ private void createVaultIdentityRole( .getBytes(); try { - verifyOk(rest.body(body).post().getStatus()); + awaitSuccess(rest.body(body).post().getStatus()); } catch (RestException e) { throw new RuntimeException(e); } @@ -355,7 +354,7 @@ public String toString() { public static class Builder { private String vaultAddress; - private CompletableFuture vaultTokenSupplier; + private Supplier> vaultTokenSupplier; private Supplier keyNameSupplier; private Function roleNameBuilder; private List> serviceRolesSources = DEFAULT_SERVICE_ROLES_SOURCES; @@ -373,7 +372,7 @@ public Builder vaultAddress(String vaultAddress) { return this; } - public Builder vaultTokenSupplier(CompletableFuture vaultTokenSupplier) { + public Builder vaultTokenSupplier(Supplier> vaultTokenSupplier) { this.vaultTokenSupplier = vaultTokenSupplier; return this; } @@ -393,11 +392,6 @@ public Builder serviceRolesSources(List> serviceRolesSour return this; } - public Builder serviceRolesSources(Supplier... serviceRolesSources) { - this.serviceRolesSources = Arrays.asList(serviceRolesSources); - return this; - } - public Builder keyAlgorithm(String keyAlgorithm) { this.keyAlgorithm = keyAlgorithm; return this; diff --git a/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java index 4161fae..b1ecb64 100644 --- a/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java @@ -58,7 +58,10 @@ private static String rpcGetToken(String uri, String vaultToken) { final RestResponse response = new Rest().header(VAULT_TOKEN_HEADER, vaultToken).url(uri).get(); - verifyOk(response.getStatus()); + int status = response.getStatus(); + if (status != 200) { + throw new IllegalStateException("Failed to get service token, status=" + status); + } return Json.parse(new String(response.getBody())) .asObject() @@ -71,12 +74,6 @@ private static String rpcGetToken(String uri, String vaultToken) { } } - private static void verifyOk(int status) { - if (status != 200) { - throw new IllegalStateException("Not expected status returned, status=" + status); - } - } - private String toServiceTokenUri(Map tags) { return new StringJoiner("/", vaultAddress, "") .add("/v1/identity/oidc/token") From fccb051d1290313c3621fc6f32044ea80b27b982 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 19:33:42 +0200 Subject: [PATCH 7/9] Cosmetic changes --- .../environment/LoggingExtension.java | 54 +++++++++++++++++++ .../environment/VaultEnvironment.java | 8 +++ .../tokens/jwt/VaultIdentityTokenTests.java | 10 ++-- .../vault/VaultServiceTokenTests.java | 7 +-- .../org.junit.jupiter.api.extension.Extension | 1 + .../test/resources/junit-platform.properties | 1 + .../tokens/jwt/JsonwebtokenResolver.java | 15 +++--- .../security/tokens/jwt/JwksKeyLocator.java | 8 ++- .../vault/VaultServiceRolesInstaller.java | 24 ++++----- .../vault/VaultServiceTokenSupplier.java | 23 ++++---- 10 files changed, 107 insertions(+), 44 deletions(-) create mode 100644 tests/src/test/java/io/scalecube/security/environment/LoggingExtension.java create mode 100644 tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 tests/src/test/resources/junit-platform.properties diff --git a/tests/src/test/java/io/scalecube/security/environment/LoggingExtension.java b/tests/src/test/java/io/scalecube/security/environment/LoggingExtension.java new file mode 100644 index 0000000..d1cf32c --- /dev/null +++ b/tests/src/test/java/io/scalecube/security/environment/LoggingExtension.java @@ -0,0 +1,54 @@ +package io.scalecube.security.environment; + +import java.lang.reflect.Method; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoggingExtension + implements AfterEachCallback, BeforeEachCallback, AfterAllCallback, BeforeAllCallback { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggingExtension.class); + + @Override + public void beforeAll(ExtensionContext context) { + LOGGER.info( + "***** Setup: " + context.getTestClass().map(Class::getSimpleName).orElse("") + " *****"); + } + + @Override + public void afterEach(ExtensionContext context) { + LOGGER.info( + "***** Test finished: " + + context.getTestClass().map(Class::getSimpleName).orElse("") + + "." + + context.getTestMethod().map(Method::getName).orElse("") + + "." + + context.getDisplayName() + + " *****"); + } + + @Override + public void beforeEach(ExtensionContext context) { + LOGGER.info( + "***** Test started: " + + context.getTestClass().map(Class::getSimpleName).orElse("") + + "." + + context.getTestMethod().map(Method::getName).orElse("") + + "." + + context.getDisplayName() + + " *****"); + } + + @Override + public void afterAll(ExtensionContext context) { + LOGGER.info( + "***** TearDown: " + + context.getTestClass().map(Class::getSimpleName).orElse("") + + " *****"); + } +} diff --git a/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java index 7f28a92..9e9456c 100644 --- a/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java +++ b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java @@ -203,6 +203,14 @@ public String policiesAclUri(String roleName) { return vaultAddr + "/v1/sys/policies/acl/" + roleName; } + public static Throwable getRootCause(Throwable throwable) { + Throwable cause; + while ((cause = throwable.getCause()) != null) { + throwable = cause; + } + return throwable; + } + @Override public void close() { vault.stop(); diff --git a/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java b/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java index b37fee8..edec896 100644 --- a/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java +++ b/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java @@ -1,5 +1,6 @@ package io.scalecube.security.tokens.jwt; +import static io.scalecube.security.environment.VaultEnvironment.getRootCause; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -11,7 +12,6 @@ import io.scalecube.security.environment.VaultEnvironment; import java.security.Key; import java.time.Duration; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -55,7 +55,7 @@ void testResolveTokenSuccessfully() throws Exception { } @Test - void testJwksKeyLocatorThrowsError() throws Exception { + void testJwksKeyLocatorThrowsError() { final var token = generateToken(); Locator keyLocator = mock(Locator.class); @@ -64,11 +64,11 @@ void testJwksKeyLocatorThrowsError() throws Exception { try { new JsonwebtokenResolver(keyLocator).resolve(token).get(3, TimeUnit.SECONDS); fail("Expected exception"); - } catch (ExecutionException e) { - final var ex = e.getCause(); + } catch (Exception e) { + final var ex = getRootCause(e); assertNotNull(ex); assertNotNull(ex.getMessage()); - assertTrue(ex.getMessage().startsWith("Cannot get key")); + assertTrue(ex.getMessage().startsWith("Cannot get key"), "Exception: " + ex); } } diff --git a/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java index 35a2f0d..c37ef53 100644 --- a/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java +++ b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java @@ -1,5 +1,6 @@ package io.scalecube.security.vault; +import static io.scalecube.security.environment.VaultEnvironment.getRootCause; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -51,7 +52,7 @@ void testGetServiceTokenUsingWrongCredentials() throws Exception { serviceTokenSupplier.getToken(Collections.emptyMap()).get(3, TimeUnit.SECONDS); fail("Exception expected"); } catch (ExecutionException e) { - final var ex = e.getCause(); + final var ex = getRootCause(e); assertNotNull(ex); assertNotNull(ex.getMessage()); assertTrue( @@ -75,7 +76,7 @@ void testGetNonExistingServiceToken() throws Exception { serviceTokenSupplier.getToken(Collections.emptyMap()).get(3, TimeUnit.SECONDS); fail("Exception expected"); } catch (ExecutionException e) { - final var ex = e.getCause(); + final var ex = getRootCause(e); assertNotNull(ex); assertNotNull(ex.getMessage()); assertTrue( @@ -119,7 +120,7 @@ void testGetServiceTokenByWrongServiceRole() throws Exception { serviceTokenSupplier.getToken(Collections.emptyMap()).get(3, TimeUnit.SECONDS); fail("Exception expected"); } catch (ExecutionException e) { - final var ex = e.getCause(); + final var ex = getRootCause(e); assertNotNull(ex); assertNotNull(ex.getMessage()); assertTrue( diff --git a/tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000..61e9951 --- /dev/null +++ b/tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.scalecube.security.environment.LoggingExtension diff --git a/tests/src/test/resources/junit-platform.properties b/tests/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..6efc0d5 --- /dev/null +++ b/tests/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.extensions.autodetection.enabled=true diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java index 606e592..2108370 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java @@ -21,27 +21,26 @@ public JsonwebtokenResolver(Locator keyLocator) { public CompletableFuture resolve(String token) { return CompletableFuture.supplyAsync( () -> { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Resolve token: {}", mask(token)); - } - final var claimsJws = Jwts.parser().keyLocator(keyLocator).build().parseSignedClaims(token); - return new JwtToken(claimsJws.getHeader(), claimsJws.getPayload()); }) - .whenComplete( + .handle( (jwtToken, ex) -> { if (jwtToken != null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Resolved token: {}", mask(token)); } + return jwtToken; } if (ex != null) { - if (LOGGER.isWarnEnabled()) { - LOGGER.warn("Failed to resolve token: {}, cause: {}", mask(token), ex.toString()); + if (ex instanceof JwtTokenException) { + throw (JwtTokenException) ex; + } else { + throw new JwtTokenException("Failed to resolve token: " + mask(token), ex); } } + return null; }); } diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java index 2934573..3102a58 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java @@ -55,13 +55,11 @@ protected Key locate(JwsHeader header) { kid -> { final var key = findKeyById(computeKeyList(), kid); if (key == null) { - throw new JwtTokenException("Cannot find key by kid: " + kid); + throw new RuntimeException("Cannot find key by kid: " + kid); } return new CachedKey(key, System.currentTimeMillis() + keyTtl); }) .key(); - } catch (JwtTokenException ex) { - throw ex; } catch (Exception ex) { throw new JwtTokenException(ex); } finally { @@ -80,12 +78,12 @@ private JwkInfoList computeKeyList() { HttpRequest.newBuilder(jwksUri).GET().timeout(requestTimeout).build(), BodyHandlers.ofInputStream()); } catch (Exception e) { - throw new JwtTokenException("Failed to retrive jwk keys", e); + throw new RuntimeException("Failed to retrive jwk keys", e); } final var statusCode = httpResponse.statusCode(); if (statusCode != 200) { - throw new JwtTokenException("Failed to retrive jwk keys, status: " + statusCode); + throw new RuntimeException("Failed to retrive jwk keys, status: " + statusCode); } return toJwkInfoList(httpResponse.body()); diff --git a/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java index 09150dd..977d3ab 100644 --- a/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java @@ -86,12 +86,12 @@ public void install() { final var rest = new Rest().header(VAULT_TOKEN_HEADER, token); final var keyName = keyNameSupplier.get(); - createVaultIdentityKey(rest.url(buildVaultIdentityKeyUri(keyName)), keyName); + createVaultIdentityKey(rest.url(vaultIdentityKeyUri(keyName)), keyName); for (var role : serviceRoles.roles) { String roleName = roleNameBuilder.apply(role.role); createVaultIdentityRole( - rest.url(buildVaultIdentityRoleUri(roleName)), + rest.url(vaultIdentityRoleUri(roleName)), keyName, roleName, role.permissions); @@ -127,9 +127,7 @@ private static void awaitSuccess(int status) { } private void createVaultIdentityKey(Rest rest, String keyName) { - LOGGER.debug("[createVaultIdentityKey] {}", keyName); - - byte[] body = + final byte[] body = Json.object() .add("rotation_period", keyRotationPeriod) .add("verification_ttl", keyVerificationTtl) @@ -140,16 +138,15 @@ private void createVaultIdentityKey(Rest rest, String keyName) { try { awaitSuccess(rest.body(body).post().getStatus()); + LOGGER.debug("Created vault identity key: {}", keyName); } catch (RestException e) { - throw new RuntimeException(e); + throw new RuntimeException("Failed to create vault identity key: " + keyName, e); } } private void createVaultIdentityRole( Rest rest, String keyName, String roleName, List permissions) { - LOGGER.debug("[createVaultIdentityRole] {}", roleName); - - byte[] body = + final byte[] body = Json.object() .add("key", keyName) .add("template", createTemplate(permissions)) @@ -159,8 +156,9 @@ private void createVaultIdentityRole( try { awaitSuccess(rest.body(body).post().getStatus()); + LOGGER.debug("Created vault identity role: {}", roleName); } catch (RestException e) { - throw new RuntimeException(e); + throw new RuntimeException("Failed to create vault identity role: " + roleName, e); } } @@ -170,14 +168,14 @@ private static String createTemplate(List permissions) { Json.object().add("permissions", String.join(",", permissions)).toString().getBytes()); } - private String buildVaultIdentityKeyUri(String keyName) { + private String vaultIdentityKeyUri(String keyName) { return new StringJoiner("/", vaultAddress, "") .add("/v1/identity/oidc/key") .add(keyName) .toString(); } - private String buildVaultIdentityRoleUri(String roleName) { + private String vaultIdentityRoleUri(String roleName) { return new StringJoiner("/", vaultAddress, "") .add("/v1/identity/oidc/role") .add(roleName) @@ -338,7 +336,7 @@ public ServiceRoles get() { try (final FileInputStream fis = new FileInputStream(file)) { return OBJECT_MAPPER.readValue(fis, ServiceRoles.class); } - } catch (Exception e) { + } catch (IOException e) { throw new RuntimeException(e); } } diff --git a/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java index b1ecb64..1cfcee6 100644 --- a/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java @@ -45,11 +45,17 @@ public CompletableFuture getToken(Map tags) { .get() .thenApplyAsync( vaultToken -> { - final String uri = toServiceTokenUri(tags); - final String token = rpcGetToken(uri, vaultToken); - LOGGER.debug( - "[getToken][success] uri={}, tags={}, result={}", uri, tags, mask(token)); - return token; + final var role = serviceTokenNameBuilder.apply(serviceRole, tags); + final var uri = serviceTokenUri(vaultAddress, role); + try { + final var token = rpcGetToken(uri, vaultToken); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Got service token: {}, role: {}", mask(token), role); + } + return token; + } catch (Exception ex) { + throw new RuntimeException("Failed to get service token, role: " + role, ex); + } }); } @@ -74,11 +80,8 @@ private static String rpcGetToken(String uri, String vaultToken) { } } - private String toServiceTokenUri(Map tags) { - return new StringJoiner("/", vaultAddress, "") - .add("/v1/identity/oidc/token") - .add(serviceTokenNameBuilder.apply(serviceRole, tags)) - .toString(); + private static String serviceTokenUri(final String address, final String role) { + return new StringJoiner("/", address, "").add("/v1/identity/oidc/token").add(role).toString(); } private static String mask(String data) { From 1825fe8afed99ba2b363b6792272badda9e4518c Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 19:51:44 +0200 Subject: [PATCH 8/9] Cosmetic changes --- .../security/tokens/jwt/JwksKeyLocator.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java index 3102a58..06c7cea 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java @@ -156,21 +156,49 @@ public static class Builder { private Duration requestTimeout = Duration.ofSeconds(10); private int keyTtl = 60 * 1000; + /** + * Setter for JWKS URI. The JWKS URI typically follows a well-known pattern, such as + * https://server_domain/.well-known/jwks.json. This endpoint is a read-only URL that responds + * to GET requests by returning the JWKS in JSON format. + * + * @param jwksUri jwksUri + * @return this + */ public Builder jwksUri(String jwksUri) { this.jwksUri = URI.create(jwksUri); return this; } + /** + * Setter for {@code connectTimeout}. + * + * @param connectTimeout connectTimeout (optional) + * @return this + */ public Builder connectTimeout(Duration connectTimeout) { this.connectTimeout = connectTimeout; return this; } + /** + * Setter for {@code requestTimeout}. + * + * @param requestTimeout requestTimeout (optional) + * @return this + */ public Builder requestTimeout(Duration requestTimeout) { this.requestTimeout = requestTimeout; return this; } + /** + * Setter for {@code keyTtl}. Keys that was obtained from JWKS URI gets cached for some period + * of time, after that they being removed from the cache. This caching time period is controlled + * by {@code keyTtl} setting. + * + * @param keyTtl keyTtl (optional) + * @return this + */ public Builder keyTtl(int keyTtl) { this.keyTtl = keyTtl; return this; From ce67997197c5015e768c8eb774a2518a685aa536 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Sun, 2 Feb 2025 19:56:38 +0200 Subject: [PATCH 9/9] Renamed test --- ...ltIdentityTokenTests.java => JsonwebtokenResolverTests.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/src/test/java/io/scalecube/security/tokens/jwt/{VaultIdentityTokenTests.java => JsonwebtokenResolverTests.java} (98%) diff --git a/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java b/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java similarity index 98% rename from tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java rename to tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java index edec896..d790f97 100644 --- a/tests/src/test/java/io/scalecube/security/tokens/jwt/VaultIdentityTokenTests.java +++ b/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java @@ -18,7 +18,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -class VaultIdentityTokenTests { +public class JsonwebtokenResolverTests { private static VaultEnvironment vaultEnvironment;