diff --git a/jwt/pom.xml b/jwt/pom.xml
new file mode 100644
index 0000000..3df4fd1
--- /dev/null
+++ b/jwt/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+
+ io.scalecube
+ scalecube-security-parent
+ 1.1.8-SNAPSHOT
+
+
+ scalecube-security-jwt
+ ${project.artifactId}
+
+
+
+ com.auth0
+ java-jwt
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfo.java b/jwt/src/main/java/io/scalecube/security/jwt/JwkInfo.java
similarity index 97%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfo.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwkInfo.java
index 25cb6c8..0098621 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfo.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwkInfo.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.StringJoiner;
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java b/jwt/src/main/java/io/scalecube/security/jwt/JwkInfoList.java
similarity index 93%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwkInfoList.java
index 3ccee0a..f3f8f61 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwkInfoList.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwkInfoList.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
import java.util.ArrayList;
import java.util.List;
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java b/jwt/src/main/java/io/scalecube/security/jwt/JwksKeyProvider.java
similarity index 83%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwksKeyProvider.java
index d0c41ca..1321589 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyLocator.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwksKeyProvider.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -6,8 +6,6 @@
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;
@@ -29,7 +27,11 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
-public class JwksKeyLocator extends LocatorAdapter {
+/**
+ * Provides public keys from a remote JWKS endpoint and caches them temporarily. Keys are fetched on
+ * demand by their {@code kid} and automatically removed when expired.
+ */
+public class JwksKeyProvider {
private static final ObjectMapper OBJECT_MAPPER = newObjectMapper();
@@ -42,28 +44,38 @@ public class JwksKeyLocator extends LocatorAdapter {
private final Map keyResolutions = new ConcurrentHashMap<>();
private final ReentrantLock cleanupLock = new ReentrantLock();
- private JwksKeyLocator(Builder builder) {
+ private JwksKeyProvider(Builder builder) {
this.jwksUri = Objects.requireNonNull(builder.jwksUri, "jwksUri");
this.connectTimeout = Objects.requireNonNull(builder.connectTimeout, "connectTimeout");
this.requestTimeout = Objects.requireNonNull(builder.requestTimeout, "requestTimeout");
this.keyTtl = builder.keyTtl;
- this.httpClient = HttpClient.newBuilder().connectTimeout(connectTimeout).build();
+ this.httpClient =
+ builder.httpClient != null
+ ? builder.httpClient
+ : HttpClient.newBuilder().connectTimeout(connectTimeout).build();
}
public static Builder builder() {
return new Builder();
}
- @Override
- protected Key locate(JwsHeader header) {
+ /**
+ * Returns the public key for the given {@code kid}. If not cached, the key is fetched from the
+ * JWKS endpoint and cached for future use.
+ *
+ * @param kid key id of the public key to retrieve
+ * @return {@link Key} object associated with given {@code kid}
+ * @throws JwtUnavailableException if key cannot be found or JWKS cannot be retrieved
+ */
+ public Key getKey(String kid) {
try {
return keyResolutions
.computeIfAbsent(
- header.getKeyId(),
- kid -> {
- final var key = findKeyById(computeKeyList(), kid);
+ kid,
+ id -> {
+ final var key = findKeyById(computeKeyList(), id);
if (key == null) {
- throw new JwtUnavailableException("Cannot find key by kid: " + kid);
+ throw new JwtUnavailableException("Cannot find key by kid: " + id);
}
return new CachedKey(key, System.currentTimeMillis() + keyTtl);
})
@@ -163,6 +175,7 @@ public static class Builder {
private Duration connectTimeout = Duration.ofSeconds(10);
private Duration requestTimeout = Duration.ofSeconds(10);
private int keyTtl = 60 * 1000;
+ private HttpClient httpClient;
private Builder() {}
@@ -214,8 +227,19 @@ public Builder keyTtl(int keyTtl) {
return this;
}
- public JwksKeyLocator build() {
- return new JwksKeyLocator(this);
+ /**
+ * Setter for optional {@link HttpClient}.
+ *
+ * @param httpClient httpClient
+ * @return this
+ */
+ public Builder httpClient(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ return this;
+ }
+
+ public JwksKeyProvider build() {
+ return new JwksKeyProvider(this);
}
}
}
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java b/jwt/src/main/java/io/scalecube/security/jwt/JwksTokenResolver.java
similarity index 55%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwksTokenResolver.java
index f3ac565..5ceab1d 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwksTokenResolver.java
@@ -1,29 +1,36 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
-import io.jsonwebtoken.JwtParser;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.Locator;
-import java.security.Key;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import java.security.interfaces.RSAPublicKey;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class JsonwebtokenResolver implements JwtTokenResolver {
+/**
+ * Resolves and verifies JWT tokens using public keys provided by {@link JwksKeyProvider}. Tokens
+ * are validated asynchronously and parsed into {@link JwtToken} instances.
+ */
+public class JwksTokenResolver implements JwtTokenResolver {
- private static final Logger LOGGER = LoggerFactory.getLogger(JsonwebtokenResolver.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(JwksTokenResolver.class);
- private final JwtParser jwtParser;
+ private final JwksKeyProvider keyProvider;
- public JsonwebtokenResolver(Locator keyLocator) {
- jwtParser = Jwts.parser().keyLocator(keyLocator).build();
+ public JwksTokenResolver(JwksKeyProvider keyProvider) {
+ this.keyProvider = keyProvider;
}
@Override
public CompletableFuture resolveToken(String token) {
return CompletableFuture.supplyAsync(
() -> {
- final var claimsJws = jwtParser.parseSignedClaims(token);
- return new JwtToken(claimsJws.getHeader(), claimsJws.getPayload());
+ final var rawToken = JWT.decode(token);
+ final var kid = rawToken.getKeyId();
+ final var publicKey = (RSAPublicKey) keyProvider.getKey(kid);
+ final var verifier = JWT.require(Algorithm.RSA256(publicKey, null)).build();
+ verifier.verify(token);
+ return JwtToken.parseToken(token);
})
.handle(
(jwtToken, ex) -> {
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java b/jwt/src/main/java/io/scalecube/security/jwt/JwtToken.java
similarity index 77%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwtToken.java
index 828164e..35e3468 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwtToken.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
@@ -6,13 +6,19 @@
import java.util.Base64;
import java.util.Map;
+/**
+ * Represents parsed JWT (JSON Web Token), including its header and payload claims.
+ *
+ * @param header JWT header as map of key-value pairs
+ * @param payload JWT payload (claims) as map of key-value pairs
+ */
public record JwtToken(Map header, Map payload) {
/**
* Parses given JWT without verifying its signature.
*
* @param token jwt token
- * @return parsed token
+ * @return {@link JwtToken} object, or {@link JwtTokenException} will be thrown
*/
public static JwtToken parseToken(String token) {
String[] parts = token.split("\\.");
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java b/jwt/src/main/java/io/scalecube/security/jwt/JwtTokenException.java
similarity index 94%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwtTokenException.java
index 5ea597c..c36fdf7 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenException.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwtTokenException.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
import java.util.StringJoiner;
diff --git a/jwt/src/main/java/io/scalecube/security/jwt/JwtTokenResolver.java b/jwt/src/main/java/io/scalecube/security/jwt/JwtTokenResolver.java
new file mode 100644
index 0000000..e5c3bd7
--- /dev/null
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwtTokenResolver.java
@@ -0,0 +1,19 @@
+package io.scalecube.security.jwt;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Resolves and verifies JWT tokens asynchronously. Implementations parse the token, validate its
+ * signature, and extract claims.
+ */
+public interface JwtTokenResolver {
+
+ /**
+ * Verifies given JWT and parses its header and claims.
+ *
+ * @param token jwt token
+ * @return async result completing with {@link JwtToken}, or completing exceptionally with {@link
+ * JwtTokenException} on failure
+ */
+ CompletableFuture resolveToken(String token);
+}
diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtUnavailableException.java b/jwt/src/main/java/io/scalecube/security/jwt/JwtUnavailableException.java
similarity index 95%
rename from tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtUnavailableException.java
rename to jwt/src/main/java/io/scalecube/security/jwt/JwtUnavailableException.java
index adb00a4..c6cdb7b 100644
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtUnavailableException.java
+++ b/jwt/src/main/java/io/scalecube/security/jwt/JwtUnavailableException.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
/**
* Special JWT exception type indicating transient error during token resolution. For example such
diff --git a/pom.xml b/pom.xml
index 01eac55..b7283ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,5 +1,7 @@
-
+
4.0.0
@@ -33,7 +35,7 @@
- tokens
+ jwt
vault
tests
@@ -42,9 +44,10 @@
5.1.0
2.19.2
1.7.36
- 0.12.6
+ 4.5.0
- 4.6.1
+ 5.20.0
+ 5.2.0
5.8.2
1.3
2.17.2
@@ -69,21 +72,11 @@
slf4j-api
${slf4j.version}
-
+
- io.jsonwebtoken
- jjwt-api
- ${jjwt.version}
-
-
- io.jsonwebtoken
- jjwt-impl
- ${jjwt.version}
-
-
- io.jsonwebtoken
- jjwt-jackson
- ${jjwt.version}
+ com.auth0
+ java-jwt
+ ${auth0.java-jwt.version}
@@ -135,7 +128,7 @@
org.mockito
mockito-inline
- ${mockito-junit.version}
+ ${mockito-inline.version}
test
diff --git a/tests/pom.xml b/tests/pom.xml
index 100287e..7ecde6a 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -14,7 +14,7 @@
io.scalecube
- scalecube-security-tokens
+ scalecube-security-jwt
${project.parent.version}
diff --git a/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java b/tests/src/test/java/io/scalecube/security/jwt/JwksTokenResolverTests.java
similarity index 76%
rename from tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java
rename to tests/src/test/java/io/scalecube/security/jwt/JwksTokenResolverTests.java
index bc9e8de..d7b1a58 100644
--- a/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java
+++ b/tests/src/test/java/io/scalecube/security/jwt/JwksTokenResolverTests.java
@@ -1,4 +1,4 @@
-package io.scalecube.security.tokens.jwt;
+package io.scalecube.security.jwt;
import static io.scalecube.security.environment.VaultEnvironment.getRootCause;
import static org.hamcrest.CoreMatchers.instanceOf;
@@ -11,25 +11,23 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import io.jsonwebtoken.Locator;
import io.scalecube.security.environment.IntegrationEnvironmentFixture;
import io.scalecube.security.environment.VaultEnvironment;
-import java.security.Key;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(IntegrationEnvironmentFixture.class)
-public class JsonwebtokenResolverTests {
+public class JwksTokenResolverTests {
@Test
void testResolveTokenTokenSuccessfully(VaultEnvironment vaultEnvironment) throws Exception {
final var token = vaultEnvironment.newServiceToken();
final var jwtToken =
- new JsonwebtokenResolver(
- JwksKeyLocator.builder()
+ new JwksTokenResolver(
+ JwksKeyProvider.builder()
.jwksUri(vaultEnvironment.jwksUri())
.connectTimeout(Duration.ofSeconds(3))
.requestTimeout(Duration.ofSeconds(3))
@@ -55,14 +53,14 @@ void testParseTokenSuccessfully(VaultEnvironment vaultEnvironment) {
}
@Test
- void testJwksKeyLocatorThrowsError(VaultEnvironment vaultEnvironment) {
+ void testJwksKeyProviderThrowsError(VaultEnvironment vaultEnvironment) {
final var token = vaultEnvironment.newServiceToken();
- Locator keyLocator = mock(Locator.class);
- when(keyLocator.locate(any())).thenThrow(new RuntimeException("Cannot get key"));
+ final var keyProvider = mock(JwksKeyProvider.class);
+ when(keyProvider.getKey(any())).thenThrow(new RuntimeException("Cannot get key"));
try {
- new JsonwebtokenResolver(keyLocator).resolveToken(token).get(3, TimeUnit.SECONDS);
+ new JwksTokenResolver(keyProvider).resolveToken(token).get(3, TimeUnit.SECONDS);
fail("Expected exception");
} catch (Exception e) {
final var ex = getRootCause(e);
@@ -72,14 +70,14 @@ void testJwksKeyLocatorThrowsError(VaultEnvironment vaultEnvironment) {
}
@Test
- void testJwksKeyLocatorThrowsRetryableError(VaultEnvironment vaultEnvironment) {
+ void testJwksKeyProviderThrowsRetryableError(VaultEnvironment vaultEnvironment) {
final var token = vaultEnvironment.newServiceToken();
- Locator keyLocator = mock(Locator.class);
- when(keyLocator.locate(any())).thenThrow(new JwtUnavailableException("JWKS timeout"));
+ final var keyProvider = mock(JwksKeyProvider.class);
+ when(keyProvider.getKey(any())).thenThrow(new JwtUnavailableException("JWKS timeout"));
try {
- new JsonwebtokenResolver(keyLocator).resolveToken(token).get(3, TimeUnit.SECONDS);
+ new JwksTokenResolver(keyProvider).resolveToken(token).get(3, TimeUnit.SECONDS);
fail("Expected exception");
} catch (Exception e) {
final var ex = getRootCause(e);
diff --git a/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java
index 80f3430..0be7e5b 100644
--- a/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java
+++ b/tests/src/test/java/io/scalecube/security/vault/VaultServiceTokenTests.java
@@ -11,8 +11,8 @@
import io.scalecube.security.environment.IntegrationEnvironmentFixture;
import io.scalecube.security.environment.VaultEnvironment;
-import io.scalecube.security.tokens.jwt.JsonwebtokenResolver;
-import io.scalecube.security.tokens.jwt.JwksKeyLocator;
+import io.scalecube.security.jwt.JwksKeyProvider;
+import io.scalecube.security.jwt.JwksTokenResolver;
import io.scalecube.security.vault.VaultServiceRolesInstaller.ServiceRoles;
import io.scalecube.security.vault.VaultServiceRolesInstaller.ServiceRoles.Role;
import java.util.Collections;
@@ -141,8 +141,7 @@ void testGetServiceTokenSuccessfully(VaultEnvironment vaultEnvironment) throws E
// Verify serviceToken
final var jwtToken =
- new JsonwebtokenResolver(
- JwksKeyLocator.builder().jwksUri(vaultEnvironment.jwksUri()).build())
+ new JwksTokenResolver(JwksKeyProvider.builder().jwksUri(vaultEnvironment.jwksUri()).build())
.resolveToken(serviceToken)
.get(3, TimeUnit.SECONDS);
diff --git a/tokens/pom.xml b/tokens/pom.xml
deleted file mode 100644
index eab756f..0000000
--- a/tokens/pom.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
- 4.0.0
-
-
- io.scalecube
- scalecube-security-parent
- 1.1.8-SNAPSHOT
-
-
- scalecube-security-tokens
- ${project.artifactId}
-
-
-
- io.jsonwebtoken
- jjwt-api
-
-
- io.jsonwebtoken
- jjwt-impl
-
-
- io.jsonwebtoken
- jjwt-jackson
-
-
- org.slf4j
- slf4j-api
-
-
-
-
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
deleted file mode 100644
index a85a276..0000000
--- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package io.scalecube.security.tokens.jwt;
-
-import java.util.concurrent.CompletableFuture;
-
-public interface JwtTokenResolver {
-
- /**
- * Verifies given JWT and parses its header and claims.
- *
- * @param token jwt token
- * @return async result with {@link JwtToken}, or error
- */
- CompletableFuture resolveToken(String token);
-}