Skip to content

Commit

Permalink
ARC-1222: Basic working OIDC server
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed Feb 2, 2024
1 parent bcba63a commit ebb24fc
Show file tree
Hide file tree
Showing 29 changed files with 1,174 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ target/
gesundheitsid/env.properties
*.iml
gesundheitsid/dependency-reduced-pom.xml
.flattened-pom.xml
*_jwks.json
Original file line number Diff line number Diff line change
@@ -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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
46 changes: 39 additions & 7 deletions oidc-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>gesundheitsid</artifactId>
<artifactId>oidc-server</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>com.oviva.gesundheitsid</groupId>
<artifactId>gesundheitsid</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down Expand Up @@ -42,6 +47,39 @@
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
</dependency>

<!-- BEGIN logging-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback.contrib</groupId>
<artifactId>logback-json-classic</artifactId>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ch.qos.logback.contrib</groupId>
<artifactId>logback-jackson</artifactId>
</dependency>
<!-- END logging-->

<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down Expand Up @@ -104,12 +142,6 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> supportedResponseTypes, List<URI> validRedirectUris) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.oviva.gesundheitsid.relyingparty.cfg;

import java.util.Optional;

public interface ConfigProvider {
Optional<String> get(String name);
}
Original file line number Diff line number Diff line change
@@ -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<String, String> getenv;

public EnvConfigProvider(String prefix, Function<String, String> getenv) {
this.prefix = prefix;
this.getenv = getenv;
}

@Override
public Optional<String> get(String name) {

var mangled = prefix + "_" + name;
mangled = mangled.toUpperCase(Locale.ROOT);
mangled = mangled.replaceAll("[^A-Z0-9]", "_");

return Optional.ofNullable(getenv.apply(mangled));
}
}
Original file line number Diff line number Diff line change
@@ -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<Code> remove(String code);
}
Original file line number Diff line number Diff line change
@@ -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<String, Code> store = new ConcurrentHashMap<>();

@Override
public void save(@NonNull Code code) {
store.put(code.code(), code);
}

@NonNull
@Override
public Optional<Code> remove(String code) {
return Optional.ofNullable(store.remove(code));
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Session> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
Loading

0 comments on commit ebb24fc

Please sign in to comment.