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