Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<modules>
<module>jwt</module>
<module>tokens</module>
</modules>

<properties>
Expand Down
133 changes: 133 additions & 0 deletions tokens/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.scalecube</groupId>
<artifactId>scalecube-security-parent</artifactId>
<version>1.0.10-SNAPSHOT</version>
</parent>

<artifactId>scalecube-security-tokens</artifactId>

<properties>
<jjwt.version>0.11.1</jjwt.version>

<reactor.version>Dysprosium-SR7</reactor.version>
<jackson.version>2.11.0</jackson.version>
<slf4j.version>1.7.30</slf4j.version>

<junit-jupiter.version>5.4.2</junit-jupiter.version>
<vault-java-driver.version>5.0.0</vault-java-driver.version>
<testcontainers.version>1.14.0</testcontainers.version>
<mockito.version>3.1.0</mockito.version>

<github.repository>exberry-io/${project.artifactId}</github.repository>
</properties>

<dependencyManagement>
<dependencies>
<!-- Jsonwebtoken -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Reactor -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>${reactor.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>vault</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.bettercloud</groupId>
<artifactId>vault-java-driver</artifactId>
<version>${vault-java-driver.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.scalecube.security.tokens.jwt;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class JwtToken {

private final Map<String, Object> header;
private final Map<String, Object> body;

public JwtToken(Map<String, Object> header, Map<String, Object> body) {
this.header = Collections.unmodifiableMap(new HashMap<>(header));
this.body = Collections.unmodifiableMap(new HashMap<>(body));
}

public Map<String, Object> header() {
return header;
}

public Map<String, Object> body() {
return body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.scalecube.security.tokens.jwt;

import java.security.Key;

public interface JwtTokenParser {

JwtToken parseToken();

JwtToken verifyToken(Key key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.scalecube.security.tokens.jwt;

public interface JwtTokenParserFactory {

JwtTokenParser newParser(String token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.scalecube.security.tokens.jwt;

import java.util.Map;
import reactor.core.publisher.Mono;

@FunctionalInterface
public interface JwtTokenResolver {

/**
* Verifies and returns token claims if everything went ok.
*
* @param token jwt token
* @return mono result with parsed claims (or error)
*/
Mono<Map<String, Object>> resolve(String token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.scalecube.security.tokens.jwt;

import java.security.Key;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

public final class JwtTokenResolverImpl implements JwtTokenResolver {

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

private final KeyProvider keyProvider;
private final JwtTokenParserFactory tokenParserFactory;
private final int cleanupIntervalSec;
private final Scheduler scheduler;

private final Map<String, Mono<Key>> keyResolutions = new ConcurrentHashMap<>();

/**
* Constructor.
*
* @param keyProvider key provider
* @param tokenParserFactory token parser factoty
*/
public JwtTokenResolverImpl(KeyProvider keyProvider, JwtTokenParserFactory tokenParserFactory) {
this(keyProvider, tokenParserFactory, 3600, Schedulers.newSingle("caching-key-provider", true));
}

/**
* Constructor.
*
* @param keyProvider key provider
* @param tokenParserFactory token parser factoty
* @param cleanupIntervalSec cleanup interval (in sec) for resolved cached keys
* @param scheduler cleanup scheduler
*/
public JwtTokenResolverImpl(
KeyProvider keyProvider,
JwtTokenParserFactory tokenParserFactory,
int cleanupIntervalSec,
Scheduler scheduler) {
this.keyProvider = keyProvider;
this.tokenParserFactory = tokenParserFactory;
this.cleanupIntervalSec = cleanupIntervalSec;
this.scheduler = scheduler;
}

@Override
public Mono<Map<String, Object>> resolve(String token) {
return Mono.defer(
() -> {
JwtTokenParser tokenParser = tokenParserFactory.newParser(token);
JwtToken jwtToken = tokenParser.parseToken();

Map<String, Object> header = jwtToken.header();
String kid = (String) header.get("kid");
Objects.requireNonNull(kid, "kid is missing");

Map<String, Object> body = jwtToken.body();
String aud = (String) body.get("aud"); // optional

LOGGER.debug(
"[resolveToken][aud:{}][kid:{}] Resolving token {}", aud, kid, Utils.mask(token));

// workaround to remove safely on errors
AtomicReference<Mono<Key>> computedValueHolder = new AtomicReference<>();

return findKey(kid, computedValueHolder)
.map(key -> tokenParser.verifyToken(key).body())
.doOnError(throwable -> cleanup(kid, computedValueHolder))
.doOnError(
throwable ->
LOGGER.error(
"[resolveToken][aud:{}][kid:{}][{}] Exception occurred: {}",
aud,
kid,
Utils.mask(token),
throwable.toString()))
.doOnSuccess(
s ->
LOGGER.debug(
"[resolveToken][aud:{}][kid:{}] Resolved token {}",
aud,
kid,
Utils.mask(token)));
});
}

private Mono<Key> findKey(String kid, AtomicReference<Mono<Key>> computedValueHolder) {
return keyResolutions.computeIfAbsent(
kid,
(kid1) -> {
Mono<Key> result =
computedValueHolder.updateAndGet(
mono -> Mono.defer(() -> keyProvider.findKey(kid)).cache());
scheduleCleanup(kid, computedValueHolder);
return result;
});
}

private void scheduleCleanup(String kid, AtomicReference<Mono<Key>> computedValueHolder) {
scheduler.schedule(
() -> cleanup(kid, computedValueHolder), cleanupIntervalSec, TimeUnit.SECONDS);
}

private void cleanup(String kid, AtomicReference<Mono<Key>> computedValueHolder) {
if (computedValueHolder.get() != null) {
keyResolutions.remove(kid, computedValueHolder.get());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.scalecube.security.tokens.jwt;

import java.security.Key;
import reactor.core.publisher.Mono;

@FunctionalInterface
public interface KeyProvider {

/**
* Finds key for jwt token verification.
*
* @param kid key id token attribute
* @return mono result with key (or error)
*/
Mono<Key> findKey(String kid);
}
50 changes: 50 additions & 0 deletions tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.scalecube.security.tokens.jwt;

import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.KeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import java.util.Base64.Decoder;
import reactor.core.Exceptions;

public class Utils {

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 getRsaPublicKey(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);
}
}

/**
* 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) {
if (data == null || data.isEmpty() || data.length() < 5) {
return "*****";
}

return data.replace(data.substring(2, data.length() - 2), "***");
}
}
Loading