diff --git a/.gitignore b/.gitignore
index 747efea..7fce934 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ target/
gesundheitsid/env.properties
*.iml
gesundheitsid/dependency-reduced-pom.xml
+.flattened-pom.xml
*_jwks.json
diff --git a/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JoseModule.java b/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JoseModule.java
new file mode 100644
index 0000000..4c5d895
--- /dev/null
+++ b/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JoseModule.java
@@ -0,0 +1,17 @@
+package com.oviva.gesundheitsid.util;
+
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.oviva.gesundheitsid.util.JWKSetDeserializer.JWKDeserializer;
+
+public class JoseModule extends SimpleModule {
+
+ public JoseModule() {
+ super("jose");
+ addDeserializer(JWK.class, new JWKDeserializer(JWK.class));
+ addDeserializer(JWKSet.class, new JWKSetDeserializer(JWKSet.class));
+ addSerializer(new StdDelegatingSerializer(JWKSet.class, new JWKSetConverter()));
+ }
+}
diff --git a/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JsonCodec.java b/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JsonCodec.java
index 1065818..1cc387f 100644
--- a/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JsonCodec.java
+++ b/gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JsonCodec.java
@@ -3,11 +3,6 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer;
-import com.nimbusds.jose.jwk.JWK;
-import com.nimbusds.jose.jwk.JWKSet;
-import com.oviva.gesundheitsid.util.JWKSetDeserializer.JWKDeserializer;
import java.io.IOException;
public class JsonCodec {
@@ -17,11 +12,7 @@ public class JsonCodec {
static {
var om = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
- var mod = new SimpleModule("jwks");
- mod.addDeserializer(JWK.class, new JWKDeserializer(JWK.class));
- mod.addDeserializer(JWKSet.class, new JWKSetDeserializer(JWKSet.class));
- mod.addSerializer(new StdDelegatingSerializer(JWKSet.class, new JWKSetConverter()));
- om.registerModule(mod);
+ om.registerModule(new JoseModule());
JsonCodec.om = om;
}
diff --git a/oidc-server/pom.xml b/oidc-server/pom.xml
index c03e0f4..8bf6581 100644
--- a/oidc-server/pom.xml
+++ b/oidc-server/pom.xml
@@ -10,10 +10,15 @@
0.0.1-SNAPSHOT
- gesundheitsid
+ oidc-server
jar
+
+ com.oviva.gesundheitsid
+ gesundheitsid
+ ${project.version}
+
org.slf4j
slf4j-api
@@ -42,6 +47,39 @@
jakarta.ws.rs
jakarta.ws.rs-api
+
+ org.jboss.resteasy
+ resteasy-core
+
+
+ org.jboss.resteasy
+ resteasy-undertow
+
+
+ org.jboss.resteasy
+ resteasy-jackson2-provider
+
+
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ ch.qos.logback.contrib
+ logback-json-classic
+
+
+ ch.qos.logback
+ logback-core
+
+
+
+
+ ch.qos.logback.contrib
+ logback-jackson
+
+
org.junit.jupiter
@@ -104,12 +142,6 @@
test
-
- org.jboss.resteasy
- resteasy-core
- test
-
-
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/Main.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/Main.java
new file mode 100644
index 0000000..21a1e16
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/Main.java
@@ -0,0 +1,99 @@
+package com.oviva.gesundheitsid.relyingparty;
+
+import com.oviva.gesundheitsid.relyingparty.cfg.Config;
+import com.oviva.gesundheitsid.relyingparty.cfg.ConfigProvider;
+import com.oviva.gesundheitsid.relyingparty.cfg.EnvConfigProvider;
+import com.oviva.gesundheitsid.relyingparty.svc.InMemoryCodeRepo;
+import com.oviva.gesundheitsid.relyingparty.svc.InMemorySessionRepo;
+import com.oviva.gesundheitsid.relyingparty.svc.KeyStore;
+import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuerImpl;
+import com.oviva.gesundheitsid.relyingparty.ws.App;
+import jakarta.ws.rs.SeBootstrap;
+import jakarta.ws.rs.SeBootstrap.Configuration;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Stream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Main {
+
+ private static final Logger logger = LoggerFactory.getLogger(Main.class);
+
+ private static final String BANNER =
+ """
+ ____ _
+ / __ \\_ __(_) _____ _
+ / /_/ / |/ / / |/ / _ `/
+ \\____/|___/_/|___/\\_,_/
+ GesundheitsID OpenID Connect Relying-Party
+ """;
+
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+
+ var main = new Main();
+ main.run(new EnvConfigProvider("OIDC_SERVER", System::getenv));
+ }
+
+ private void run(ConfigProvider configProvider) throws ExecutionException, InterruptedException {
+ logger.atInfo().log("\n" + BANNER);
+
+ var baseUri = URI.create("https://t.oviva.io");
+ var validRedirectUris =
+ List.of(URI.create("https://idp-test.oviva.io/auth/realms/master/broker/oidc/endpoint"));
+
+ var supportedResponseTypes = List.of("code");
+
+ var port =
+ configProvider.get("port").stream().mapToInt(Integer::parseInt).findFirst().orElse(1234);
+ var config =
+ new Config(
+ port,
+ baseUri, // TOOD: hardcoded :)
+ // configProvider.get("base_uri").map(URI::create).orElse(URI.create("http://localhost:"
+ // + port)),
+ supportedResponseTypes,
+ validRedirectUris // TODO: hardcoded :)
+ // configProvider.get("redirect_uris").stream()
+ // .flatMap(this::mustParseCommaList)
+ // .map(URI::create)
+ // .toList()
+ );
+
+ var keyStore = new KeyStore();
+ var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, new InMemoryCodeRepo());
+ var sessionRepo = new InMemorySessionRepo();
+
+ var instance =
+ SeBootstrap.start(
+ new App(config, sessionRepo, keyStore, tokenIssuer),
+ Configuration.builder().host("0.0.0.0").port(config.port()).build())
+ .toCompletableFuture()
+ .get();
+
+ var localUri = instance.configuration().baseUri();
+ logger.atInfo().addKeyValue("local_addr", localUri).log("Magic at {}", config.baseUri());
+
+ // wait forever
+ Thread.currentThread().join();
+ }
+
+ private Stream mustParseCommaList(String value) {
+ if (value == null || value.isBlank()) {
+ return Stream.empty();
+ }
+
+ return Arrays.stream(value.split(",")).map(this::trimmed).filter(Objects::nonNull);
+ }
+
+ private String trimmed(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+
+ return value.trim();
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/Config.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/Config.java
new file mode 100644
index 0000000..d176a76
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/Config.java
@@ -0,0 +1,7 @@
+package com.oviva.gesundheitsid.relyingparty.cfg;
+
+import java.net.URI;
+import java.util.List;
+
+public record Config(
+ int port, URI baseUri, List supportedResponseTypes, List validRedirectUris) {}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/ConfigProvider.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/ConfigProvider.java
new file mode 100644
index 0000000..a6ca670
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/ConfigProvider.java
@@ -0,0 +1,7 @@
+package com.oviva.gesundheitsid.relyingparty.cfg;
+
+import java.util.Optional;
+
+public interface ConfigProvider {
+ Optional get(String name);
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/EnvConfigProvider.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/EnvConfigProvider.java
new file mode 100644
index 0000000..53f59b0
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/cfg/EnvConfigProvider.java
@@ -0,0 +1,26 @@
+package com.oviva.gesundheitsid.relyingparty.cfg;
+
+import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Function;
+
+public class EnvConfigProvider implements ConfigProvider {
+
+ private final String prefix;
+ private final Function getenv;
+
+ public EnvConfigProvider(String prefix, Function getenv) {
+ this.prefix = prefix;
+ this.getenv = getenv;
+ }
+
+ @Override
+ public Optional get(String name) {
+
+ var mangled = prefix + "_" + name;
+ mangled = mangled.toUpperCase(Locale.ROOT);
+ mangled = mangled.replaceAll("[^A-Z0-9]", "_");
+
+ return Optional.ofNullable(getenv.apply(mangled));
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/CodeRepo.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/CodeRepo.java
new file mode 100644
index 0000000..699073b
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/CodeRepo.java
@@ -0,0 +1,11 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuer.Code;
+import java.util.Optional;
+
+public interface CodeRepo {
+
+ void save(Code code);
+
+ Optional remove(String code);
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/InMemoryCodeRepo.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/InMemoryCodeRepo.java
new file mode 100644
index 0000000..4045d57
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/InMemoryCodeRepo.java
@@ -0,0 +1,23 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuer.Code;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class InMemoryCodeRepo implements CodeRepo {
+
+ private final ConcurrentMap store = new ConcurrentHashMap<>();
+
+ @Override
+ public void save(@NonNull Code code) {
+ store.put(code.code(), code);
+ }
+
+ @NonNull
+ @Override
+ public Optional remove(String code) {
+ return Optional.ofNullable(store.remove(code));
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/InMemorySessionRepo.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/InMemorySessionRepo.java
new file mode 100644
index 0000000..36eddc4
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/InMemorySessionRepo.java
@@ -0,0 +1,35 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import com.oviva.gesundheitsid.relyingparty.util.IdGenerator;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class InMemorySessionRepo implements SessionRepo {
+
+ private final ConcurrentMap repo = new ConcurrentHashMap<>();
+
+ @Override
+ public String save(@NonNull Session session) {
+ if (session.id() != null) {
+ throw new IllegalStateException(
+ "session already has an ID=%s, already saved?".formatted(session.id()));
+ }
+
+ var id = IdGenerator.generateID();
+ session =
+ new Session(
+ id, session.state(), session.nonce(), session.redirectUri(), session.clientId());
+
+ repo.put(id, session);
+
+ return id;
+ }
+
+ @Nullable
+ @Override
+ public Session load(@NonNull String sessionId) {
+ return repo.get(sessionId);
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/KeyStore.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/KeyStore.java
new file mode 100644
index 0000000..191a362
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/KeyStore.java
@@ -0,0 +1,29 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+
+public class KeyStore {
+
+ private final ECKey signingKey;
+
+ public KeyStore() {
+
+ try {
+ this.signingKey =
+ new ECKeyGenerator(Curve.P_256)
+ .keyIDFromThumbprint(false)
+ .keyUse(KeyUse.SIGNATURE)
+ .generate();
+ } catch (JOSEException e) {
+ throw new IllegalStateException("failed to generate EC signing key", e);
+ }
+ }
+
+ public ECKey signingKey() {
+ return signingKey;
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/SessionRepo.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/SessionRepo.java
new file mode 100644
index 0000000..d495f92
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/SessionRepo.java
@@ -0,0 +1,13 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.net.URI;
+
+public interface SessionRepo {
+
+ String save(@NonNull Session session);
+
+ Session load(@NonNull String sessionId);
+
+ record Session(String id, String state, String nonce, URI redirectUri, String clientId) {}
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/TokenIssuer.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/TokenIssuer.java
new file mode 100644
index 0000000..2ab63b2
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/TokenIssuer.java
@@ -0,0 +1,23 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import com.oviva.gesundheitsid.relyingparty.svc.SessionRepo.Session;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.net.URI;
+import java.time.Instant;
+
+public interface TokenIssuer {
+
+ Code issueCode(Session session);
+
+ Token redeem(@NonNull String code);
+
+ record Code(
+ String code,
+ Instant issuedAt,
+ Instant expiresAt,
+ URI redirectUri,
+ String nonce,
+ String clientId) {}
+
+ record Token(String accessToken, String idToken, long expiresInSeconds) {}
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/TokenIssuerImpl.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/TokenIssuerImpl.java
new file mode 100644
index 0000000..aa58491
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/svc/TokenIssuerImpl.java
@@ -0,0 +1,128 @@
+package com.oviva.gesundheitsid.relyingparty.svc;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.oviva.gesundheitsid.relyingparty.svc.SessionRepo.Session;
+import com.oviva.gesundheitsid.relyingparty.util.IdGenerator;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.net.URI;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Date;
+import java.util.UUID;
+
+public class TokenIssuerImpl implements TokenIssuer {
+
+ private final URI issuer;
+
+ private final KeyStore keyStore;
+
+ private final Duration TTL = Duration.ofSeconds(60);
+ private final Clock clock = Clock.systemUTC();
+
+ private final CodeRepo codeRepo;
+
+ public TokenIssuerImpl(URI issuer, KeyStore keyStore, CodeRepo codeRepo) {
+ this.issuer = issuer;
+ this.keyStore = keyStore;
+ this.codeRepo = codeRepo;
+ }
+
+ @Override
+ public Code issueCode(Session session) {
+ var code = IdGenerator.generateID();
+ var value =
+ new Code(
+ code,
+ clock.instant(),
+ clock.instant().plus(TTL),
+ session.redirectUri(),
+ session.nonce(),
+ session.clientId());
+ codeRepo.save(value);
+ return value;
+ }
+
+ @Override
+ public Token redeem(@NonNull String code) {
+ var redeemed = codeRepo.remove(code).orElse(null);
+ if (redeemed == null) {
+ return null;
+ }
+
+ if (redeemed.expiresAt().isBefore(clock.instant())) {
+ return null;
+ }
+
+ var accessTokenTtl = Duration.ofMinutes(5);
+ return new Token(
+ issueAccessToken(accessTokenTtl, redeemed.clientId()),
+ issueIdToken(redeemed.clientId(), redeemed.nonce()),
+ accessTokenTtl.getSeconds());
+ }
+
+ private String issueIdToken(String audience, String nonce) {
+ try {
+ var jwk = keyStore.signingKey();
+ var signer = new ECDSASigner(jwk);
+
+ // Prepare JWT with claims set
+ var now = clock.instant();
+ var claimsBuilder =
+ new JWTClaimsSet.Builder()
+ .issuer(issuer.toString())
+ .audience(audience)
+ .subject(UUID.randomUUID().toString())
+ .issueTime(Date.from(now))
+ .expirationTime(Date.from(now.plus(Duration.ofHours(8))));
+
+ if (nonce != null) {
+ claimsBuilder.claim("nonce", nonce);
+ }
+
+ var claims = claimsBuilder.build();
+
+ var signedJWT =
+ new SignedJWT(
+ new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(jwk.getKeyID()).build(), claims);
+
+ signedJWT.sign(signer);
+
+ return signedJWT.serialize();
+ } catch (JOSEException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String issueAccessToken(Duration ttl, String audience) {
+ try {
+ var jwk = keyStore.signingKey();
+ var signer = new ECDSASigner(jwk);
+
+ // Prepare JWT with claims set
+ var now = clock.instant();
+ var claims =
+ new JWTClaimsSet.Builder()
+ .issuer(issuer.toString())
+ .audience(audience)
+ .subject(UUID.randomUUID().toString())
+ .issueTime(Date.from(now))
+ .expirationTime(Date.from(now.plus(ttl)))
+ .build();
+
+ var signedJWT =
+ new SignedJWT(
+ new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(jwk.getKeyID()).build(), claims);
+
+ signedJWT.sign(signer);
+
+ return signedJWT.serialize();
+ } catch (JOSEException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/util/IdGenerator.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/util/IdGenerator.java
new file mode 100644
index 0000000..f8d7543
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/util/IdGenerator.java
@@ -0,0 +1,19 @@
+package com.oviva.gesundheitsid.relyingparty.util;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Base64.Encoder;
+
+public class IdGenerator {
+
+ private static final SecureRandom sr = new SecureRandom();
+ private static final Encoder encoder = Base64.getUrlEncoder().withoutPadding();
+
+ private IdGenerator() {}
+
+ public static String generateID() {
+ var raw = new byte[32];
+ sr.nextBytes(raw);
+ return encoder.encodeToString(raw);
+ }
+}
diff --git a/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/ws/App.java b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/ws/App.java
new file mode 100644
index 0000000..9905a40
--- /dev/null
+++ b/oidc-server/src/main/java/com/oviva/gesundheitsid/relyingparty/ws/App.java
@@ -0,0 +1,42 @@
+package com.oviva.gesundheitsid.relyingparty.ws;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider;
+import com.oviva.gesundheitsid.relyingparty.cfg.Config;
+import com.oviva.gesundheitsid.relyingparty.svc.KeyStore;
+import com.oviva.gesundheitsid.relyingparty.svc.SessionRepo;
+import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuer;
+import com.oviva.gesundheitsid.util.JoseModule;
+import jakarta.ws.rs.core.Application;
+import java.util.Set;
+
+public class App extends Application {
+
+ private final Config config;
+ private final SessionRepo sessionRepo;
+
+ private final KeyStore keyStore;
+ private final TokenIssuer tokenIssuer;
+
+ public App(Config config, SessionRepo sessionRepo, KeyStore keyStore, TokenIssuer tokenIssuer) {
+ this.config = config;
+ this.sessionRepo = sessionRepo;
+ this.keyStore = keyStore;
+ this.tokenIssuer = tokenIssuer;
+ }
+
+ @Override
+ public Set