From a0e52ba4adda321662347dd22148468e968d8284 Mon Sep 17 00:00:00 2001 From: Thomas Richner Date: Tue, 6 Feb 2024 17:04:16 +0100 Subject: [PATCH] ARC-1234: Keygenerator for Signing and Encryption Keys (#13) * ARC-1234: Cleanup all around & proper docs * ARC-1234: Fixed sonar project. --- README.md | 68 +++++++++++++++-- ehealthid-rp/pom.xml | 1 + .../oviva/ehealthid/relyingparty/Main.java | 74 +++++++++++-------- .../relyingparty/fed/FederationConfig.java | 8 ++ .../relyingparty/fed/FederationEndpoint.java | 7 +- .../cfg/EnvFederationConfigProviderTest.java | 2 +- .../fed/FederationEndpointTest.java | 1 + esgen/pom.xml | 2 +- .../java/com/oviva/ehealthid/esgen/Main.java | 3 +- pom.xml | 2 +- start.sh | 5 +- 11 files changed, 123 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 0782613..8857e31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=oviva-ag_keycloak-gesundheitsid&metric=alert_status&token=64c09371c0f6c1d729fc0b0424706cd54011cb90)](https://sonarcloud.io/summary/new_code?id=oviva-ag_keycloak-gesundheitsid) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=oviva-ag_keycloak-gesundheitsid&metric=coverage&token=64c09371c0f6c1d729fc0b0424706cd54011cb90)](https://sonarcloud.io/summary/new_code?id=oviva-ag_keycloak-gesundheitsid) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=oviva-ag_ehealthid-relying-party&metric=alert_status&token=ee904c8acea811b217358c63297ebe91fd6aee14)](https://sonarcloud.io/summary/new_code?id=oviva-ag_ehealthid-relying-party) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=oviva-ag_ehealthid-relying-party&metric=coverage&token=ee904c8acea811b217358c63297ebe91fd6aee14)](https://sonarcloud.io/summary/new_code?id=oviva-ag_ehealthid-relying-party) # OpenID Connect Relying Party for GesundheitsID (eHealthID) @@ -8,12 +8,57 @@ - [ehealthid-rp](./ehealthid-rp) - A standalone application to act as a OpenID Connect (OIDC) Relying Party. Bridges OIDC and Germany's GesundheitsID OpenID federation. - [esgen](./esgen) - A script to generate keys and federation registration forms. -- [gesundheitsid](./gesundheitsid) - A plain Java library to build RelyingParties for GesundheitsID. +- [ehealthid](./ehealthid) - A plain Java library to build RelyingParties for GesundheitsID. - API clients - Models for the EntityStatments, IDP list endpoints etc. - Narrow support for the 'Fachdienst' use-case. -## Generate Keys & Register for Federation +# Quickstart + +```shell +# build everything +mvn clean verify + +# generate keys for the application, keep those safe +./gen_keys.sh \ + --issuer-uri=https://mydiga.example.com \ + --member-id="$MEMBER_ID" \ + --organisation-name="My DiGA" \ + --generate-keys + +# configure the application +export EHEALTHID_RP_APP_NAME=Awesome DiGA +export EHEALTHID_RP_BASE_URI=https://mydiga.example.com +export EHEALTHID_RP_FEDERATION_ENC_JWKS_PATH=enc_jwks.json +export EHEALTHID_RP_FEDERATION_MASTER=https://app-test.federationmaster.de +export EHEALTHID_RP_FEDERATION_SIG_JWKS_PATH=sig_jwks.json +export EHEALTHID_RP_REDIRECT_URIS=https://sso-mydiga.example.com/auth/callback +export EHEALTHID_RP_ES_TTL=PT5M + +# boots the relying party server +./start.sh +``` + +# Configuration + +Use environment variables to configure the relying party server. + +| Name | Description | Example | +|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| +| `EHEALTHID_RP_FEDERATION_ENC_JWKS_PATH` | Path to a JWKS with at least one keypair for encryption of ID tokens. | `./enc_jwks.json` | +| `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` | +| `EHEALTHID_RP_REDIRECT_URIS` | Valid redirection URIs for OpenID connect. | `https://sso-mydiga.example.com/auth/callback` | +| `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` | +| `EHEALTHID_RP_HOST` | Host to bind to. | `0.0.0.0` | +| `EHEALTHID_RP_PORT` | Port to bind to. | `1234` | +| `EHEALTHID_RP_FEDERATION_MASTER` | The URI of the federation master. | `https://app-test.federationmaster.de` | +| `EHEALTHID_RP_APP_NAME` | The application name within the federation. | `Awesome DiGA` | +| `EHEALTHID_RP_ES_TTL` | The time to live for the entity statement. In ISO8601 format. | `PT12H` | +| `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` | + + + +# Generate Keys & Register for Federation In order to participate in the GesundheitsID one needs to register the entity statement of the IDP or in this case the relying party here. @@ -25,6 +70,10 @@ See [Gematik documentation](https://wiki.gematik.de/pages/viewpage.action?pageId details on the registration process. +```shell +./gen_keys.sh --help +``` + ### Generate Fresh Keys and Prepare Registration ```shell @@ -44,15 +93,22 @@ export MEMBER_ID=FDmyDiGa0112TU # a string received from Gematik as part of the registration process export MEMBER_ID=FDmyDiGa0112TU +# specify the environment, either +# TU -> test environment +# RU -> reference environment +# PU -> productive environment +export ENVIRONMENT=RU + ./gen_keys.sh \ --issuer-uri=https://mydiga.example.com \ --member-id="$MEMBER_ID" \ --organisation-name="My DiGA" \ + --environment=$ENVIRONMENT \ --signing-jwks=./sig_jwks.json \ --encryption-jwks=./enc_jwks.json ``` -## End-to-End Test flow with Gematik Reference IDP +## Library IntegrationTest flow with Gematik Reference IDP **Prerequisites**: @@ -111,7 +167,7 @@ public class Example { ``` -See [AuthenticationFlowExampleTest](https://github.com/oviva-ag/keycloak-gesundheitsid/blob/8751c92e45531f6cdca204b8db18a2825e05e69a/gesundheitsid/src/test/java/com/oviva/gesundheitsid/auth/AuthenticationFlowExampleTest.java#L44-L117) +See [AuthenticationFlowExampleTest](https://github.com/oviva-ag/ehealthid-relying-party/blob/main/ehealthid/src/test/java/com/oviva/ehealthid/auth/AuthenticationFlowExampleTest.java) ## Working with Gematik Test Environment diff --git a/ehealthid-rp/pom.xml b/ehealthid-rp/pom.xml index c4c9381..ed4d6ef 100644 --- a/ehealthid-rp/pom.xml +++ b/ehealthid-rp/pom.xml @@ -143,6 +143,7 @@ + ${project.artifactId} org.apache.maven.plugins diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java index 0d0c291..133a0cf 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java @@ -1,4 +1,4 @@ -package ehealthid.relyingparty; +package com.oviva.ehealthid.relyingparty; import com.nimbusds.jose.jwk.JWKSet; import com.oviva.ehealthid.auth.AuthenticationFlow; @@ -27,6 +27,7 @@ import java.time.Clock; import java.time.Duration; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +35,6 @@ public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); - private static final String BANNER = """ ____ _ @@ -43,6 +43,7 @@ public class Main { \\____/|___/_/|___/\\_,_/ GesundheitsID OpenID Connect Relying-Party """; + private static final String CONFIG_PREFIX = "EHEALTHID_RP"; private final ConfigProvider configProvider; public Main(ConfigProvider configProvider) { @@ -51,7 +52,7 @@ public Main(ConfigProvider configProvider) { public static void main(String[] args) throws ExecutionException, InterruptedException { - var main = new Main(new EnvConfigProvider("OIDC_SERVER", System::getenv)); + var main = new Main(new EnvConfigProvider(CONFIG_PREFIX, System::getenv)); main.run(); } @@ -64,13 +65,17 @@ public void run() throws ExecutionException, InterruptedException { var validRedirectUris = loadAllowedRedirectUrls(); - // TODO load from config - var baseUri = URI.create("https://t.oviva.io"); - - var supportedResponseTypes = List.of("code"); + var baseUri = + configProvider + .get("base_uri") + .map(URI::create) + .orElseThrow(() -> new IllegalArgumentException("no 'base_uri' configured")); + var host = configProvider.get("host").orElse("0.0.0.0"); var port = getPortConfig(); + var supportedResponseTypes = List.of("code"); + var config = new RelyingPartyConfig(port, baseUri, supportedResponseTypes, validRedirectUris); var keyStore = new KeyStore(); @@ -81,38 +86,56 @@ public void run() throws ExecutionException, InterruptedException { // setup your environment, your own issuer MUST serve a _valid_ and _trusted_ entity // configuration // see: https://wiki.gematik.de/pages/viewpage.action?pageId=544316583 - var fedmaster = URI.create("https://app-test.federationmaster.de"); + var fedmaster = + configProvider + .get("federation_master") + .map(URI::create) + .orElse(URI.create("https://app-test.federationmaster.de")); - // TODO replace with `baseUri` - var federationIssuer = URI.create("https://idp-test.oviva.io/auth/realms/master/ehealthid"); + var appName = + configProvider + .get("app_name") + .orElseThrow(() -> new IllegalArgumentException("missing 'app_name' configuration")); + + var entityStatementTtl = + configProvider.get("es_ttl").map(Duration::parse).orElse(Duration.ofHours(1)); var authFlow = buildAuthFlow(baseUri, fedmaster, federationEncJwksPath); + var scopes = + configProvider + .get("scopes") + .or( + () -> + Optional.of( + "openid,urn:telematik:email,urn:telematik:versicherter,urn:telematik:display_name")) + .stream() + .flatMap(Strings::mustParseCommaList) + .toList(); + var federationConfig = FederationConfig.create() .sub(baseUri) .iss(baseUri) - .appName("Oviva Direkt") + .appName(appName) .federationMaster(fedmaster) .entitySigningKey(federationSigJwksPath.getKeys().get(0).toECKey()) .entitySigningKeys(federationSigJwksPath.toPublicJWKSet()) .relyingPartyEncKeys(federationEncJwksPath.toPublicJWKSet()) - - // TODO: bump up to hours, once we're confident it's correct ;) - // the spec says ~1 day - .ttl(Duration.ofMinutes(5)) + .ttl(entityStatementTtl) + .scopes(scopes) .redirectUris(List.of(baseUri.resolve("/auth/callback").toString())) .build(); var instance = SeBootstrap.start( new App(config, federationConfig, sessionRepo, keyStore, tokenIssuer, authFlow), - Configuration.builder().host("0.0.0.0").port(config.port()).build()) + Configuration.builder().host(host).port(config.port()).build()) .toCompletableFuture() .get(); var localUri = instance.configuration().baseUri(); - logger.atInfo().addKeyValue("local_addr", localUri).log("Magic at {}", config.baseUri()); + logger.atInfo().log("Magic at {} ({})", config.baseUri(), localUri); // wait forever Thread.currentThread().join(); @@ -162,18 +185,9 @@ private AuthenticationFlow buildAuthFlow(URI selfIssuer, URI fedmaster, JWKSet e } private List loadAllowedRedirectUrls() { - - var redirectUris = - configProvider.get("redirect_uris").stream() - .flatMap(Strings::mustParseCommaList) - .map(URI::create) - .toList(); - - if (!redirectUris.isEmpty()) { - return redirectUris; - } - - // TODO: hardcoded - return List.of(URI.create("https://idp-test.oviva.io/auth/realms/master/broker/oidc/endpoint")); + return configProvider.get("redirect_uris").stream() + .flatMap(Strings::mustParseCommaList) + .map(URI::create) + .toList(); } } diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationConfig.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationConfig.java index 7e28647..0f4f16c 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationConfig.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationConfig.java @@ -17,6 +17,7 @@ public record FederationConfig( JWKSet relyingPartyEncKeys, Duration ttl, List redirectUris, + List scopes, String appName) { public static Builder create() { @@ -35,6 +36,7 @@ public static final class Builder { private JWKSet relyingPartyEncKeys; private Duration ttl; private List redirectUris; + private List scopes; private String appName; public Builder() {} @@ -84,6 +86,11 @@ public Builder appName(String appName) { return this; } + public Builder scopes(List scopes) { + this.scopes = scopes; + return this; + } + public FederationConfig build() { return new FederationConfig( iss, @@ -94,6 +101,7 @@ public FederationConfig build() { relyingPartyEncKeys, ttl, redirectUris, + scopes, appName); } } diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpoint.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpoint.java index f36fea7..eaf848a 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpoint.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpoint.java @@ -53,12 +53,7 @@ public Response get() { .idTokenSignedResponseAlg("ES256") .idTokenEncryptedResponseAlg("ECDH-ES") .idTokenEncryptedResponseEnc("A256GCM") - .scope( - String.join( - " ", - "openid", - "urn:telematik:email", - "urn:telematik:versicherter")) // add urn:telematik:display_name + .scope(String.join(" ", federationConfig.scopes())) .redirectUris(federationConfig.redirectUris()) .build()) .federationEntity( diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/cfg/EnvFederationConfigProviderTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/cfg/EnvFederationConfigProviderTest.java index c5c6c3a..41af50c 100644 --- a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/cfg/EnvFederationConfigProviderTest.java +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/cfg/EnvFederationConfigProviderTest.java @@ -10,7 +10,7 @@ class EnvFederationConfigProviderTest { - private static final String PREFIX = "OIDC_SERVER"; + private static final String PREFIX = "EHEALTHID_RP"; static Stream mangleTestCases() { return Stream.of( diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpointTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpointTest.java index fededb3..7fcda70 100644 --- a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpointTest.java +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/fed/FederationEndpointTest.java @@ -52,6 +52,7 @@ static void setUp() throws ExecutionException, InterruptedException, JOSEExcepti .sub(ISSUER) .redirectUris(List.of(ISSUER + "/callback")) .appName("My App") + .scopes(List.of("openid", "email")) .federationMaster(FEDMASTER) .relyingPartyEncKeys(new JWKSet(encryptionKey)) .entitySigningKeys(new JWKSet(signatureKey)) diff --git a/esgen/pom.xml b/esgen/pom.xml index 27847ba..ae0039d 100644 --- a/esgen/pom.xml +++ b/esgen/pom.xml @@ -72,7 +72,7 @@ - esgen + ${project.artifactId} org.apache.maven.plugins diff --git a/esgen/src/main/java/com/oviva/ehealthid/esgen/Main.java b/esgen/src/main/java/com/oviva/ehealthid/esgen/Main.java index 4382a38..44efd3d 100755 --- a/esgen/src/main/java/com/oviva/ehealthid/esgen/Main.java +++ b/esgen/src/main/java/com/oviva/ehealthid/esgen/Main.java @@ -38,7 +38,8 @@ public class Main implements Callable { @Option( names = {"-e", "--environment"}, - description = "the environment to register for", + description = + "the environment to register for, either TU (Testumgebung), RU (Referenzumgebung) or PU (Produktivumgebung)", defaultValue = "TU", required = true) private Environment environment; diff --git a/pom.xml b/pom.xml index 44e3e60..84af10e 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ oviva-ag https://sonarcloud.io ${project.artifactId} - oviva-ag_keycloak-gesundheitsid + oviva-ag_ehealthid-relying-party ${project.groupId}_${project.artifactId} esgen/** diff --git a/start.sh b/start.sh index df64dd9..32fcd65 100755 --- a/start.sh +++ b/start.sh @@ -1,6 +1,3 @@ #!/bin/bash -export OIDC_SERVER_FEDERATION_ENC_JWKS_PATH=enc_jwks.json -export OIDC_SERVER_FEDERATION_SIG_JWKS_PATH=sig_jwks.json - -java -jar oidc-server/target/oidc-server-*-jar-with-dependencies.jar +java -jar ehealthid-rp/target/ehealthid-rp-jar-with-dependencies.jar