Skip to content

Commit 6bf89e9

Browse files
ARC-1222: Bootstrap Relying Party OIDC Server (#8)
* ARC-1222: Basic working OIDC server * ARC-1222: Test coverage
1 parent bcba63a commit 6bf89e9

31 files changed

+1724
-29
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ target/
33
gesundheitsid/env.properties
44
*.iml
55
gesundheitsid/dependency-reduced-pom.xml
6+
.flattened-pom.xml
67
*_jwks.json
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.oviva.gesundheitsid.util;
2+
3+
import com.fasterxml.jackson.databind.module.SimpleModule;
4+
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer;
5+
import com.nimbusds.jose.jwk.JWK;
6+
import com.nimbusds.jose.jwk.JWKSet;
7+
import com.oviva.gesundheitsid.util.JWKSetDeserializer.JWKDeserializer;
8+
9+
public class JoseModule extends SimpleModule {
10+
11+
public JoseModule() {
12+
super("jose");
13+
addDeserializer(JWK.class, new JWKDeserializer(JWK.class));
14+
addDeserializer(JWKSet.class, new JWKSetDeserializer(JWKSet.class));
15+
addSerializer(new StdDelegatingSerializer(JWKSet.class, new JWKSetConverter()));
16+
}
17+
}

gesundheitsid/src/main/java/com/oviva/gesundheitsid/util/JsonCodec.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@
33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.DeserializationFeature;
55
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import com.fasterxml.jackson.databind.module.SimpleModule;
7-
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer;
8-
import com.nimbusds.jose.jwk.JWK;
9-
import com.nimbusds.jose.jwk.JWKSet;
10-
import com.oviva.gesundheitsid.util.JWKSetDeserializer.JWKDeserializer;
116
import java.io.IOException;
127

138
public class JsonCodec {
@@ -17,11 +12,7 @@ public class JsonCodec {
1712
static {
1813
var om = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
1914

20-
var mod = new SimpleModule("jwks");
21-
mod.addDeserializer(JWK.class, new JWKDeserializer(JWK.class));
22-
mod.addDeserializer(JWKSet.class, new JWKSetDeserializer(JWKSet.class));
23-
mod.addSerializer(new StdDelegatingSerializer(JWKSet.class, new JWKSetConverter()));
24-
om.registerModule(mod);
15+
om.registerModule(new JoseModule());
2516

2617
JsonCodec.om = om;
2718
}

oidc-server/pom.xml

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
<version>0.0.1-SNAPSHOT</version>
1111
</parent>
1212

13-
<artifactId>gesundheitsid</artifactId>
13+
<artifactId>oidc-server</artifactId>
1414
<packaging>jar</packaging>
1515

1616
<dependencies>
17+
<dependency>
18+
<groupId>com.oviva.gesundheitsid</groupId>
19+
<artifactId>gesundheitsid</artifactId>
20+
<version>${project.version}</version>
21+
</dependency>
1722
<dependency>
1823
<groupId>org.slf4j</groupId>
1924
<artifactId>slf4j-api</artifactId>
@@ -42,6 +47,39 @@
4247
<groupId>jakarta.ws.rs</groupId>
4348
<artifactId>jakarta.ws.rs-api</artifactId>
4449
</dependency>
50+
<dependency>
51+
<groupId>org.jboss.resteasy</groupId>
52+
<artifactId>resteasy-core</artifactId>
53+
</dependency>
54+
<dependency>
55+
<groupId>org.jboss.resteasy</groupId>
56+
<artifactId>resteasy-undertow</artifactId>
57+
</dependency>
58+
<dependency>
59+
<groupId>com.fasterxml.jackson.jakarta.rs</groupId>
60+
<artifactId>jackson-jakarta-rs-json-provider</artifactId>
61+
</dependency>
62+
63+
<!-- BEGIN logging-->
64+
<dependency>
65+
<groupId>ch.qos.logback</groupId>
66+
<artifactId>logback-classic</artifactId>
67+
</dependency>
68+
<dependency>
69+
<groupId>ch.qos.logback.contrib</groupId>
70+
<artifactId>logback-json-classic</artifactId>
71+
<exclusions>
72+
<exclusion>
73+
<groupId>ch.qos.logback</groupId>
74+
<artifactId>logback-core</artifactId>
75+
</exclusion>
76+
</exclusions>
77+
</dependency>
78+
<dependency>
79+
<groupId>ch.qos.logback.contrib</groupId>
80+
<artifactId>logback-jackson</artifactId>
81+
</dependency>
82+
<!-- END logging-->
4583

4684
<dependency>
4785
<groupId>org.junit.jupiter</groupId>
@@ -104,12 +142,6 @@
104142
<scope>test</scope>
105143
</dependency>
106144

107-
<dependency>
108-
<groupId>org.jboss.resteasy</groupId>
109-
<artifactId>resteasy-core</artifactId>
110-
<scope>test</scope>
111-
</dependency>
112-
113145
</dependencies>
114146

115147
</project>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.oviva.gesundheitsid.relyingparty;
2+
3+
import com.oviva.gesundheitsid.relyingparty.cfg.Config;
4+
import com.oviva.gesundheitsid.relyingparty.cfg.ConfigProvider;
5+
import com.oviva.gesundheitsid.relyingparty.cfg.EnvConfigProvider;
6+
import com.oviva.gesundheitsid.relyingparty.svc.InMemoryCodeRepo;
7+
import com.oviva.gesundheitsid.relyingparty.svc.InMemorySessionRepo;
8+
import com.oviva.gesundheitsid.relyingparty.svc.KeyStore;
9+
import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuerImpl;
10+
import com.oviva.gesundheitsid.relyingparty.ws.App;
11+
import jakarta.ws.rs.SeBootstrap;
12+
import jakarta.ws.rs.SeBootstrap.Configuration;
13+
import java.net.URI;
14+
import java.util.List;
15+
import java.util.concurrent.ExecutionException;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
18+
19+
public class Main {
20+
21+
private static final Logger logger = LoggerFactory.getLogger(Main.class);
22+
23+
private static final String BANNER =
24+
"""
25+
____ _
26+
/ __ \\_ __(_) _____ _
27+
/ /_/ / |/ / / |/ / _ `/
28+
\\____/|___/_/|___/\\_,_/
29+
GesundheitsID OpenID Connect Relying-Party
30+
""";
31+
32+
public static void main(String[] args) throws ExecutionException, InterruptedException {
33+
34+
var main = new Main();
35+
main.run(new EnvConfigProvider("OIDC_SERVER", System::getenv));
36+
}
37+
38+
public void run(ConfigProvider configProvider) throws ExecutionException, InterruptedException {
39+
logger.atInfo().log("\n" + BANNER);
40+
41+
var baseUri = URI.create("https://t.oviva.io");
42+
var validRedirectUris =
43+
List.of(URI.create("https://idp-test.oviva.io/auth/realms/master/broker/oidc/endpoint"));
44+
45+
var supportedResponseTypes = List.of("code");
46+
47+
var port =
48+
configProvider.get("port").stream().mapToInt(Integer::parseInt).findFirst().orElse(1234);
49+
var config =
50+
new Config(
51+
port,
52+
baseUri, // TOOD: hardcoded :)
53+
// configProvider.get("base_uri").map(URI::create).orElse(URI.create("http://localhost:"
54+
// + port)),
55+
supportedResponseTypes,
56+
validRedirectUris // TODO: hardcoded :)
57+
58+
// configProvider.get("redirect_uris").stream()
59+
// .flatMap(Strings::mustParseCommaList)
60+
// .map(URI::create)
61+
// .toList()
62+
);
63+
64+
var keyStore = new KeyStore();
65+
var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, new InMemoryCodeRepo());
66+
var sessionRepo = new InMemorySessionRepo();
67+
68+
var instance =
69+
SeBootstrap.start(
70+
new App(config, sessionRepo, keyStore, tokenIssuer),
71+
Configuration.builder().host("0.0.0.0").port(config.port()).build())
72+
.toCompletableFuture()
73+
.get();
74+
75+
var localUri = instance.configuration().baseUri();
76+
logger.atInfo().addKeyValue("local_addr", localUri).log("Magic at {}", config.baseUri());
77+
78+
// wait forever
79+
Thread.currentThread().join();
80+
}
81+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.oviva.gesundheitsid.relyingparty.cfg;
2+
3+
import java.net.URI;
4+
import java.util.List;
5+
6+
public record Config(
7+
int port, URI baseUri, List<String> supportedResponseTypes, List<URI> validRedirectUris) {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.oviva.gesundheitsid.relyingparty.cfg;
2+
3+
import java.util.Optional;
4+
5+
public interface ConfigProvider {
6+
Optional<String> get(String name);
7+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.oviva.gesundheitsid.relyingparty.cfg;
2+
3+
import java.util.Locale;
4+
import java.util.Optional;
5+
import java.util.function.Function;
6+
7+
public class EnvConfigProvider implements ConfigProvider {
8+
9+
private final String prefix;
10+
private final Function<String, String> getenv;
11+
12+
public EnvConfigProvider(String prefix, Function<String, String> getenv) {
13+
this.prefix = prefix;
14+
this.getenv = getenv;
15+
}
16+
17+
@Override
18+
public Optional<String> get(String name) {
19+
20+
var mangled = prefix + "_" + name;
21+
mangled = mangled.toUpperCase(Locale.ROOT);
22+
mangled = mangled.replaceAll("[^A-Z0-9]", "_");
23+
24+
return Optional.ofNullable(getenv.apply(mangled));
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.oviva.gesundheitsid.relyingparty.svc;
2+
3+
import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuer.Code;
4+
import java.util.Optional;
5+
6+
public interface CodeRepo {
7+
8+
void save(Code code);
9+
10+
Optional<Code> remove(String code);
11+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.oviva.gesundheitsid.relyingparty.svc;
2+
3+
import com.oviva.gesundheitsid.relyingparty.svc.TokenIssuer.Code;
4+
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import java.util.Optional;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.ConcurrentMap;
8+
9+
public class InMemoryCodeRepo implements CodeRepo {
10+
11+
private final ConcurrentMap<String, Code> store = new ConcurrentHashMap<>();
12+
13+
@Override
14+
public void save(@NonNull Code code) {
15+
store.put(code.code(), code);
16+
}
17+
18+
@NonNull
19+
@Override
20+
public Optional<Code> remove(String code) {
21+
return Optional.ofNullable(store.remove(code));
22+
}
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.oviva.gesundheitsid.relyingparty.svc;
2+
3+
import com.oviva.gesundheitsid.relyingparty.util.IdGenerator;
4+
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import edu.umd.cs.findbugs.annotations.Nullable;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.ConcurrentMap;
8+
9+
public class InMemorySessionRepo implements SessionRepo {
10+
11+
private final ConcurrentMap<String, Session> repo = new ConcurrentHashMap<>();
12+
13+
@Override
14+
public String save(@NonNull Session session) {
15+
if (session.id() != null) {
16+
throw new IllegalStateException(
17+
"session already has an ID=%s, already saved?".formatted(session.id()));
18+
}
19+
20+
var id = IdGenerator.generateID();
21+
session =
22+
new Session(
23+
id, session.state(), session.nonce(), session.redirectUri(), session.clientId());
24+
25+
repo.put(id, session);
26+
27+
return id;
28+
}
29+
30+
@Nullable
31+
@Override
32+
public Session load(@NonNull String sessionId) {
33+
return repo.get(sessionId);
34+
}
35+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.oviva.gesundheitsid.relyingparty.svc;
2+
3+
import com.nimbusds.jose.JOSEException;
4+
import com.nimbusds.jose.jwk.Curve;
5+
import com.nimbusds.jose.jwk.ECKey;
6+
import com.nimbusds.jose.jwk.KeyUse;
7+
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
8+
9+
public class KeyStore {
10+
11+
private final ECKey signingKey;
12+
13+
public KeyStore() {
14+
15+
try {
16+
this.signingKey =
17+
new ECKeyGenerator(Curve.P_256)
18+
.keyIDFromThumbprint(false)
19+
.keyUse(KeyUse.SIGNATURE)
20+
.generate();
21+
} catch (JOSEException e) {
22+
throw new IllegalStateException("failed to generate EC signing key", e);
23+
}
24+
}
25+
26+
public ECKey signingKey() {
27+
return signingKey;
28+
}
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.oviva.gesundheitsid.relyingparty.svc;
2+
3+
import edu.umd.cs.findbugs.annotations.NonNull;
4+
import java.net.URI;
5+
6+
public interface SessionRepo {
7+
8+
String save(@NonNull Session session);
9+
10+
Session load(@NonNull String sessionId);
11+
12+
record Session(String id, String state, String nonce, URI redirectUri, String clientId) {}
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.oviva.gesundheitsid.relyingparty.svc;
2+
3+
import com.oviva.gesundheitsid.relyingparty.svc.SessionRepo.Session;
4+
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import java.net.URI;
6+
import java.time.Instant;
7+
8+
public interface TokenIssuer {
9+
10+
Code issueCode(Session session);
11+
12+
Token redeem(@NonNull String code, String redirectUri, String clientId);
13+
14+
record Code(
15+
String code,
16+
Instant issuedAt,
17+
Instant expiresAt,
18+
URI redirectUri,
19+
String nonce,
20+
String clientId) {}
21+
22+
record Token(String accessToken, String idToken, long expiresInSeconds) {}
23+
}

0 commit comments

Comments
 (0)