Skip to content

Commit 610118e

Browse files
ARC-1222: Basic working OIDC server
1 parent bcba63a commit 610118e

28 files changed

+1131
-23
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
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>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.Arrays;
15+
import java.util.List;
16+
import java.util.Objects;
17+
import java.util.concurrent.ExecutionException;
18+
import java.util.stream.Stream;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
public class Main {
23+
24+
private static final Logger logger = LoggerFactory.getLogger(Main.class);
25+
26+
private static final String BANNER =
27+
"""
28+
____ _
29+
/ __ \\_ __(_) _____ _
30+
/ /_/ / |/ / / |/ / _ `/
31+
\\____/|___/_/|___/\\_,_/
32+
GesundheitsID OpenID Connect Relying-Party
33+
""";
34+
35+
public static void main(String[] args) throws ExecutionException, InterruptedException {
36+
37+
var main = new Main();
38+
main.run(new EnvConfigProvider("OIDC_SERVER", System::getenv));
39+
}
40+
41+
private void run(ConfigProvider configProvider) throws ExecutionException, InterruptedException {
42+
logger.atInfo().log("\n" + BANNER);
43+
44+
var baseUri = URI.create("https://t.oviva.io");
45+
var validRedirectUris =
46+
List.of(URI.create("https://idp-test.oviva.io/auth/realms/master/broker/oidc/endpoint"));
47+
48+
var supportedResponseTypes = List.of("code");
49+
50+
var port =
51+
configProvider.get("port").stream().mapToInt(Integer::parseInt).findFirst().orElse(1234);
52+
var config =
53+
new Config(
54+
port,
55+
baseUri, // TOOD: hardcoded :)
56+
// configProvider.get("base_uri").map(URI::create).orElse(URI.create("http://localhost:"
57+
// + port)),
58+
supportedResponseTypes,
59+
validRedirectUris // TODO: hardcoded :)
60+
// configProvider.get("redirect_uris").stream()
61+
// .flatMap(this::mustParseCommaList)
62+
// .map(URI::create)
63+
// .toList()
64+
);
65+
66+
var keyStore = new KeyStore();
67+
var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, new InMemoryCodeRepo());
68+
var sessionRepo = new InMemorySessionRepo();
69+
70+
var instance =
71+
SeBootstrap.start(
72+
new App(config, sessionRepo, keyStore, tokenIssuer),
73+
Configuration.builder().host("0.0.0.0").port(config.port()).build())
74+
.toCompletableFuture()
75+
.get();
76+
77+
var localUri = instance.configuration().baseUri();
78+
logger.atInfo().addKeyValue("local_addr", localUri).log("Magic at {}", config.baseUri());
79+
80+
// wait forever
81+
Thread.currentThread().join();
82+
}
83+
84+
private Stream<String> mustParseCommaList(String value) {
85+
if (value == null || value.isBlank()) {
86+
return Stream.empty();
87+
}
88+
89+
return Arrays.stream(value.split(",")).map(this::trimmed).filter(Objects::nonNull);
90+
}
91+
92+
private String trimmed(String value) {
93+
if (value == null || value.isBlank()) {
94+
return null;
95+
}
96+
97+
return value.trim();
98+
}
99+
}
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);
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)