Skip to content

Commit e586f99

Browse files
authored
Merge pull request #78 from scalecube/add-security-tokens-module
Add security tokens module
2 parents ffa53c9 + 056c7ff commit e586f99

20 files changed

+1060
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<modules>
2727
<module>jwt</module>
28+
<module>tokens</module>
2829
</modules>
2930

3031
<properties>

tokens/pom.xml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>io.scalecube</groupId>
9+
<artifactId>scalecube-security-parent</artifactId>
10+
<version>1.0.10-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>scalecube-security-tokens</artifactId>
14+
15+
<properties>
16+
<jjwt.version>0.11.1</jjwt.version>
17+
18+
<reactor.version>Dysprosium-SR7</reactor.version>
19+
<jackson.version>2.11.0</jackson.version>
20+
<slf4j.version>1.7.30</slf4j.version>
21+
22+
<junit-jupiter.version>5.4.2</junit-jupiter.version>
23+
<vault-java-driver.version>5.0.0</vault-java-driver.version>
24+
<testcontainers.version>1.14.0</testcontainers.version>
25+
<mockito.version>3.1.0</mockito.version>
26+
27+
<github.repository>exberry-io/${project.artifactId}</github.repository>
28+
</properties>
29+
30+
<dependencyManagement>
31+
<dependencies>
32+
<!-- Jsonwebtoken -->
33+
<dependency>
34+
<groupId>io.jsonwebtoken</groupId>
35+
<artifactId>jjwt-api</artifactId>
36+
<version>${jjwt.version}</version>
37+
</dependency>
38+
<dependency>
39+
<groupId>io.jsonwebtoken</groupId>
40+
<artifactId>jjwt-impl</artifactId>
41+
<version>${jjwt.version}</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>io.jsonwebtoken</groupId>
45+
<artifactId>jjwt-jackson</artifactId>
46+
<version>${jjwt.version}</version>
47+
</dependency>
48+
<!-- Jackson -->
49+
<dependency>
50+
<groupId>com.fasterxml.jackson</groupId>
51+
<artifactId>jackson-bom</artifactId>
52+
<version>${jackson.version}</version>
53+
<type>pom</type>
54+
<scope>import</scope>
55+
</dependency>
56+
<!-- Reactor -->
57+
<dependency>
58+
<groupId>io.projectreactor</groupId>
59+
<artifactId>reactor-bom</artifactId>
60+
<version>${reactor.version}</version>
61+
<type>pom</type>
62+
<scope>import</scope>
63+
</dependency>
64+
<dependency>
65+
<groupId>org.slf4j</groupId>
66+
<artifactId>slf4j-api</artifactId>
67+
<version>${slf4j.version}</version>
68+
</dependency>
69+
</dependencies>
70+
</dependencyManagement>
71+
72+
<dependencies>
73+
<dependency>
74+
<groupId>io.jsonwebtoken</groupId>
75+
<artifactId>jjwt-api</artifactId>
76+
</dependency>
77+
<dependency>
78+
<groupId>io.jsonwebtoken</groupId>
79+
<artifactId>jjwt-impl</artifactId>
80+
</dependency>
81+
<dependency>
82+
<groupId>io.jsonwebtoken</groupId>
83+
<artifactId>jjwt-jackson</artifactId>
84+
</dependency>
85+
<dependency>
86+
<groupId>io.projectreactor</groupId>
87+
<artifactId>reactor-core</artifactId>
88+
</dependency>
89+
90+
<dependency>
91+
<groupId>org.slf4j</groupId>
92+
<artifactId>slf4j-api</artifactId>
93+
</dependency>
94+
95+
<dependency>
96+
<groupId>org.junit.jupiter</groupId>
97+
<artifactId>junit-jupiter</artifactId>
98+
<version>${junit-jupiter.version}</version>
99+
<scope>test</scope>
100+
</dependency>
101+
<dependency>
102+
<groupId>org.testcontainers</groupId>
103+
<artifactId>vault</artifactId>
104+
<version>${testcontainers.version}</version>
105+
<scope>test</scope>
106+
</dependency>
107+
<dependency>
108+
<groupId>com.bettercloud</groupId>
109+
<artifactId>vault-java-driver</artifactId>
110+
<version>${vault-java-driver.version}</version>
111+
<scope>test</scope>
112+
</dependency>
113+
<dependency>
114+
<groupId>org.mockito</groupId>
115+
<artifactId>mockito-junit-jupiter</artifactId>
116+
<version>${mockito.version}</version>
117+
<scope>test</scope>
118+
</dependency>
119+
<dependency>
120+
<groupId>org.junit.jupiter</groupId>
121+
<artifactId>junit-jupiter-engine</artifactId>
122+
<version>${junit-jupiter.version}</version>
123+
<scope>test</scope>
124+
</dependency>
125+
<dependency>
126+
<groupId>org.junit.jupiter</groupId>
127+
<artifactId>junit-jupiter-params</artifactId>
128+
<version>${junit-jupiter.version}</version>
129+
<scope>test</scope>
130+
</dependency>
131+
</dependencies>
132+
133+
</project>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
7+
public class JwtToken {
8+
9+
private final Map<String, Object> header;
10+
private final Map<String, Object> body;
11+
12+
public JwtToken(Map<String, Object> header, Map<String, Object> body) {
13+
this.header = Collections.unmodifiableMap(new HashMap<>(header));
14+
this.body = Collections.unmodifiableMap(new HashMap<>(body));
15+
}
16+
17+
public Map<String, Object> header() {
18+
return header;
19+
}
20+
21+
public Map<String, Object> body() {
22+
return body;
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import java.security.Key;
4+
5+
public interface JwtTokenParser {
6+
7+
JwtToken parseToken();
8+
9+
JwtToken verifyToken(Key key);
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
public interface JwtTokenParserFactory {
4+
5+
JwtTokenParser newParser(String token);
6+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import java.util.Map;
4+
import reactor.core.publisher.Mono;
5+
6+
@FunctionalInterface
7+
public interface JwtTokenResolver {
8+
9+
/**
10+
* Verifies and returns token claims if everything went ok.
11+
*
12+
* @param token jwt token
13+
* @return mono result with parsed claims (or error)
14+
*/
15+
Mono<Map<String, Object>> resolve(String token);
16+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import java.security.Key;
4+
import java.util.Map;
5+
import java.util.Objects;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.TimeUnit;
8+
import java.util.concurrent.atomic.AtomicReference;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import reactor.core.publisher.Mono;
12+
import reactor.core.scheduler.Scheduler;
13+
import reactor.core.scheduler.Schedulers;
14+
15+
public final class JwtTokenResolverImpl implements JwtTokenResolver {
16+
17+
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenResolver.class);
18+
19+
private final KeyProvider keyProvider;
20+
private final JwtTokenParserFactory tokenParserFactory;
21+
private final int cleanupIntervalSec;
22+
private final Scheduler scheduler;
23+
24+
private final Map<String, Mono<Key>> keyResolutions = new ConcurrentHashMap<>();
25+
26+
/**
27+
* Constructor.
28+
*
29+
* @param keyProvider key provider
30+
* @param tokenParserFactory token parser factoty
31+
*/
32+
public JwtTokenResolverImpl(KeyProvider keyProvider, JwtTokenParserFactory tokenParserFactory) {
33+
this(keyProvider, tokenParserFactory, 3600, Schedulers.newSingle("caching-key-provider", true));
34+
}
35+
36+
/**
37+
* Constructor.
38+
*
39+
* @param keyProvider key provider
40+
* @param tokenParserFactory token parser factoty
41+
* @param cleanupIntervalSec cleanup interval (in sec) for resolved cached keys
42+
* @param scheduler cleanup scheduler
43+
*/
44+
public JwtTokenResolverImpl(
45+
KeyProvider keyProvider,
46+
JwtTokenParserFactory tokenParserFactory,
47+
int cleanupIntervalSec,
48+
Scheduler scheduler) {
49+
this.keyProvider = keyProvider;
50+
this.tokenParserFactory = tokenParserFactory;
51+
this.cleanupIntervalSec = cleanupIntervalSec;
52+
this.scheduler = scheduler;
53+
}
54+
55+
@Override
56+
public Mono<Map<String, Object>> resolve(String token) {
57+
return Mono.defer(
58+
() -> {
59+
JwtTokenParser tokenParser = tokenParserFactory.newParser(token);
60+
JwtToken jwtToken = tokenParser.parseToken();
61+
62+
Map<String, Object> header = jwtToken.header();
63+
String kid = (String) header.get("kid");
64+
Objects.requireNonNull(kid, "kid is missing");
65+
66+
Map<String, Object> body = jwtToken.body();
67+
String aud = (String) body.get("aud"); // optional
68+
69+
LOGGER.debug(
70+
"[resolveToken][aud:{}][kid:{}] Resolving token {}", aud, kid, Utils.mask(token));
71+
72+
// workaround to remove safely on errors
73+
AtomicReference<Mono<Key>> computedValueHolder = new AtomicReference<>();
74+
75+
return findKey(kid, computedValueHolder)
76+
.map(key -> tokenParser.verifyToken(key).body())
77+
.doOnError(throwable -> cleanup(kid, computedValueHolder))
78+
.doOnError(
79+
throwable ->
80+
LOGGER.error(
81+
"[resolveToken][aud:{}][kid:{}][{}] Exception occurred: {}",
82+
aud,
83+
kid,
84+
Utils.mask(token),
85+
throwable.toString()))
86+
.doOnSuccess(
87+
s ->
88+
LOGGER.debug(
89+
"[resolveToken][aud:{}][kid:{}] Resolved token {}",
90+
aud,
91+
kid,
92+
Utils.mask(token)));
93+
});
94+
}
95+
96+
private Mono<Key> findKey(String kid, AtomicReference<Mono<Key>> computedValueHolder) {
97+
return keyResolutions.computeIfAbsent(
98+
kid,
99+
(kid1) -> {
100+
Mono<Key> result =
101+
computedValueHolder.updateAndGet(
102+
mono -> Mono.defer(() -> keyProvider.findKey(kid)).cache());
103+
scheduleCleanup(kid, computedValueHolder);
104+
return result;
105+
});
106+
}
107+
108+
private void scheduleCleanup(String kid, AtomicReference<Mono<Key>> computedValueHolder) {
109+
scheduler.schedule(
110+
() -> cleanup(kid, computedValueHolder), cleanupIntervalSec, TimeUnit.SECONDS);
111+
}
112+
113+
private void cleanup(String kid, AtomicReference<Mono<Key>> computedValueHolder) {
114+
if (computedValueHolder.get() != null) {
115+
keyResolutions.remove(kid, computedValueHolder.get());
116+
}
117+
}
118+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import java.security.Key;
4+
import reactor.core.publisher.Mono;
5+
6+
@FunctionalInterface
7+
public interface KeyProvider {
8+
9+
/**
10+
* Finds key for jwt token verification.
11+
*
12+
* @param kid key id token attribute
13+
* @return mono result with key (or error)
14+
*/
15+
Mono<Key> findKey(String kid);
16+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.scalecube.security.tokens.jwt;
2+
3+
import java.math.BigInteger;
4+
import java.security.Key;
5+
import java.security.KeyFactory;
6+
import java.security.spec.KeySpec;
7+
import java.security.spec.RSAPublicKeySpec;
8+
import java.util.Base64;
9+
import java.util.Base64.Decoder;
10+
import reactor.core.Exceptions;
11+
12+
public class Utils {
13+
14+
private Utils() {
15+
// Do not instantiate
16+
}
17+
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 getRsaPublicKey(String n, String e) {
26+
Decoder b64Decoder = Base64.getUrlDecoder();
27+
BigInteger modulus = new BigInteger(1, b64Decoder.decode(n));
28+
BigInteger exponent = new BigInteger(1, b64Decoder.decode(e));
29+
KeySpec keySpec = new RSAPublicKeySpec(modulus, exponent);
30+
try {
31+
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
32+
} catch (Exception ex) {
33+
throw Exceptions.propagate(ex);
34+
}
35+
}
36+
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) {
44+
if (data == null || data.isEmpty() || data.length() < 5) {
45+
return "*****";
46+
}
47+
48+
return data.replace(data.substring(2, data.length() - 2), "***");
49+
}
50+
}

0 commit comments

Comments
 (0)