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..07afe36 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,6 @@ scalecube-security-parent 1.1.0-SNAPSHOT pom - ScaleCube Security @@ -35,22 +34,22 @@ - jwt tokens vault + tests - 2020.0.32 5.1.0 2.18.2 1.7.36 - 0.11.2 + 0.12.6 4.6.1 5.8.2 1.3 - 1.20.1 + 2.17.2 + 1.20.4 https://maven.pkg.github.com/scalecube/scalecube-security @@ -65,14 +64,6 @@ vault-java-driver ${vault-java-driver.version} - - - io.projectreactor - reactor-bom - ${reactor.version} - pom - import - org.slf4j @@ -103,12 +94,21 @@ pom import + + + org.apache.logging.log4j + log4j-bom + ${log4j.version} + pom + import + org.testcontainers - vault + testcontainers-bom ${testcontainers.version} - test + pom + import @@ -133,6 +133,12 @@ ${mockito-junit.version} test + + org.mockito + mockito-inline + ${mockito-junit.version} + test + org.hamcrest hamcrest-all @@ -140,8 +146,18 @@ test - io.projectreactor - reactor-test + org.testcontainers + vault + test + + + org.apache.logging.log4j + log4j-slf4j-impl + test + + + org.apache.logging.log4j + log4j-core test diff --git a/tests/pom.xml b/tests/pom.xml new file mode 100644 index 0000000..cd237ce --- /dev/null +++ b/tests/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + io.scalecube + scalecube-security-parent + 1.1.0-SNAPSHOT + + + scalecube-security-tests + + + + io.scalecube + scalecube-security-tokens + ${project.parent.version} + + + io.scalecube + scalecube-security-vault + ${project.parent.version} + + + + org.testcontainers + vault + test + + + com.bettercloud + vault-java-driver + test + + + + 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 new file mode 100644 index 0000000..9e9456c --- /dev/null +++ b/tests/src/test/java/io/scalecube/security/environment/VaultEnvironment.java @@ -0,0 +1,218 @@ +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; +import com.bettercloud.vault.rest.RestResponse; +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 implements AutoCloseable { + + 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; + + private final GenericContainer vault = + new VaultContainer("vault:1.4.0") + .withVaultToken(VAULT_TOKEN) + .waitingFor(new LogMessageWaitStrategy().withRegEx("^.*Vault server started!.*$")); + + 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 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); + } + + return Json.parse(new String(restResponse.getBody())) + .asObject() + .get("data") + .asObject() + .get("token") + .asString(); + } + + 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( + "Unexpected status code on identity token policy creation: " + status); + } + } + + 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) { + if (exitCode != 0) { + throw new IllegalStateException("Exited with error: " + exitCode); + } + } + + 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); + } + return keyName; + } + + 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); + } + return roleName; + } + + public String oidcKeyUrl(String keyName) { + return vaultAddr + "/v1/identity/oidc/key/" + keyName; + } + + public String oidcRoleUrl(String roleName) { + return vaultAddr + "/v1/identity/oidc/role/" + roleName; + } + + public String oidcToken(String roleName) { + return vaultAddr + "/v1/identity/oidc/token/" + roleName; + } + + public String jwksUri() { + return vaultAddr + "/v1/identity/oidc/.well-known/keys"; + } + + 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/JsonwebtokenResolverTests.java b/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java new file mode 100644 index 0000000..d790f97 --- /dev/null +++ b/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java @@ -0,0 +1,81 @@ +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; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +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.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 JsonwebtokenResolverTests { + + 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) + .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()); + } + + @Test + void testJwksKeyLocatorThrowsError() { + 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).get(3, TimeUnit.SECONDS); + fail("Expected exception"); + } catch (Exception e) { + final var ex = getRootCause(e); + assertNotNull(ex); + assertNotNull(ex.getMessage()); + assertTrue(ex.getMessage().startsWith("Cannot get key"), "Exception: " + ex); + } + } + + private static String generateToken() { + String keyName = vaultEnvironment.createIdentityKey(); // oidc/key + String roleName = vaultEnvironment.createIdentityRole(keyName); // oidc/role + 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..c37ef53 --- /dev/null +++ b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java @@ -0,0 +1,181 @@ +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; +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 = getRootCause(e); + 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 = getRootCause(e); + 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 = getRootCause(e); + 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/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/tests/src/test/resources/log4j2-test.xml b/tests/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..0dbe306 --- /dev/null +++ b/tests/src/test/resources/log4j2-test.xml @@ -0,0 +1,25 @@ + + + + + %level{length=1} %d{ISO8601} %c{1.} %m [%t]%n + + + + + + + + + + + + + + + + + + + + diff --git a/tokens/pom.xml b/tokens/pom.xml index 5a6caf5..b24af2d 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 @@ -31,17 +29,6 @@ org.slf4j slf4j-api - - - org.testcontainers - vault - test - - - com.bettercloud - vault-java-driver - 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..2108370 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java @@ -0,0 +1,53 @@ +package io.scalecube.security.tokens.jwt; + +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; + +public class JsonwebtokenResolver implements JwtTokenResolver { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonwebtokenResolver.class); + + private final Locator keyLocator; + + public JsonwebtokenResolver(Locator keyLocator) { + this.keyLocator = keyLocator; + } + + @Override + public CompletableFuture resolve(String token) { + return CompletableFuture.supplyAsync( + () -> { + final var claimsJws = + Jwts.parser().keyLocator(keyLocator).build().parseSignedClaims(token); + return new JwtToken(claimsJws.getHeader(), claimsJws.getPayload()); + }) + .handle( + (jwtToken, ex) -> { + if (jwtToken != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Resolved token: {}", mask(token)); + } + return jwtToken; + } + if (ex != null) { + if (ex instanceof JwtTokenException) { + throw (JwtTokenException) ex; + } else { + throw new JwtTokenException("Failed to resolve token: " + mask(token), ex); + } + } + return null; + }); + } + + 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..06c7cea --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java @@ -0,0 +1,211 @@ +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 RuntimeException("Cannot find key by kid: " + kid); + } + return new CachedKey(key, System.currentTimeMillis() + keyTtl); + }) + .key(); + } 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 RuntimeException("Failed to retrive jwk keys", e); + } + + final var statusCode = httpResponse.statusCode(); + if (statusCode != 200) { + throw new RuntimeException("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; + + /** + * 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; + } + + 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..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,23 +1,14 @@ package io.scalecube.security.tokens.jwt; -import java.util.Map; -import reactor.core.publisher.Mono; +import java.util.concurrent.CompletableFuture; 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); + CompletableFuture 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 deleted file mode 100644 index f2b9dab..0000000 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultEnvironment.java +++ /dev/null @@ -1,163 +0,0 @@ -package io.scalecube.security.tokens.jwt; - -import com.bettercloud.vault.json.Json; -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 { - - private static final String VAULT_TOKEN = "test"; - private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; - - private static final GenericContainer VAULT_CONTAINER = - 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(); - } - - public static 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(); - - if (status != 200 && status != 204) { - throw new IllegalStateException( - "Unexpected status code on identity token creation: " + status); - } - - return Json.parse(new String(restResponse.getBody())) - .asObject() - .get("data") - .asObject() - .get("token") - .asString(); - } - - public static 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(); - - if (status != 200 && status != 204) { - throw new IllegalStateException( - "Unexpected status code on identity token policy creation: " + status); - } - } - - public static String createEntity(final String roleName) - throws IOException, InterruptedException { - - checkSuccess( - VAULT_CONTAINER.execInContainer("vault auth enable userpass".split("\\s")).getExitCode()); - checkSuccess( - VAULT_CONTAINER - .execInContainer( - ("vault write auth/userpass/users/abc password=abc policies=" + roleName) - .split("\\s")) - .getExitCode()); - - ExecResult loginExecResult = - VAULT_CONTAINER.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 static void checkSuccess(int exitCode) { - if (exitCode != 0) { - throw new IllegalStateException("Exited with error: " + exitCode); - } - } - - public static 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(); - - if (status != 200 && status != 204) { - throw new IllegalStateException("Unexpected status code on oidc/key creation: " + status); - } - return keyName; - } - - public static 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(); - - if (status != 200 && status != 204) { - throw new IllegalStateException("Unexpected status code on oidc/role creation: " + status); - } - return roleName; - } - - public static String oidcKeyUrl(String keyName) { - return vaultAddr + "/v1/identity/oidc/key/" + keyName; - } - - public static String oidcRoleUrl(String roleName) { - return vaultAddr + "/v1/identity/oidc/role/" + roleName; - } - - public static String oidcToken(String roleName) { - return vaultAddr + "/v1/identity/oidc/token/" + roleName; - } - - public static String jwksUri() { - return vaultAddr + "/v1/identity/oidc/.well-known/keys"; - } - - public static String policiesAclUri(String roleName) { - return vaultAddr + "/v1/sys/policies/acl/" + 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 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..977d3ab 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,17 +80,18 @@ public void install() { try { vaultTokenSupplier + .get() .thenAcceptAsync( token -> { 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); @@ -121,16 +120,14 @@ 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); } } 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,17 +137,16 @@ private void createVaultIdentityKey(Rest rest, String keyName) { .getBytes(); try { - verifyOk(rest.body(body).post().getStatus()); + 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,9 +155,10 @@ private void createVaultIdentityRole( .getBytes(); try { - verifyOk(rest.body(body).post().getStatus()); + 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); } } @@ -171,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) @@ -339,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); } } @@ -355,7 +352,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 +370,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 +390,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 1804b0d..1cfcee6 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,23 @@ 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 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); + } + }); } private static String rpcGetToken(String uri, String vaultToken) { @@ -56,7 +64,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() @@ -69,17 +80,8 @@ 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") - .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) { @@ -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;