Skip to content

Commit 36ecaa3

Browse files
ARC-1246: Improved In-Memory Stores for Codes and Sessions (#17)
* ARC-1246: Added bounded session and code stores. * ARC-1246: Remove session onces flow is done. * ARC-1246: Fix sonar issue * ARC-1246: Check off task!
1 parent f0bc320 commit 36ecaa3

19 files changed

+693
-194
lines changed

README.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ In order of priority:
66
- [ ] Health endpoint - sanity check whether Jakarta ws is up should be enough. I.e. `/health`
77
- [ ] Dockerfile + CI/CD
88
- [ ] Helm chart (externally)
9-
- [ ] Better in-memory stores. Should have some expiry and size limits.
10-
- com.oviva.ehealthid.relyingparty.svc.SessionRepo
11-
- com.oviva.ehealthid.relyingparty.svc.CodeRepo
129
- [ ] Internationalization (ResourceBundles) for templates (en & de)
1310
- see [Mustache Library](https://github.com/spullara/mustache.java/blob/main/compiler/src/main/java/com/github/mustachejava/functions/BundleFunctions.java)
1411
- [ ] Metrics endpoint
@@ -93,18 +90,22 @@ The discovery document can be used to configure the relying party in an existing
9390
9491
Use environment variables to configure the relying party server.
9592
96-
| Name | Description | Example |
97-
|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------|
98-
| `EHEALTHID_RP_FEDERATION_ENC_JWKS_PATH` | Path to a JWKS with at least one keypair for encryption of ID tokens. | `./enc_jwks.json` |
99-
| `EHEALTHID_RP_FEDERATION_SIG_JWKS_PATH` | Path to a JWKS with at least one keypair for signature withing the federation. All these keys __MUST__ be registered with the federation master. | `./sig_jwks.json` |
100-
| `EHEALTHID_RP_REDIRECT_URIS` | Valid redirection URIs for OpenID connect. | `https://sso-mydiga.example.com/auth/callback` |
101-
| `EHEALTHID_RP_BASE_URI` | The external base URI of the relying party. This is also the `issuer` towards the OpenID federation. Additional paths are unsupported for now. | `https://mydiga-rp.example.com` |
102-
| `EHEALTHID_RP_HOST` | Host to bind to. | `0.0.0.0` |
103-
| `EHEALTHID_RP_PORT` | Port to bind to. | `1234` |
104-
| `EHEALTHID_RP_FEDERATION_MASTER` | The URI of the federation master. | `https://app-test.federationmaster.de` |
105-
| `EHEALTHID_RP_APP_NAME` | The application name within the federation. | `Awesome DiGA` |
106-
| `EHEALTHID_RP_ES_TTL` | The time to live for the entity statement. In ISO8601 format. | `PT12H` |
107-
| `EHEALTHID_RP_SCOPES` | The comma separated list of scopes requested in the federation. This __MUST__ match what was registered with the federation master. | `openid,urn:telematik:email,urn:telematik:display_name` |
93+
| Name | Description | Example |
94+
|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------|
95+
| `EHEALTHID_RP_FEDERATION_ENC_JWKS_PATH` | Path to a JWKS with at least one keypair for encryption of ID tokens. | `./enc_jwks.json` |
96+
| `EHEALTHID_RP_FEDERATION_SIG_JWKS_PATH` | Path to a JWKS with at least one keypair for signature withing the federation. All these keys __MUST__ be registered with the federation master. | `./sig_jwks.json` |
97+
| `EHEALTHID_RP_REDIRECT_URIS` | Valid redirection URIs for OpenID connect. | `https://sso-mydiga.example.com/auth/callback` |
98+
| `EHEALTHID_RP_BASE_URI` | The external base URI of the relying party. This is also the `issuer` towards the OpenID federation. Additional paths are unsupported for now. | `https://mydiga-rp.example.com` |
99+
| `EHEALTHID_RP_HOST` | Host to bind to. | `0.0.0.0` |
100+
| `EHEALTHID_RP_PORT` | Port to bind to. | `1234` |
101+
| `EHEALTHID_RP_FEDERATION_MASTER` | The URI of the federation master. | `https://app-test.federationmaster.de` |
102+
| `EHEALTHID_RP_APP_NAME` | The application name within the federation. | `Awesome DiGA` |
103+
| `EHEALTHID_RP_ES_TTL` | The time to live for the entity statement. In ISO8601 format. | `PT12H` |
104+
| `EHEALTHID_RP_SCOPES` | The comma separated list of scopes requested in the federation. This __MUST__ match what was registered with the federation master. | `openid,urn:telematik:email,urn:telematik:display_name` |
105+
| `EHEALTHID_RP_SESSION_STORE_TTL` | The time to live for sessions. In ISO8601 format. | `PT20M` |
106+
| `EHEALTHID_RP_SESSION_STORE_MAX_ENTRIES` | The maximum number of sessions to store. Keeps memory bounded. | `1000` |
107+
| `EHEALTHID_RP_CODE_STORE_TTL` | The time to live for codes, i.e. successful logins where the code is not redeemed yet. In ISO8601 format. | `PT5M` |
108+
| `EHEALTHID_RP_CODE_STORE_MAX_ENTRIES` | The maximum number of codes to store. Keeps memory bounded. | `1000` |
108109
109110
# Generate Keys & Register for Federation
110111

ehealthid-rp/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
<groupId>com.nimbusds</groupId>
3737
<artifactId>nimbus-jose-jwt</artifactId>
3838
</dependency>
39+
<dependency>
40+
<groupId>com.github.ben-manes.caffeine</groupId>
41+
<artifactId>caffeine</artifactId>
42+
</dependency>
43+
44+
<!-- BEGIN jackson -->
3945
<dependency>
4046
<groupId>com.fasterxml.jackson.core</groupId>
4147
<artifactId>jackson-core</artifactId>
@@ -48,6 +54,7 @@
4854
<groupId>com.fasterxml.jackson.core</groupId>
4955
<artifactId>jackson-databind</artifactId>
5056
</dependency>
57+
<!-- END jackson -->
5158

5259
<!-- BEGIN jakarta ws -->
5360
<dependency>
@@ -76,7 +83,6 @@
7683
<dependency>
7784
<groupId>net.logstash.logback</groupId>
7885
<artifactId>logstash-logback-encoder</artifactId>
79-
<version>7.4</version>
8086
</dependency>
8187
<!-- END logging-->
8288

ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ConfigReader.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public class ConfigReader {
2525
public static final String CONFIG_APP_NAME = "app_name";
2626
public static final String CONFIG_SCOPES = "scopes";
2727

28+
public static final String CONFIG_SESSION_STORE_TTL = "session_store_ttl";
29+
public static final String CONFIG_SESSION_STORE_MAX_ENTRIES = "session_store_max_entries";
30+
31+
public static final String CONFIG_CODE_STORE_TTL = "code_store_ttl";
32+
public static final String CONFIG_CODE_STORE_MAX_ENTRIES = "code_store_max_entries";
33+
2834
private final ConfigProvider configProvider;
2935

3036
public ConfigReader(ConfigProvider configProvider) {
@@ -78,7 +84,26 @@ public Config read() {
7884
var relyingPartyConfig =
7985
new RelyingPartyConfig(supportedResponseTypes, loadAllowedRedirectUrls());
8086

81-
return new Config(relyingPartyConfig, federationConfig, host, port, baseUri);
87+
return new Config(
88+
relyingPartyConfig,
89+
federationConfig,
90+
host,
91+
port,
92+
baseUri,
93+
sessionStoreConfig(),
94+
codeStoreConfig());
95+
}
96+
97+
private SessionStoreConfig sessionStoreConfig() {
98+
var ttl = getDurationOrDefault(CONFIG_SESSION_STORE_TTL, Duration.ofMinutes(20));
99+
var maxEntries = getIntOrDefault(CONFIG_SESSION_STORE_MAX_ENTRIES, 1000);
100+
return new SessionStoreConfig(ttl, maxEntries);
101+
}
102+
103+
private CodeStoreConfig codeStoreConfig() {
104+
var ttl = getDurationOrDefault(CONFIG_CODE_STORE_TTL, Duration.ofMinutes(5));
105+
var maxEntries = getIntOrDefault(CONFIG_CODE_STORE_MAX_ENTRIES, 1000);
106+
return new CodeStoreConfig(ttl, maxEntries);
82107
}
83108

84109
private List<URI> loadAllowedRedirectUrls() {
@@ -88,6 +113,14 @@ private List<URI> loadAllowedRedirectUrls() {
88113
.toList();
89114
}
90115

116+
private Duration getDurationOrDefault(String config, Duration defaultValue) {
117+
return configProvider.get(config).map(Duration::parse).orElse(defaultValue);
118+
}
119+
120+
private int getIntOrDefault(String config, int defaultValue) {
121+
return configProvider.get(config).map(Integer::parseInt).orElse(defaultValue);
122+
}
123+
91124
private List<String> getScopes() {
92125

93126
return configProvider
@@ -127,5 +160,11 @@ public record Config(
127160
FederationConfig federation,
128161
String host,
129162
int port,
130-
URI baseUri) {}
163+
URI baseUri,
164+
SessionStoreConfig sessionStore,
165+
CodeStoreConfig codeStoreConfig) {}
166+
167+
public record SessionStoreConfig(Duration ttl, int maxEntries) {}
168+
169+
public record CodeStoreConfig(Duration ttl, int maxEntries) {}
131170
}

ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.oviva.ehealthid.relyingparty;
22

3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import com.github.benmanes.caffeine.cache.Caffeine;
35
import com.nimbusds.jose.jwk.JWKSet;
46
import com.oviva.ehealthid.auth.AuthenticationFlow;
57
import com.oviva.ehealthid.fedclient.FederationMasterClientImpl;
@@ -8,12 +10,19 @@
810
import com.oviva.ehealthid.fedclient.api.InMemoryCacheImpl;
911
import com.oviva.ehealthid.fedclient.api.JavaHttpClient;
1012
import com.oviva.ehealthid.fedclient.api.OpenIdClient;
13+
import com.oviva.ehealthid.relyingparty.ConfigReader.CodeStoreConfig;
14+
import com.oviva.ehealthid.relyingparty.ConfigReader.SessionStoreConfig;
1115
import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider;
1216
import com.oviva.ehealthid.relyingparty.cfg.EnvConfigProvider;
1317
import com.oviva.ehealthid.relyingparty.poc.GematikHeaderDecoratorHttpClient;
14-
import com.oviva.ehealthid.relyingparty.svc.InMemoryCodeRepo;
15-
import com.oviva.ehealthid.relyingparty.svc.InMemorySessionRepo;
18+
import com.oviva.ehealthid.relyingparty.svc.AfterCreatedExpiry;
19+
import com.oviva.ehealthid.relyingparty.svc.CaffeineCodeRepo;
20+
import com.oviva.ehealthid.relyingparty.svc.CaffeineSessionRepo;
21+
import com.oviva.ehealthid.relyingparty.svc.CodeRepo;
1622
import com.oviva.ehealthid.relyingparty.svc.KeyStore;
23+
import com.oviva.ehealthid.relyingparty.svc.SessionRepo;
24+
import com.oviva.ehealthid.relyingparty.svc.SessionRepo.Session;
25+
import com.oviva.ehealthid.relyingparty.svc.TokenIssuer.Code;
1726
import com.oviva.ehealthid.relyingparty.svc.TokenIssuerImpl;
1827
import com.oviva.ehealthid.relyingparty.ws.App;
1928
import jakarta.ws.rs.SeBootstrap;
@@ -63,8 +72,9 @@ public void run() throws ExecutionException, InterruptedException {
6372
var config = configReader.read();
6473

6574
var keyStore = new KeyStore();
66-
var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, new InMemoryCodeRepo());
67-
var sessionRepo = new InMemorySessionRepo();
75+
var codeRepo = buildCodeRepo(config.codeStoreConfig());
76+
var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, codeRepo);
77+
var sessionRepo = buildSessionRepo(config.sessionStore());
6878

6979
var authFlow =
7080
buildAuthFlow(
@@ -110,4 +120,21 @@ private AuthenticationFlow buildAuthFlow(URI selfIssuer, URI fedmaster, JWKSet e
110120
return new AuthenticationFlow(
111121
selfIssuer, fedmasterClient, openIdClient, encJwks::getKeyByKeyId);
112122
}
123+
124+
private SessionRepo buildSessionRepo(SessionStoreConfig config) {
125+
Cache<String, Session> store = buildCache(config.ttl(), config.maxEntries());
126+
return new CaffeineSessionRepo(store, config.ttl());
127+
}
128+
129+
private CodeRepo buildCodeRepo(CodeStoreConfig config) {
130+
Cache<String, Code> store = buildCache(config.ttl(), config.maxEntries());
131+
return new CaffeineCodeRepo(store);
132+
}
133+
134+
private <T> Cache<String, T> buildCache(Duration ttl, int maxSize) {
135+
return Caffeine.newBuilder()
136+
.expireAfter(new AfterCreatedExpiry<>(ttl.toNanos()))
137+
.maximumSize(maxSize)
138+
.build();
139+
}
113140
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.oviva.ehealthid.relyingparty.svc;
2+
3+
import com.github.benmanes.caffeine.cache.Expiry;
4+
import org.checkerframework.checker.index.qual.NonNegative;
5+
6+
public record AfterCreatedExpiry<T>(long timeToLiveNanos) implements Expiry<String, T> {
7+
8+
@Override
9+
public long expireAfterCreate(String key, T value, long currentTime) {
10+
return timeToLiveNanos;
11+
}
12+
13+
@Override
14+
public long expireAfterUpdate(
15+
String key, T value, long currentTime, @NonNegative long currentDuration) {
16+
return timeToLiveNanos - currentDuration;
17+
}
18+
19+
@Override
20+
public long expireAfterRead(
21+
String key, T value, long currentTime, @NonNegative long currentDuration) {
22+
return timeToLiveNanos - currentDuration;
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.oviva.ehealthid.relyingparty.svc;
2+
3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import com.oviva.ehealthid.relyingparty.svc.TokenIssuer.Code;
5+
import java.util.Optional;
6+
7+
public class CaffeineCodeRepo implements CodeRepo {
8+
9+
private final Cache<String, Code> store;
10+
11+
public CaffeineCodeRepo(Cache<String, Code> store) {
12+
this.store = store;
13+
}
14+
15+
@Override
16+
public void save(Code code) {
17+
store.put(code.code(), code);
18+
}
19+
20+
@Override
21+
public Optional<Code> remove(String code) {
22+
return Optional.ofNullable(store.asMap().remove(code));
23+
}
24+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.oviva.ehealthid.relyingparty.svc;
2+
3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import edu.umd.cs.findbugs.annotations.Nullable;
6+
import java.time.Duration;
7+
import java.time.Instant;
8+
9+
public class CaffeineSessionRepo implements SessionRepo {
10+
11+
private final Cache<String, Session> store;
12+
private final Duration timeToLive;
13+
14+
public CaffeineSessionRepo(Cache<String, Session> cache, Duration timeToLive) {
15+
this.store = cache;
16+
this.timeToLive = timeToLive;
17+
}
18+
19+
@Override
20+
public void save(@NonNull Session session) {
21+
if (session.id() == null) {
22+
throw new IllegalArgumentException("session has no ID");
23+
}
24+
25+
store.put(session.id(), session);
26+
}
27+
28+
@Nullable
29+
@Override
30+
public Session load(@NonNull String sessionId) {
31+
var session = store.getIfPresent(sessionId);
32+
if (session == null || session.createdAt().plus(timeToLive).isBefore(Instant.now())) {
33+
return null;
34+
}
35+
return session;
36+
}
37+
38+
@Nullable
39+
@Override
40+
public Session remove(@NonNull String sessionId) {
41+
var session = store.asMap().remove(sessionId);
42+
if (session == null || session.createdAt().plus(timeToLive).isBefore(Instant.now())) {
43+
return null;
44+
}
45+
return session;
46+
}
47+
}

ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/svc/InMemoryCodeRepo.java

Lines changed: 0 additions & 23 deletions
This file was deleted.

ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/svc/InMemorySessionRepo.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)