From 81c1f186dc342185a9103a5fb4a1a10ba490ab93 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Thu, 14 May 2020 11:04:24 +0300 Subject: [PATCH 1/3] Start on add security tokens module --- pom.xml | 1 + tokens/pom.xml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tokens/pom.xml diff --git a/pom.xml b/pom.xml index 721e8d3..5723a03 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ jwt + tokens diff --git a/tokens/pom.xml b/tokens/pom.xml new file mode 100644 index 0000000..bd9ee05 --- /dev/null +++ b/tokens/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + io.scalecube + scalecube-security-parent + 1.0.10-SNAPSHOT + + + scalecube-security-tokens + + + + From 1fb4ff6dc840cd5fbbe64c7559bf37b8ab034932 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Thu, 14 May 2020 13:08:32 +0300 Subject: [PATCH 2/3] Added tokens module --- tokens/pom.xml | 116 ++++++++++ .../security/tokens/jwt/JwtToken.java | 24 ++ .../security/tokens/jwt/JwtTokenParser.java | 10 + .../tokens/jwt/JwtTokenParserFactory.java | 6 + .../security/tokens/jwt/JwtTokenResolver.java | 16 ++ .../tokens/jwt/JwtTokenResolverImpl.java | 118 ++++++++++ .../security/tokens/jwt/KeyProvider.java | 16 ++ .../scalecube/security/tokens/jwt/Utils.java | 50 +++++ .../jwt/jsonwebtoken/JsonwebtokenParser.java | 45 ++++ .../JsonwebtokenParserFactory.java | 16 ++ .../security/tokens/jwt/vault/VaultJwk.java | 61 +++++ .../tokens/jwt/vault/VaultJwkList.java | 27 +++ .../jwt/vault/VaultJwksKeyProvider.java | 86 +++++++ .../com/om2/exchange/tokens/jwt/BaseTest.java | 49 ++++ .../tokens/jwt/JwtTokenResolverTests.java | 167 ++++++++++++++ .../jwt/vault/VaultJwksKeyProviderTests.java | 209 ++++++++++++++++++ ...token-and-pubkey.after-rotation.properties | 4 + .../resources/token-and-pubkey.properties | 4 + .../token-and-wrong-pubkey.properties | 4 + 19 files changed, 1028 insertions(+) create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwk.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwkList.java create mode 100644 tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwksKeyProvider.java create mode 100644 tokens/src/test/java/com/om2/exchange/tokens/jwt/BaseTest.java create mode 100644 tokens/src/test/java/com/om2/exchange/tokens/jwt/JwtTokenResolverTests.java create mode 100644 tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java create mode 100644 tokens/src/test/resources/token-and-pubkey.after-rotation.properties create mode 100644 tokens/src/test/resources/token-and-pubkey.properties create mode 100644 tokens/src/test/resources/token-and-wrong-pubkey.properties diff --git a/tokens/pom.xml b/tokens/pom.xml index bd9ee05..c979dde 100644 --- a/tokens/pom.xml +++ b/tokens/pom.xml @@ -12,6 +12,122 @@ scalecube-security-tokens + + 0.11.1 + Dysprosium-SR7 + 2.11.0 + 1.7.30 + + 5.4.2 + 5.0.0 + 1.14.0 + 3.1.0 + + exberry-io/${project.artifactId} + + + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + io.projectreactor + reactor-bom + ${reactor.version} + pom + import + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + + + io.projectreactor + reactor-core + + + + org.slf4j + slf4j-api + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + org.testcontainers + vault + ${testcontainers.version} + test + + + com.bettercloud + vault-java-driver + ${vault-java-driver.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java new file mode 100644 index 0000000..3346751 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtToken.java @@ -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 header; + private final Map body; + + public JwtToken(Map header, Map body) { + this.header = Collections.unmodifiableMap(new HashMap<>(header)); + this.body = Collections.unmodifiableMap(new HashMap<>(body)); + } + + public Map header() { + return header; + } + + public Map body() { + return body; + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java new file mode 100644 index 0000000..6b3600f --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParser.java @@ -0,0 +1,10 @@ +package io.scalecube.security.tokens.jwt; + +import java.security.Key; + +public interface JwtTokenParser { + + JwtToken parseToken(); + + JwtToken verifyToken(Key key); +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java new file mode 100644 index 0000000..f5ef270 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenParserFactory.java @@ -0,0 +1,6 @@ +package io.scalecube.security.tokens.jwt; + +public interface JwtTokenParserFactory { + + JwtTokenParser newParser(String token); +} 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 new file mode 100644 index 0000000..6768c2b --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java @@ -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> resolve(String token); +} 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 new file mode 100644 index 0000000..c344fb0 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolverImpl.java @@ -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> 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> resolve(String token) { + return Mono.defer( + () -> { + JwtTokenParser tokenParser = tokenParserFactory.newParser(token); + JwtToken jwtToken = tokenParser.parseToken(); + + Map header = jwtToken.header(); + String kid = (String) header.get("kid"); + Objects.requireNonNull(kid, "kid is missing"); + + Map 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> 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 findKey(String kid, AtomicReference> computedValueHolder) { + return keyResolutions.computeIfAbsent( + kid, + (kid1) -> { + Mono result = + computedValueHolder.updateAndGet( + mono -> Mono.defer(() -> keyProvider.findKey(kid)).cache()); + scheduleCleanup(kid, computedValueHolder); + return result; + }); + } + + private void scheduleCleanup(String kid, AtomicReference> computedValueHolder) { + scheduler.schedule( + () -> cleanup(kid, computedValueHolder), cleanupIntervalSec, TimeUnit.SECONDS); + } + + private void cleanup(String kid, AtomicReference> computedValueHolder) { + if (computedValueHolder.get() != null) { + keyResolutions.remove(kid, computedValueHolder.get()); + } + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java new file mode 100644 index 0000000..2a3f22d --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/KeyProvider.java @@ -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 findKey(String kid); +} 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 new file mode 100644 index 0000000..c33a2da --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/Utils.java @@ -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), "***"); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java new file mode 100644 index 0000000..d97764d --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParser.java @@ -0,0 +1,45 @@ +package io.scalecube.security.tokens.jwt.jsonwebtoken; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtParserBuilder; +import io.scalecube.security.tokens.jwt.JwtToken; +import io.scalecube.security.tokens.jwt.JwtTokenParser; +import java.security.Key; + +public class JsonwebtokenParser implements JwtTokenParser { + + private final String token; + private final String justClaims; + private final JwtParserBuilder parserBuilder; + + /** + * Constructor. + * + * @param token jwt token + * @param justClaims just claims + * @param parserBuilder parser builder + */ + public JsonwebtokenParser(String token, String justClaims, JwtParserBuilder parserBuilder) { + this.token = token; + this.justClaims = justClaims; + this.parserBuilder = parserBuilder; + } + + @Override + public JwtToken parseToken() { + //noinspection rawtypes + Jwt jwt = parserBuilder.build().parseClaimsJwt(justClaims); + //noinspection unchecked + return new JwtToken(jwt.getHeader(), jwt.getBody()); + } + + @Override + public JwtToken verifyToken(Key key) { + Jws jws = parserBuilder.setSigningKey(key).build().parseClaimsJws(token); + //noinspection unchecked + return new JwtToken(jws.getHeader(), jws.getBody()); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java new file mode 100644 index 0000000..e3ef719 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/jsonwebtoken/JsonwebtokenParserFactory.java @@ -0,0 +1,16 @@ +package io.scalecube.security.tokens.jwt.jsonwebtoken; + +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Jwts; +import io.scalecube.security.tokens.jwt.JwtTokenParser; +import io.scalecube.security.tokens.jwt.JwtTokenParserFactory; + +public class JsonwebtokenParserFactory implements JwtTokenParserFactory { + + @Override + public JwtTokenParser newParser(String token) { + String justClaims = token.substring(0, token.lastIndexOf(".") + 1); + JwtParserBuilder parserBuilder = Jwts.parserBuilder(); + return new JsonwebtokenParser(token, justClaims, parserBuilder); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwk.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwk.java new file mode 100644 index 0000000..c2eccc2 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwk.java @@ -0,0 +1,61 @@ +package io.scalecube.security.tokens.jwt.vault; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.StringJoiner; + +public class VaultJwk { + + private String use; + private String kty; + private String kid; + private String alg; + + @JsonProperty("n") + private String modulus; // n + + @JsonProperty("e") + private String exponent; // e + + /** + * Serialization only constructor. + * + * @deprecated not to be used + */ + public VaultJwk() {} + + public String use() { + return use; + } + + public String kty() { + return kty; + } + + public String kid() { + return kid; + } + + public String alg() { + return alg; + } + + public String modulus() { + return modulus; + } + + public String exponent() { + return exponent; + } + + @Override + public String toString() { + return new StringJoiner(", ", VaultJwk.class.getSimpleName() + "[", "]") + .add("use='" + use + "'") + .add("kty='" + kty + "'") + .add("kid='" + kid + "'") + .add("alg='" + alg + "'") + .add("n='" + modulus + "'") + .add("e='" + exponent + "'") + .toString(); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwkList.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwkList.java new file mode 100644 index 0000000..029dba0 --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwkList.java @@ -0,0 +1,27 @@ +package io.scalecube.security.tokens.jwt.vault; + +import java.util.List; +import java.util.StringJoiner; + +public class VaultJwkList { + + private List keys; + + /** + * Serialization only constructor. + * + * @deprecated not to be used + */ + public VaultJwkList() {} + + public List keys() { + return keys; + } + + @Override + public String toString() { + return new StringJoiner(", ", VaultJwkList.class.getSimpleName() + "[", "]") + .add("keys=" + keys) + .toString(); + } +} diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwksKeyProvider.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwksKeyProvider.java new file mode 100644 index 0000000..16e308c --- /dev/null +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/vault/VaultJwksKeyProvider.java @@ -0,0 +1,86 @@ +package io.scalecube.security.tokens.jwt.vault; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import io.scalecube.security.tokens.jwt.KeyProvider; +import io.scalecube.security.tokens.jwt.Utils; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.security.Key; +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; + +public final class VaultJwksKeyProvider implements KeyProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(VaultJwksKeyProvider.class); + + private final Scheduler scheduler = Schedulers.newSingle("vault-jwks", true); + + private final ObjectMapper mapper; + + private final String jwksUri; + + public VaultJwksKeyProvider(String jwksUri) { + this.jwksUri = jwksUri; + this.mapper = initMapper(); + } + + @Override + public Mono findKey(String kid) { + return Mono.defer(this::callJwksUri) + .map(stream -> toRsaKey(stream, kid)) + .doOnSubscribe(s -> LOGGER.debug("[findKey] Looking up key in jwks, kid: {}", kid)) + .subscribeOn(scheduler); + } + + private Mono callJwksUri() { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(jwksUri)).build(); + return Mono.fromFuture( + client.sendAsync(request, BodyHandlers.ofInputStream()).thenApply(HttpResponse::body)); + } + + private Key toRsaKey(InputStream stream, String kid) { + return getKeyList(stream).keys().stream() + .filter(k -> kid.equals(k.kid())) + .filter(k -> "RSA".equals(k.kty())) // RSA + .filter(k -> "sig".equals(k.use())) // signature + .findFirst() + .map(vaultJwk -> Utils.getRsaPublicKey(vaultJwk.modulus(), vaultJwk.exponent())) + .orElseThrow(() -> new RuntimeException("Key was not found, kid: " + kid)); + } + + private VaultJwkList getKeyList(InputStream stream) { + VaultJwkList list; + try { + list = mapper.readValue(stream, VaultJwkList.class); + } catch (IOException e) { + throw Exceptions.propagate(e); + } + return list; + } + + private static ObjectMapper initMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper; + } +} diff --git a/tokens/src/test/java/com/om2/exchange/tokens/jwt/BaseTest.java b/tokens/src/test/java/com/om2/exchange/tokens/jwt/BaseTest.java new file mode 100644 index 0000000..472469c --- /dev/null +++ b/tokens/src/test/java/com/om2/exchange/tokens/jwt/BaseTest.java @@ -0,0 +1,49 @@ +package com.om2.exchange.tokens.jwt; + +import java.lang.reflect.Method; +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.test.StepVerifier; + +public class BaseTest { + + protected static final Logger LOGGER = LoggerFactory.getLogger(BaseTest.class); + + public static final Duration TIMEOUT = Duration.ofSeconds(10); + + @BeforeAll + public static void init() { + StepVerifier.setDefaultTimeout(TIMEOUT); + } + + @AfterAll + public static void reset() { + StepVerifier.resetDefaultTimeout(); + } + + @BeforeEach + public final void baseSetUp(TestInfo testInfo) { + LOGGER.info( + "***** Test started : " + + testInfo.getTestClass().map(Class::getSimpleName).orElse("") + + "." + + testInfo.getTestMethod().map(Method::getName).orElse("") + + " *****"); + } + + @AfterEach + public final void baseTearDown(TestInfo testInfo) { + LOGGER.info( + "***** Test finished : " + + testInfo.getTestClass().map(Class::getSimpleName).orElse("") + + "." + + testInfo.getTestMethod().map(Method::getName).orElse("") + + " *****"); + } +} diff --git a/tokens/src/test/java/com/om2/exchange/tokens/jwt/JwtTokenResolverTests.java b/tokens/src/test/java/com/om2/exchange/tokens/jwt/JwtTokenResolverTests.java new file mode 100644 index 0000000..e7af519 --- /dev/null +++ b/tokens/src/test/java/com/om2/exchange/tokens/jwt/JwtTokenResolverTests.java @@ -0,0 +1,167 @@ +package com.om2.exchange.tokens.jwt; + +import io.scalecube.security.tokens.jwt.JwtToken; +import io.scalecube.security.tokens.jwt.JwtTokenParser; +import io.scalecube.security.tokens.jwt.JwtTokenParserFactory; +import io.scalecube.security.tokens.jwt.JwtTokenResolverImpl; +import io.scalecube.security.tokens.jwt.KeyProvider; +import io.scalecube.security.tokens.jwt.Utils; +import java.io.IOException; +import java.security.Key; +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; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class JwtTokenResolverTests extends BaseTest { + + public static final Map BODY = Collections.singletonMap("aud", "aud"); + + @Test + void testTokenResolver() throws IOException { + TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties"); + + JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); + Mockito.when(tokenParser.parseToken()) + .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithKey.kid), BODY)); + Mockito.when(tokenParser.verifyToken(tokenWithKey.key)) + .thenReturn(Mockito.mock(JwtToken.class)); + + JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); + Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) + .thenReturn(tokenParser); + + KeyProvider keyProvider = Mockito.mock(KeyProvider.class); + Mockito.when(keyProvider.findKey(tokenWithKey.kid)).thenReturn(Mono.just(tokenWithKey.key)); + + JwtTokenResolverImpl tokenResolver = new JwtTokenResolverImpl(keyProvider, tokenParserFactory); + + // N times call resolve + StepVerifier.create(tokenResolver.resolve(tokenWithKey.token).repeat(3)) + .expectNextCount(3) + .thenCancel() + .verify(); + + // check caching, must have been called 1 time + Mockito.verify(keyProvider, Mockito.times(1)).findKey(tokenWithKey.kid); + } + + @Test + void testTokenResolverWithRotatingKey() throws IOException { + TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties"); + TokenWithKey tokenWithKeyAfterRotation = + new TokenWithKey("token-and-pubkey.after-rotation.properties"); + + JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); + Mockito.when(tokenParser.parseToken()) + .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithKey.kid), BODY)) + .thenReturn( + new JwtToken(Collections.singletonMap("kid", tokenWithKeyAfterRotation.kid), BODY)); + + Mockito.when(tokenParser.verifyToken(tokenWithKey.key)) + .thenReturn(Mockito.mock(JwtToken.class)); + Mockito.when(tokenParser.verifyToken(tokenWithKeyAfterRotation.key)) + .thenReturn(Mockito.mock(JwtToken.class)); + + JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); + Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) + .thenReturn(tokenParser); + + KeyProvider keyProvider = Mockito.mock(KeyProvider.class); + Mockito.when(keyProvider.findKey(tokenWithKey.kid)).thenReturn(Mono.just(tokenWithKey.key)); + Mockito.when(keyProvider.findKey(tokenWithKeyAfterRotation.kid)) + .thenReturn(Mono.just(tokenWithKeyAfterRotation.key)); + + JwtTokenResolverImpl tokenResolver = new JwtTokenResolverImpl(keyProvider, tokenParserFactory); + + // Call normal token first + StepVerifier.create(tokenResolver.resolve(tokenWithKey.token)) + .expectNextCount(1) + .expectComplete() + .verify(); + + // Call token after rotation (call N times) + StepVerifier.create(tokenResolver.resolve(tokenWithKeyAfterRotation.token).repeat(3)) + .expectNextCount(3) + .thenCancel() + .verify(); + + // in total must have been called 2 times + Mockito.verify(keyProvider, Mockito.times(1)).findKey(tokenWithKey.kid); + Mockito.verify(keyProvider, Mockito.times(1)).findKey(tokenWithKeyAfterRotation.kid); + } + + @Test + void testTokenResolverWithWrongKey() throws IOException { + TokenWithKey tokenWithWrongKey = new TokenWithKey("token-and-wrong-pubkey.properties"); + + JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); + Mockito.when(tokenParser.parseToken()) + .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithWrongKey.kid), BODY)); + Mockito.when(tokenParser.verifyToken(tokenWithWrongKey.key)).thenThrow(RuntimeException.class); + + JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); + Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) + .thenReturn(tokenParser); + + KeyProvider keyProvider = Mockito.mock(KeyProvider.class); + Mockito.when(keyProvider.findKey(tokenWithWrongKey.kid)) + .thenReturn(Mono.just(tokenWithWrongKey.key)); + + JwtTokenResolverImpl tokenResolver = new JwtTokenResolverImpl(keyProvider, tokenParserFactory); + + // Must fail (retry N times) + StepVerifier.create(tokenResolver.resolve(tokenWithWrongKey.token).retry(1)) + .expectError() + .verify(); + + // failed resolution not stored => keyProvider must have been called 2 times + Mockito.verify(keyProvider, Mockito.times(2)).findKey(tokenWithWrongKey.kid); + } + + @Test + void testTokenResolverWhenKeyProviderFailing() throws IOException { + TokenWithKey tokenWithKey = new TokenWithKey("token-and-pubkey.properties"); + + JwtTokenParser tokenParser = Mockito.mock(JwtTokenParser.class); + Mockito.when(tokenParser.parseToken()) + .thenReturn(new JwtToken(Collections.singletonMap("kid", tokenWithKey.kid), BODY)); + Mockito.when(tokenParser.verifyToken(tokenWithKey.key)) + .thenReturn(Mockito.mock(JwtToken.class)); + + JwtTokenParserFactory tokenParserFactory = Mockito.mock(JwtTokenParserFactory.class); + Mockito.when(tokenParserFactory.newParser(ArgumentMatchers.anyString())) + .thenReturn(tokenParser); + + KeyProvider keyProvider = Mockito.mock(KeyProvider.class); + Mockito.when(keyProvider.findKey(tokenWithKey.kid)).thenThrow(RuntimeException.class); + + JwtTokenResolverImpl tokenResolver = new JwtTokenResolverImpl(keyProvider, tokenParserFactory); + + // Must fail with "hola" (retry N times) + StepVerifier.create(tokenResolver.resolve(tokenWithKey.token).retry(1)).expectError().verify(); + + // 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 IOException { + 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 = Utils.getRsaPublicKey(props.getProperty("n"), props.getProperty("e")); + } + } +} diff --git a/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java b/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java new file mode 100644 index 0000000..e3cf069 --- /dev/null +++ b/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java @@ -0,0 +1,209 @@ +package com.om2.exchange.tokens.jwt.vault; + +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestException; +import com.bettercloud.vault.rest.RestResponse; +import com.om2.exchange.tokens.jwt.BaseTest; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Jwts; +import io.scalecube.security.tokens.jwt.vault.VaultJwksKeyProvider; +import java.io.IOException; +import java.util.UUID; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.vault.VaultContainer; +import reactor.test.StepVerifier; + +class VaultJwksKeyProviderTests extends BaseTest { + + private static final String VAULT_TOKEN = "test"; + private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; + + private static final GenericContainer VAULT_CONTAINER = + new VaultContainer("vault:1.4.0") + .withVaultToken(VAULT_TOKEN) + .waitingFor(new LogMessageWaitStrategy().withRegEx("^.*Vault server started!.*$")); + + private static String vaultAddr; + + @BeforeAll + static void beforeAll() { + VAULT_CONTAINER.start(); + vaultAddr = "http://localhost:" + VAULT_CONTAINER.getMappedPort(8200); + } + + @AfterAll + static void afterAll() { + VAULT_CONTAINER.stop(); + } + + @Test + void testJwksKeysRetrieval() throws RestException, IOException, InterruptedException { + String keyName = createIdentityKey(vaultAddr); // oidc/key + String roleName = createIdentityRole(vaultAddr, keyName); // oidc/role + createIdentityTokenPolicy(roleName); // write policy policyfile.hcl + String clientToken = createEntity(roleName); // onboard some entity with policy line above + String token = generateIdentityToken(clientToken, roleName); // oidc/token + String kid = getKid(token); + + VaultJwksKeyProvider keyProvider = new VaultJwksKeyProvider(jwksUri(vaultAddr)); + + StepVerifier.create(keyProvider.findKey(kid)).expectNextCount(1).expectComplete().verify(); + } + + @Test + void testJwksKeysRetrievalKeyNotFound() { + VaultJwksKeyProvider keyProvider = new VaultJwksKeyProvider(jwksUri(vaultAddr)); + + StepVerifier.create(keyProvider.findKey(UUID.randomUUID().toString())) + .expectErrorMatches( + th -> th.getMessage() != null && th.getMessage().contains("Key was not found")) + .verify(); + } + + private static String getKid(String token) { + String justClaims = token.substring(0, token.lastIndexOf(".") + 1); + JwtParserBuilder parserBuilder = Jwts.parserBuilder(); + //noinspection rawtypes + Jwt claims = parserBuilder.build().parseClaimsJwt(justClaims); + //noinspection rawtypes + Header header = claims.getHeader(); + return (String) header.get("kid"); + } + + private static String generateIdentityToken(String clientToken, String roleName) + throws RestException { + RestResponse restResponse = + new Rest() + .header(VAULT_TOKEN_HEADER, clientToken) + .url(oidcToken(vaultAddr, roleName)) + .get(); + int status = restResponse.getStatus(); + + if (status != 200 && status != 204) { + throw new IllegalStateException( + "Unexpected status code on identity token creation: " + status); + } + + return Json.parse(new String(restResponse.getBody())) + .asObject() + .get("data") + .asObject() + .get("token") + .asString(); + } + + private static void createIdentityTokenPolicy(String roleName) throws RestException { + int status = + new Rest() + .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) + .url(policiesAclUri(vaultAddr, roleName)) + .body( + ("{\"policy\":\"path \\\"identity/oidc/token/" + + roleName + + "\\\" {capabilities=[\\\"create\\\", \\\"read\\\"]}\"}") + .getBytes()) + .post() + .getStatus(); + + if (status != 200 && status != 204) { + throw new IllegalStateException( + "Unexpected status code on identity token policy creation: " + status); + } + } + + private static String createEntity(final String roleName) + throws IOException, InterruptedException { + + checkSuccess( + VAULT_CONTAINER.execInContainer("vault auth enable userpass".split("\\s")).getExitCode()); + checkSuccess( + VAULT_CONTAINER + .execInContainer( + ("vault write auth/userpass/users/abc password=abc policies=" + roleName) + .split("\\s")) + .getExitCode()); + + ExecResult loginExecResult = + VAULT_CONTAINER.execInContainer( + "vault login -format json -method=userpass username=abc password=abc".split("\\s")); + checkSuccess(loginExecResult.getExitCode()); + return Json.parse(loginExecResult.getStdout().replaceAll("\\r?\\n", "")) + .asObject() + .get("auth") + .asObject() + .get("client_token") + .asString(); + } + + private static void checkSuccess(int exitCode) { + if (exitCode != 0) { + throw new IllegalStateException("Exited with error: " + exitCode); + } + } + + private static String createIdentityKey(String vaultAddr) throws RestException { + String keyName = UUID.randomUUID().toString(); + int status = + new Rest() + .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) + .url(oidcKeyUrl(vaultAddr, keyName)) + .body( + ("{\"rotation_period\":\"1h\", " + + "\"verification_ttl\": 0, " + + "\"allowed_client_ids\": \"*\", " + + "\"algorithm\": \"RS256\"}") + .getBytes()) + .post() + .getStatus(); + + if (status != 200 && status != 204) { + throw new IllegalStateException("Unexpected status code on oidc/key creation: " + status); + } + return keyName; + } + + private static String createIdentityRole(String vaultAddr, String keyName) throws RestException { + String roleName = UUID.randomUUID().toString(); + int status = + new Rest() + .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) + .url(oidcRoleUrl(vaultAddr, roleName)) + .body(("{\"key\":\"" + keyName + "\",\"ttl\": \"1h\"}").getBytes()) + .post() + .getStatus(); + + if (status != 200 && status != 204) { + throw new IllegalStateException("Unexpected status code on oidc/role creation: " + status); + } + return roleName; + } + + private static String oidcKeyUrl(String vaultAddr, String keyName) { + return vaultAddr + "/v1/identity/oidc/key/" + keyName; + } + + private static String oidcRoleUrl(String vaultAddr, String roleName) { + return vaultAddr + "/v1/identity/oidc/role/" + roleName; + } + + private static String oidcToken(String vaultAddr, String roleName) { + return vaultAddr + "/v1/identity/oidc/token/" + roleName; + } + + private static String jwksUri(String vaultAddr) { + return vaultAddr + "/v1/identity/oidc/.well-known/keys"; + } + + private static String policiesAclUri(String vaultAddr, String roleName) { + return vaultAddr + "/v1/sys/policies/acl/" + roleName; + } +} diff --git a/tokens/src/test/resources/token-and-pubkey.after-rotation.properties b/tokens/src/test/resources/token-and-pubkey.after-rotation.properties new file mode 100644 index 0000000..8668fd6 --- /dev/null +++ b/tokens/src/test/resources/token-and-pubkey.after-rotation.properties @@ -0,0 +1,4 @@ +kid=bae2ad9c-6f11-a20c-993a-171221826b80 +token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImJhZTJhZDljLTZmMTEtYTIwYy05OTNhLTE3MTIyMTgyNmI4MCJ9.eyJhdWQiOiJ0Q0dRRDFkdUJUZjdST2ZucE1NQ0lDU01xRCIsImV4Y2hhbmdlX2lkIjoxLCJleHAiOjE1ODkxMjUzMjYsImlhdCI6MTU4OTAzODkyNiwiaXNzIjoiaHR0cDovLzAuMC4wLjA6ODIwMC92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInBlcm1pc3Npb25zIjpbIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpuZXdfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjp1cGRhdGVfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjphc3NpZ25fY2FsZW5kYXIiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246YXNzaWduX2NiciIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpyZW1vdmVfY2JyIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOnRyYWRpbmdfYWN0aW9uOmhhbHQiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246dHJhZGluZ19hY3Rpb246cmVzdW1lIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOm9wZXJhdGlvbl9ldmVudHMiXSwic3ViIjoiMDY4OTdiNTAtOGQ5Zi0xZDRkLWEwZjUtOGMxNmQ1ZGU4MzIwIn0.FEEB3cL3k2cFacdBFKCewW5M0BjKwp2uhz_CuLBus7aTqfSG1rtIrViPi_Y9a889kaG5j38FhRkG1fzgvEv7BeE6ar3XfrUtDMY7fvDDIr1LKs58KtK6I-ZTI6MlYVvMNJzOplwuvq45D1BLk9_jSKPG_szSSmbveF6zRloB4Nkm_I5VC2kZ126zhiEBn7SELloFGCti7IUuKG80wlcVFQe9upVr4yaCctTOfPs2NyWngCIp55eVGDBNtVqSgZbDoMpvmq4tJmEQ9in0HZkPAEyuN3Zu8IOZMey0jEIDOtds3Lo5xi65t3-1AapY0JyEI08kB-a8pVppl8o6H9DPhQ +n=yab6iEkwcbS0BBGV3gD_M5IW7cEgSZV5yFkASIcA1Ip53nU98yxivhS1ouCQpbyZ1QMv8W4Xol0S9LJfmbAtfxJ3FY4yp1OQQYXA9KMXW4SDkXpZ7mZWR9DWBrX6U-pmGbYtIUDgjlyzAoSu9HKRQjWnV9MEPn3aaDN8wGNON3BbIkCspF-WMJm2FA_-ZqsuGM-0U2WtMTlJjL5a2BqbYWFBRmMvwJ2akqoScIa2RB50kGCPCGO9L-rXcikGTs6yG3mIVmMBFALkGmHXps9JCT33ky4k5QN1aTuCrxzyL9hID5HWrdvhaBAETStH9zWSgvA1-ovGRjxuApiSg9EFtw +e=AQAB diff --git a/tokens/src/test/resources/token-and-pubkey.properties b/tokens/src/test/resources/token-and-pubkey.properties new file mode 100644 index 0000000..36ccc16 --- /dev/null +++ b/tokens/src/test/resources/token-and-pubkey.properties @@ -0,0 +1,4 @@ +kid=c09fef96-6af3-62f7-8c73-a8c4813f1bb5 +token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImMwOWZlZjk2LTZhZjMtNjJmNy04YzczLWE4YzQ4MTNmMWJiNSJ9.eyJhdWQiOiJ0Q0dRRDFkdUJUZjdST2ZucE1NQ0lDU01xRCIsImV4Y2hhbmdlX2lkIjoxLCJleHAiOjE1ODkxMjUyMDUsImlhdCI6MTU4OTAzODgwNSwiaXNzIjoiaHR0cDovLzAuMC4wLjA6ODIwMC92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInBlcm1pc3Npb25zIjpbIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpuZXdfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjp1cGRhdGVfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjphc3NpZ25fY2FsZW5kYXIiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246YXNzaWduX2NiciIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpyZW1vdmVfY2JyIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOnRyYWRpbmdfYWN0aW9uOmhhbHQiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246dHJhZGluZ19hY3Rpb246cmVzdW1lIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOm9wZXJhdGlvbl9ldmVudHMiXSwic3ViIjoiMDY4OTdiNTAtOGQ5Zi0xZDRkLWEwZjUtOGMxNmQ1ZGU4MzIwIn0.gNCXOnjEcq-38oDqWVQcrQ146OfqUAChcQu1vF1UkywZGlVAPGWq1Cvayh2LdbVcCLQK5z-ixGfnAzuvAlTn1rrrUb31bgc39DM8-DLjrngvo37n66OI15PKN0aBptnIfpBzwOIxhyUuFKbkupvfEEdEE6k1ClZ3K-4cCzzG8Ec3A80D8F-ntNPv_5aGECSXr8lccoCF0-k_5YyW2r4NX9klAgIIfIbi8UVAU6ikReh8PxSpn8JtlMt7v9CW4gI4uNZfywzC7BXUWQIvf8R9K6OkeoZ3jCFPsrocqtokOCixND2rp1OVe9_7g-FI4XY5GTg06lp832Gvwa7-C2nb_Q +n=zD_WVTbF_bOnYaoGJcJdcZOVZzJTWIXoU4aY_2orS5mcOLC519oU-Pa-i51O_q2l7JHJmYA4ZisH_NMrAPblDFXYB4OIgec8IvHvuS7kn66EgsoLmZu_Xzs4VM_610WwgPIIo3jpEqgBI8dIZEbGUkWKBwlV2G-uY16ftk_sKD84_2cZTVsnBReC_b8JFdkDvmC6KDTDoQsStehvSnOLcby6acbdnoaka0M5V2pXElReDhNKx-NXF4E26-HedGKmys5a0IknF70-Jp5rK5x9Y3vCVcuh7Eyz6zAMB2ovzTCqHTLHuN0BY6SCgy5zZ-S-C-ytNinv41Qkf80BEDyaww +e=AQAB diff --git a/tokens/src/test/resources/token-and-wrong-pubkey.properties b/tokens/src/test/resources/token-and-wrong-pubkey.properties new file mode 100644 index 0000000..e90d768 --- /dev/null +++ b/tokens/src/test/resources/token-and-wrong-pubkey.properties @@ -0,0 +1,4 @@ +kid=c09fef96-6af3-62f7-8c73-a8c4813f1bb5 +token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImMwOWZlZjk2LTZhZjMtNjJmNy04YzczLWE4YzQ4MTNmMWJiNSJ9.eyJhdWQiOiJ0Q0dRRDFkdUJUZjdST2ZucE1NQ0lDU01xRCIsImV4Y2hhbmdlX2lkIjoxLCJleHAiOjE1ODkxMjUyMDUsImlhdCI6MTU4OTAzODgwNSwiaXNzIjoiaHR0cDovLzAuMC4wLjA6ODIwMC92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInBlcm1pc3Npb25zIjpbIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpuZXdfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjp1cGRhdGVfaW5zdHJ1bWVudCIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjphc3NpZ25fY2FsZW5kYXIiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246YXNzaWduX2NiciIsIm1hcmtldC1zZXJ2aWNlOm9wZXJhdGlvbjpyZW1vdmVfY2JyIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOnRyYWRpbmdfYWN0aW9uOmhhbHQiLCJtYXJrZXQtc2VydmljZTpvcGVyYXRpb246dHJhZGluZ19hY3Rpb246cmVzdW1lIiwibWFya2V0LXNlcnZpY2U6b3BlcmF0aW9uOm9wZXJhdGlvbl9ldmVudHMiXSwic3ViIjoiMDY4OTdiNTAtOGQ5Zi0xZDRkLWEwZjUtOGMxNmQ1ZGU4MzIwIn0.gNCXOnjEcq-38oDqWVQcrQ146OfqUAChcQu1vF1UkywZGlVAPGWq1Cvayh2LdbVcCLQK5z-ixGfnAzuvAlTn1rrrUb31bgc39DM8-DLjrngvo37n66OI15PKN0aBptnIfpBzwOIxhyUuFKbkupvfEEdEE6k1ClZ3K-4cCzzG8Ec3A80D8F-ntNPv_5aGECSXr8lccoCF0-k_5YyW2r4NX9klAgIIfIbi8UVAU6ikReh8PxSpn8JtlMt7v9CW4gI4uNZfywzC7BXUWQIvf8R9K6OkeoZ3jCFPsrocqtokOCixND2rp1OVe9_7g-FI4XY5GTg06lp832Gvwa7-C2nb_Q +n=4aegHcKayVNuDtTR3XPQ5FwXI7MzBMYg4jS_Yden_r0LqaRiPeetTk9sqEhbBVAf8dLTBk1J9LtlQYStNisui0y-arK4lIPWs2wYWtIrZ3UYBoe8EkdDEiWzAVAhSgwA3jIHQrDsfyVjsUTeIgL7hkvIAXf3BIVOHtQqe12vvjHeruOvoea_wk5rftpsFyMDg_40XcgcwPWMhfMI3Uq6kbo-uC1D2WT12k5AJp_qJXRl_qQqWj3swmEgRfeAxrRx8iCWhG59YgIAwf0PUYEV5b8ni4ofnfu3_pLQ7TiDG16BoNBPn_GVcH-zJJtoSups9MY3N2--VmHhREV1Om_Q9Q +e=AQAB From 056c7ff600415809db286432154495657d170a20 Mon Sep 17 00:00:00 2001 From: Artem Vysochyn Date: Thu, 14 May 2020 15:05:35 +0300 Subject: [PATCH 3/3] Minor --- .../jwt/vault/VaultJwksKeyProviderTests.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java b/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java index e3cf069..ee7feb2 100644 --- a/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java +++ b/tokens/src/test/java/com/om2/exchange/tokens/jwt/vault/VaultJwksKeyProviderTests.java @@ -151,14 +151,23 @@ private static void checkSuccess(int exitCode) { } private static String createIdentityKey(String vaultAddr) throws RestException { + return createIdentityKey(vaultAddr, "1m", "1m"); + } + + private static String createIdentityKey( + String vaultAddr, String rotationPeriod, String verificationTtl) throws RestException { String keyName = UUID.randomUUID().toString(); int status = new Rest() .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) .url(oidcKeyUrl(vaultAddr, keyName)) .body( - ("{\"rotation_period\":\"1h\", " - + "\"verification_ttl\": 0, " + ("{\"rotation_period\":\"" + + rotationPeriod + + "\", " + + "\"verification_ttl\": \"" + + verificationTtl + + "\", " + "\"allowed_client_ids\": \"*\", " + "\"algorithm\": \"RS256\"}") .getBytes()) @@ -172,12 +181,17 @@ private static String createIdentityKey(String vaultAddr) throws RestException { } private static String createIdentityRole(String vaultAddr, String keyName) throws RestException { + return createIdentityRole(vaultAddr, keyName, "1h"); + } + + private static String createIdentityRole(String vaultAddr, String keyName, String ttl) + throws RestException { String roleName = UUID.randomUUID().toString(); int status = new Rest() .header(VAULT_TOKEN_HEADER, VAULT_TOKEN) .url(oidcRoleUrl(vaultAddr, roleName)) - .body(("{\"key\":\"" + keyName + "\",\"ttl\": \"1h\"}").getBytes()) + .body(("{\"key\":\"" + keyName + "\",\"ttl\": \"" + ttl + "\"}").getBytes()) .post() .getStatus();