Skip to content

Commit 5789472

Browse files
authored
Enhanced exceptions hierarchy for jwt-key resolution (#89)
* Enhanced exceptions hierarchy for jwt-key resolution
1 parent 63d1f85 commit 5789472

File tree

12 files changed

+109
-107
lines changed

12 files changed

+109
-107
lines changed

jwt/src/main/java/io/scalecube/security/jwt/DefaultJwtAuthenticator.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,19 @@ public DefaultJwtAuthenticator(JwtKeyResolver jwtKeyResolver) {
1919

2020
@Override
2121
public Mono<Profile> authenticate(String token) {
22-
return Mono.defer(
23-
() -> {
24-
String tokenWithoutSignature = token.substring(0, token.lastIndexOf(".") + 1);
22+
return Mono.defer(() -> authenticate0(token)).onErrorMap(AuthenticationException::new);
23+
}
24+
25+
private Mono<Profile> authenticate0(String token) {
26+
String tokenWithoutSignature = token.substring(0, token.lastIndexOf(".") + 1);
2527

26-
JwtParser parser = Jwts.parser();
28+
JwtParser parser = Jwts.parser();
2729

28-
Jwt<Header, Claims> claims = parser.parseClaimsJwt(tokenWithoutSignature);
30+
Jwt<Header, Claims> claims = parser.parseClaimsJwt(tokenWithoutSignature);
2931

30-
return jwtKeyResolver
31-
.resolve((Map<String, Object>) claims.getHeader())
32-
.map(key -> parser.setSigningKey(key).parseClaimsJws(token).getBody())
33-
.map(this::profileFromClaims);
34-
})
35-
.onErrorMap(AuthenticationException::new);
32+
return jwtKeyResolver
33+
.resolve((Map<String, Object>) claims.getHeader())
34+
.map(key -> parser.setSigningKey(key).parseClaimsJws(token).getBody())
35+
.map(this::profileFromClaims);
3636
}
3737
}

jwt/src/main/java/io/scalecube/security/jwt/JwtAuthenticator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public interface JwtAuthenticator extends Authenticator {
1717

1818
/**
1919
* Create a profile from claims.
20+
*
2021
* @param tokenClaims the claims to parse
2122
* @return a profile from the claims
2223
*/

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@
127127
<version>${hamcrest.version}</version>
128128
<scope>test</scope>
129129
</dependency>
130+
<dependency>
131+
<groupId>org.mockito</groupId>
132+
<artifactId>mockito-junit-jupiter</artifactId>
133+
<version>${mockito.version}</version>
134+
<scope>test</scope>
135+
</dependency>
130136
<dependency>
131137
<groupId>io.projectreactor</groupId>
132138
<artifactId>reactor-test</artifactId>

tokens/pom.xml

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<artifactId>scalecube-security-tokens</artifactId>
1212

1313
<dependencies>
14+
<dependency>
15+
<groupId>io.projectreactor</groupId>
16+
<artifactId>reactor-core</artifactId>
17+
</dependency>
1418
<dependency>
1519
<groupId>io.jsonwebtoken</groupId>
1620
<artifactId>jjwt-api</artifactId>
@@ -23,21 +27,11 @@
2327
<groupId>io.jsonwebtoken</groupId>
2428
<artifactId>jjwt-jackson</artifactId>
2529
</dependency>
26-
<dependency>
27-
<groupId>io.projectreactor</groupId>
28-
<artifactId>reactor-core</artifactId>
29-
</dependency>
3030
<dependency>
3131
<groupId>org.slf4j</groupId>
3232
<artifactId>slf4j-api</artifactId>
3333
</dependency>
3434
<!-- Tests -->
35-
<dependency>
36-
<groupId>org.junit.jupiter</groupId>
37-
<artifactId>junit-jupiter</artifactId>
38-
<version>${junit-jupiter.version}</version>
39-
<scope>test</scope>
40-
</dependency>
4135
<dependency>
4236
<groupId>org.testcontainers</groupId>
4337
<artifactId>vault</artifactId>
@@ -50,12 +44,6 @@
5044
<version>${vault-java-driver.version}</version>
5145
<scope>test</scope>
5246
</dependency>
53-
<dependency>
54-
<groupId>org.mockito</groupId>
55-
<artifactId>mockito-junit-jupiter</artifactId>
56-
<version>${mockito.version}</version>
57-
<scope>test</scope>
58-
</dependency>
5947
</dependencies>
6048

6149
</project>

tokens/src/main/java/io/scalecube/security/tokens/jwt/JwksKeyProvider.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Optional;
1919
import org.slf4j.Logger;
2020
import org.slf4j.LoggerFactory;
21+
import reactor.core.Exceptions;
2122
import reactor.core.publisher.Mono;
2223
import reactor.core.scheduler.Scheduler;
2324
import reactor.core.scheduler.Schedulers;
@@ -26,6 +27,9 @@ public final class JwksKeyProvider implements KeyProvider {
2627

2728
private static final Logger LOGGER = LoggerFactory.getLogger(JwksKeyProvider.class);
2829

30+
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10);
31+
private static final Duration READ_TIMEOUT = Duration.ofSeconds(10);
32+
2933
private static final ObjectMapper OBJECT_MAPPER = newObjectMapper();
3034

3135
private final Scheduler scheduler;
@@ -39,7 +43,7 @@ public final class JwksKeyProvider implements KeyProvider {
3943
* @param jwksUri jwksUri
4044
*/
4145
public JwksKeyProvider(String jwksUri) {
42-
this(jwksUri, newScheduler(), Duration.ofSeconds(10), Duration.ofSeconds(10));
46+
this(jwksUri, newScheduler(), CONNECT_TIMEOUT, READ_TIMEOUT);
4347
}
4448

4549
/**
@@ -60,38 +64,38 @@ public JwksKeyProvider(
6064

6165
@Override
6266
public Mono<Key> findKey(String kid) {
63-
return Mono.defer(this::callJwksUri)
64-
.map(this::toKeyList)
65-
.flatMap(list -> Mono.justOrEmpty(findRsaKey(list, kid)))
66-
.switchIfEmpty(Mono.error(new KeyProviderException("Key was not found, kid: " + kid)))
67+
return computeKey(kid)
68+
.switchIfEmpty(Mono.error(new KeyNotFoundException("Key was not found, kid: " + kid)))
6769
.doOnSubscribe(s -> LOGGER.debug("[findKey] Looking up key in jwks, kid: {}", kid))
68-
.subscribeOn(scheduler)
69-
.publishOn(scheduler);
70+
.subscribeOn(scheduler);
71+
}
72+
73+
private Mono<Key> computeKey(String kid) {
74+
return Mono.fromCallable(this::computeKeyList)
75+
.flatMap(list -> Mono.justOrEmpty(findRsaKey(list, kid)))
76+
.onErrorMap(th -> th instanceof KeyProviderException ? th : new KeyProviderException(th));
7077
}
7178

72-
private Mono<InputStream> callJwksUri() {
73-
return Mono.fromCallable(
74-
() -> {
75-
HttpURLConnection httpClient = (HttpURLConnection) new URL(jwksUri).openConnection();
76-
httpClient.setConnectTimeout((int) connectTimeoutMillis);
77-
httpClient.setReadTimeout((int) readTimeoutMillis);
78-
79-
int responseCode = httpClient.getResponseCode();
80-
if (responseCode != 200) {
81-
LOGGER.error("[callJwksUri][{}] Not expected response code: {}", jwksUri, responseCode);
82-
throw new KeyProviderException("Not expected response code: " + responseCode);
83-
}
84-
85-
return httpClient.getInputStream();
86-
});
79+
private JwkInfoList computeKeyList() throws IOException {
80+
HttpURLConnection httpClient = (HttpURLConnection) new URL(jwksUri).openConnection();
81+
httpClient.setConnectTimeout((int) connectTimeoutMillis);
82+
httpClient.setReadTimeout((int) readTimeoutMillis);
83+
84+
int responseCode = httpClient.getResponseCode();
85+
if (responseCode != 200) {
86+
LOGGER.error("[computeKey][{}] Not expected response code: {}", jwksUri, responseCode);
87+
throw new KeyProviderException("Not expected response code: " + responseCode);
88+
}
89+
90+
return toKeyList(httpClient.getInputStream());
8791
}
8892

89-
private JwkInfoList toKeyList(InputStream stream) {
93+
private static JwkInfoList toKeyList(InputStream stream) {
9094
try (InputStream inputStream = new BufferedInputStream(stream)) {
9195
return OBJECT_MAPPER.readValue(inputStream, JwkInfoList.class);
9296
} catch (IOException e) {
9397
LOGGER.error("[toKeyList] Exception occurred: {}", e.toString());
94-
throw new KeyProviderException(e);
98+
throw Exceptions.propagate(e);
9599
}
96100
}
97101

tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public final class JwtTokenResolverImpl implements JwtTokenResolver {
1818

1919
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenResolver.class);
2020

21+
private static final Duration CLEANUP_INTERVAL = Duration.ofSeconds(60);
22+
2123
private final KeyProvider keyProvider;
2224
private final JwtTokenParserFactory tokenParserFactory;
2325
private final Scheduler scheduler;
@@ -31,7 +33,7 @@ public final class JwtTokenResolverImpl implements JwtTokenResolver {
3133
* @param keyProvider key provider
3234
*/
3335
public JwtTokenResolverImpl(KeyProvider keyProvider) {
34-
this(keyProvider, new JsonwebtokenParserFactory(), newScheduler(), Duration.ofSeconds(60));
36+
this(keyProvider, new JsonwebtokenParserFactory(), newScheduler(), CLEANUP_INTERVAL);
3537
}
3638

3739
/**
@@ -49,8 +51,8 @@ public JwtTokenResolverImpl(
4951
Duration cleanupInterval) {
5052
this.keyProvider = keyProvider;
5153
this.tokenParserFactory = tokenParserFactory;
52-
this.cleanupInterval = cleanupInterval;
5354
this.scheduler = scheduler;
55+
this.cleanupInterval = cleanupInterval;
5456
}
5557

5658
@Override
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
public final class KeyNotFoundException extends RuntimeException {
4+
5+
public KeyNotFoundException(String s) {
6+
super(s);
7+
}
8+
9+
@Override
10+
public synchronized Throwable fillInStackTrace() {
11+
return this;
12+
}
13+
14+
@Override
15+
public String toString() {
16+
return getClass().getSimpleName() + "{errorMessage=" + getMessage() + '}';
17+
}
18+
}

tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProviderException.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,12 @@
22

33
public final class KeyProviderException extends RuntimeException {
44

5-
public KeyProviderException() {}
6-
75
public KeyProviderException(String s) {
86
super(s);
97
}
108

11-
public KeyProviderException(String s, Throwable throwable) {
12-
super(s, throwable);
13-
}
14-
15-
public KeyProviderException(Throwable throwable) {
16-
super(throwable);
9+
public KeyProviderException(Throwable cause) {
10+
super(cause);
1711
}
1812

1913
@Override

tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,7 @@ private Utils() {
1515
// Do not instantiate
1616
}
1717

18-
/**
19-
* Turns b64 url encoded {@code n} and {@code e} into RSA public key.
20-
*
21-
* @param n modulus (b64 url encoded)
22-
* @param e exponent (b64 url encoded)
23-
* @return RSA public key instance
24-
*/
25-
public static Key toRsaPublicKey(String n, String e) {
18+
static Key toRsaPublicKey(String n, String e) {
2619
Decoder b64Decoder = Base64.getUrlDecoder();
2720
BigInteger modulus = new BigInteger(1, b64Decoder.decode(n));
2821
BigInteger exponent = new BigInteger(1, b64Decoder.decode(e));
@@ -34,13 +27,7 @@ public static Key toRsaPublicKey(String n, String e) {
3427
}
3528
}
3629

37-
/**
38-
* Mask sensitive data by replacing part of string with an asterisk symbol.
39-
*
40-
* @param data sensitive data to be masked
41-
* @return masked data
42-
*/
43-
public static String mask(String data) {
30+
static String mask(String data) {
4431
if (data == null || data.isEmpty() || data.length() < 5) {
4532
return "*****";
4633
}

tokens/src/test/java/io/scalecube/security/tokens/jwt/JwtTokenResolverTests.java

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package io.scalecube.security.tokens.jwt;
22

3-
import static io.scalecube.security.tokens.jwt.Utils.toRsaPublicKey;
4-
5-
import java.security.Key;
63
import java.time.Duration;
74
import java.util.Collections;
85
import java.util.Map;
9-
import java.util.Properties;
106
import org.junit.jupiter.api.Test;
117
import org.mockito.ArgumentMatchers;
128
import org.mockito.Mockito;
@@ -20,7 +16,7 @@ class JwtTokenResolverTests {
2016

2117
@Test
2218
void testTokenResolver() throws Exception {
23-
TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties");
19+
JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties");
2420

2521
JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class);
2622
Mockito.when(tokenParser.parseToken())
@@ -51,9 +47,9 @@ void testTokenResolver() throws Exception {
5147

5248
@Test
5349
void testTokenResolverWithRotatingKey() throws Exception {
54-
TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties");
55-
TokenWithKey tokenWithKeyAfterRotation =
56-
new TokenWithKey("token-and-pubkey.after-rotation.properties");
50+
JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties");
51+
JwtTokenWithKey tokenWithKeyAfterRotation =
52+
new JwtTokenWithKey("token-and-pubkey.after-rotation.properties");
5753

5854
JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class);
5955
Mockito.when(tokenParser.parseToken())
@@ -98,7 +94,7 @@ void testTokenResolverWithRotatingKey() throws Exception {
9894

9995
@Test
10096
void testTokenResolverWithWrongKey() throws Exception {
101-
TokenWithKey tokenWithWrongKey = new TokenWithKey("token-and-wrong-pubkey.properties");
97+
JwtTokenWithKey tokenWithWrongKey = new JwtTokenWithKey("token-and-wrong-pubkey.properties");
10298

10399
JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class);
104100
Mockito.when(tokenParser.parseToken())
@@ -128,7 +124,7 @@ void testTokenResolverWithWrongKey() throws Exception {
128124

129125
@Test
130126
void testTokenResolverWhenKeyProviderFailing() throws Exception {
131-
TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties");
127+
JwtTokenWithKey tokenWithKey = new JwtTokenWithKey("token-and-pubkey.properties");
132128

133129
JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class);
134130
Mockito.when(tokenParser.parseToken())
@@ -153,20 +149,4 @@ void testTokenResolverWhenKeyProviderFailing() throws Exception {
153149
// failed resolution not stored => keyProvider must have been called 2 times
154150
Mockito.verify(keyProvider, Mockito.times(2)).findKey(tokenWithKey.kid);
155151
}
156-
157-
static class TokenWithKey {
158-
159-
final String token;
160-
final Key key;
161-
final String kid;
162-
163-
TokenWithKey(String s) throws Exception {
164-
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
165-
Properties props = new Properties();
166-
props.load(classLoader.getResourceAsStream(s));
167-
this.token = props.getProperty("token");
168-
this.kid = props.getProperty("kid");
169-
this.key = toRsaPublicKey(props.getProperty("n"), props.getProperty("e"));
170-
}
171-
}
172152
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import static io.scalecube.security.tokens.jwt.Utils.toRsaPublicKey;
4+
5+
import java.security.Key;
6+
import java.util.Properties;
7+
8+
class JwtTokenWithKey {
9+
10+
final String token;
11+
final Key key;
12+
final String kid;
13+
14+
JwtTokenWithKey(String s) throws Exception {
15+
Properties props = new Properties();
16+
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
17+
props.load(classLoader.getResourceAsStream(s));
18+
this.token = props.getProperty("token");
19+
this.kid = props.getProperty("kid");
20+
this.key = toRsaPublicKey(props.getProperty("n"), props.getProperty("e"));
21+
}
22+
}

0 commit comments

Comments
 (0)