diff --git a/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java b/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java index 093aa4c..98fc2c0 100644 --- a/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java +++ b/jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java @@ -19,19 +19,19 @@ public DefaultJwtAuthenticator(JwtKeyResolver jwtKeyResolver) { @Override public Mono authenticate(String token) { - return Mono.defer( - () -> { - String tokenWithoutSignature = token.substring(0, token.lastIndexOf(".") + 1); + 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(); + JwtParser parser = Jwts.parser(); - Jwt claims = parser.parseClaimsJwt(tokenWithoutSignature); + Jwt claims = parser.parseClaimsJwt(tokenWithoutSignature); - return jwtKeyResolver - .resolve((Map) claims.getHeader()) - .map(key -> parser.setSigningKey(key).parseClaimsJws(token).getBody()) - .map(this::profileFromClaims); - }) - .onErrorMap(AuthenticationException::new); + 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 index 984e337..eae5d3f 100644 --- a/jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java +++ b/jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java @@ -17,6 +17,7 @@ public interface JwtAuthenticator extends Authenticator { /** * Create a profile from claims. + * * @param tokenClaims the claims to parse * @return a profile from the claims */ diff --git a/pom.xml b/pom.xml index 7ced344..df937d6 100644 --- a/pom.xml +++ b/pom.xml @@ -127,6 +127,12 @@ ${hamcrest.version} test + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + io.projectreactor reactor-test diff --git a/tokens/pom.xml b/tokens/pom.xml index f1e0829..7ee51d1 100644 --- a/tokens/pom.xml +++ b/tokens/pom.xml @@ -11,6 +11,10 @@ scalecube-security-tokens + + io.projectreactor + reactor-core + io.jsonwebtoken jjwt-api @@ -23,21 +27,11 @@ io.jsonwebtoken jjwt-jackson - - io.projectreactor - reactor-core - org.slf4j slf4j-api - - org.junit.jupiter - junit-jupiter - ${junit-jupiter.version} - test - org.testcontainers vault @@ -50,12 +44,6 @@ ${vault-java-driver.version} test - - org.mockito - mockito-junit-jupiter - ${mockito.version} - test - 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 index cb4074e..3df74e1 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java @@ -18,6 +18,7 @@ import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.Exceptions; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -26,6 +27,9 @@ public final class JwksKeyProvider implements KeyProvider { private static final Logger LOGGER = LoggerFactory.getLogger(JwksKeyProvider.class); + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); + private static final Duration READ_TIMEOUT = Duration.ofSeconds(10); + private static final ObjectMapper OBJECT_MAPPER = newObjectMapper(); private final Scheduler scheduler; @@ -39,7 +43,7 @@ public final class JwksKeyProvider implements KeyProvider { * @param jwksUri jwksUri */ public JwksKeyProvider(String jwksUri) { - this(jwksUri, newScheduler(), Duration.ofSeconds(10), Duration.ofSeconds(10)); + this(jwksUri, newScheduler(), CONNECT_TIMEOUT, READ_TIMEOUT); } /** @@ -60,38 +64,38 @@ public JwksKeyProvider( @Override public Mono findKey(String kid) { - return Mono.defer(this::callJwksUri) - .map(this::toKeyList) - .flatMap(list -> Mono.justOrEmpty(findRsaKey(list, kid))) - .switchIfEmpty(Mono.error(new KeyProviderException("Key was not found, kid: " + kid))) + return computeKey(kid) + .switchIfEmpty(Mono.error(new KeyNotFoundException("Key was not found, kid: " + kid))) .doOnSubscribe(s -> LOGGER.debug("[findKey] Looking up key in jwks, kid: {}", kid)) - .subscribeOn(scheduler) - .publishOn(scheduler); + .subscribeOn(scheduler); + } + + 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 Mono callJwksUri() { - return Mono.fromCallable( - () -> { - HttpURLConnection httpClient = (HttpURLConnection) new URL(jwksUri).openConnection(); - httpClient.setConnectTimeout((int) connectTimeoutMillis); - httpClient.setReadTimeout((int) readTimeoutMillis); - - int responseCode = httpClient.getResponseCode(); - if (responseCode != 200) { - LOGGER.error("[callJwksUri][{}] Not expected response code: {}", jwksUri, responseCode); - throw new KeyProviderException("Not expected response code: " + responseCode); - } - - return httpClient.getInputStream(); - }); + private JwkInfoList computeKeyList() throws IOException { + HttpURLConnection httpClient = (HttpURLConnection) new URL(jwksUri).openConnection(); + httpClient.setConnectTimeout((int) connectTimeoutMillis); + httpClient.setReadTimeout((int) readTimeoutMillis); + + int responseCode = httpClient.getResponseCode(); + if (responseCode != 200) { + LOGGER.error("[computeKey][{}] Not expected response code: {}", jwksUri, responseCode); + throw new KeyProviderException("Not expected response code: " + responseCode); + } + + return toKeyList(httpClient.getInputStream()); } - private JwkInfoList toKeyList(InputStream stream) { + private static JwkInfoList toKeyList(InputStream stream) { try (InputStream inputStream = new BufferedInputStream(stream)) { return OBJECT_MAPPER.readValue(inputStream, JwkInfoList.class); } catch (IOException e) { LOGGER.error("[toKeyList] Exception occurred: {}", e.toString()); - throw new KeyProviderException(e); + throw Exceptions.propagate(e); } } 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 index a0ca216..dec2fb9 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java @@ -18,6 +18,8 @@ public final class JwtTokenResolverImpl implements JwtTokenResolver { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenResolver.class); + private static final Duration CLEANUP_INTERVAL = Duration.ofSeconds(60); + private final KeyProvider keyProvider; private final JwtTokenParserFactory tokenParserFactory; private final Scheduler scheduler; @@ -31,7 +33,7 @@ public final class JwtTokenResolverImpl implements JwtTokenResolver { * @param keyProvider key provider */ public JwtTokenResolverImpl(KeyProvider keyProvider) { - this(keyProvider, new JsonwebtokenParserFactory(), newScheduler(), Duration.ofSeconds(60)); + this(keyProvider, new JsonwebtokenParserFactory(), newScheduler(), CLEANUP_INTERVAL); } /** @@ -49,8 +51,8 @@ public JwtTokenResolverImpl( Duration cleanupInterval) { this.keyProvider = keyProvider; this.tokenParserFactory = tokenParserFactory; - this.cleanupInterval = cleanupInterval; this.scheduler = scheduler; + this.cleanupInterval = cleanupInterval; } @Override 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 new file mode 100644 index 0000000..8e683ed --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyNotFoundException.java @@ -0,0 +1,18 @@ +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/KeyProviderException.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java index 512eab3..2bb298b 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java @@ -2,18 +2,12 @@ public final class KeyProviderException extends RuntimeException { - public KeyProviderException() {} - public KeyProviderException(String s) { super(s); } - public KeyProviderException(String s, Throwable throwable) { - super(s, throwable); - } - - public KeyProviderException(Throwable throwable) { - super(throwable); + public KeyProviderException(Throwable cause) { + super(cause); } @Override diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java index 0f0ca3d..2c84b36 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java @@ -15,14 +15,7 @@ private Utils() { // Do not instantiate } - /** - * Turns b64 url encoded {@code n} and {@code e} into RSA public key. - * - * @param n modulus (b64 url encoded) - * @param e exponent (b64 url encoded) - * @return RSA public key instance - */ - public static Key toRsaPublicKey(String n, String e) { + 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)); @@ -34,13 +27,7 @@ public static Key toRsaPublicKey(String n, String e) { } } - /** - * Mask sensitive data by replacing part of string with an asterisk symbol. - * - * @param data sensitive data to be masked - * @return masked data - */ - public static String mask(String data) { + static String mask(String data) { if (data == null || data.isEmpty() || data.length() < 5) { return "*****"; } 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 index c97c240..1536931 100644 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java +++ b/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java @@ -1,12 +1,8 @@ package io.scalecube.security.tokens.jwt; -import static io.scalecube.security.tokens.jwt.Utils.toRsaPublicKey; - -import java.security.Key; import java.time.Duration; import java.util.Collections; import java.util.Map; -import java.util.Properties; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -20,7 +16,7 @@ class JwtTokenResolverTests { @Test void testTokenResolver() throws Exception { - TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties"); + JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties"); JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); Mockito.when(tokenParser.parseToken()) @@ -51,9 +47,9 @@ void testTokenResolver() throws Exception { @Test void testTokenResolverWithRotatingKey() throws Exception { - TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties"); - TokenWithKey tokenWithKeyAfterRotation = - new TokenWithKey("token-and-pubkey.after-rotation.properties"); + 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()) @@ -98,7 +94,7 @@ void testTokenResolverWithRotatingKey() throws Exception { @Test void testTokenResolverWithWrongKey() throws Exception { - TokenWithKey tokenWithWrongKey = new TokenWithKey("token-and-wrong-pubkey.properties"); + JwtTokenWithKey tokenWithWrongKey = new JwtTokenWithKey("token-and-wrong-pubkey.properties"); JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); Mockito.when(tokenParser.parseToken()) @@ -128,7 +124,7 @@ void testTokenResolverWithWrongKey() throws Exception { @Test void testTokenResolverWhenKeyProviderFailing() throws Exception { - TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties"); + JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties"); JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); Mockito.when(tokenParser.parseToken()) @@ -153,20 +149,4 @@ void testTokenResolverWhenKeyProviderFailing() throws Exception { // failed resolution not stored => keyProvider must have been called 2 times Mockito.verify(keyProvider, Mockito.times(2)).findKey(tokenWithKey.kid); } - - static class TokenWithKey { - - final String token; - final Key key; - final String kid; - - TokenWithKey(String s) throws Exception { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - Properties props = new Properties(); - props.load(classLoader.getResourceAsStream(s)); - this.token = props.getProperty("token"); - this.kid = props.getProperty("kid"); - this.key = toRsaPublicKey(props.getProperty("n"), props.getProperty("e")); - } - } } 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 new file mode 100644 index 0000000..a04d666 --- /dev/null +++ b/tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenWithKey.java @@ -0,0 +1,22 @@ +package io.scalecube.security.tokens.jwt; + +import static io.scalecube.security.tokens.jwt.Utils.toRsaPublicKey; + +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 = toRsaPublicKey(props.getProperty("n"), props.getProperty("e")); + } +} 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 index 4480613..25bdd04 100644 --- a/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultJwksKeyProviderTests.java +++ b/tokens/src/test/java/io/scalecube/security/tokens/jwt/VaultJwksKeyProviderTests.java @@ -15,7 +15,7 @@ import io.jsonwebtoken.Jwts; import java.time.Duration; import java.util.UUID; -import org.junit.Assert; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -69,8 +69,8 @@ void testFindNonExistentKey() throws Exception { StepVerifier.create(keyProvider.findKey(UUID.randomUUID().toString())) .expectErrorSatisfies( throwable -> { - Assertions.assertEquals(throwable.getClass(), KeyProviderException.class); - Assert.assertThat(throwable.getMessage(), startsWith("Key was not found")); + Assertions.assertEquals(KeyNotFoundException.class, throwable.getClass()); + MatcherAssert.assertThat(throwable.getMessage(), startsWith("Key was not found")); }) .verify(TIMEOUT); } @@ -83,8 +83,8 @@ void testKeyNotFoundOnEmptyEnvironment() { StepVerifier.create(keyProvider.findKey(UUID.randomUUID().toString())) .expectErrorSatisfies( throwable -> { - Assertions.assertEquals(throwable.getClass(), KeyProviderException.class); - Assert.assertThat(throwable.getMessage(), startsWith("Key was not found")); + Assertions.assertEquals(KeyNotFoundException.class, throwable.getClass()); + MatcherAssert.assertThat(throwable.getMessage(), startsWith("Key was not found")); }) .verify(TIMEOUT); }