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 1, 2024
1 parent 47dedac commit 6e98486
Show file tree
Hide file tree
Showing 28 changed files with 1,241 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ target/
gesundheitsid/env.properties
*.iml
gesundheitsid/dependency-reduced-pom.xml
.flattened-pom.xml
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 @@ -17,11 +17,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
115 changes: 115 additions & 0 deletions oidc-server/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.oviva.gesundheitsid</groupId>
<artifactId>gesundheitsid-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

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

<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<scope>test</scope>
</dependency>

<!-- BEGIN wiremock // fix for broken dependency convergence -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars-helpers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
</dependency>
<!-- END wiremock -->

<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
<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.InMemorySessionRepo;
import com.oviva.gesundheitsid.relyingparty.svc.InMemoryCodeRepo;
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,6 @@
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;
}
}
Loading

0 comments on commit 6e98486

Please sign in to comment.