From cbce0f762fe89a95587004b542cbe1ff28859455 Mon Sep 17 00:00:00 2001 From: Thomas Richner Date: Tue, 6 Feb 2024 18:40:18 +0100 Subject: [PATCH] ARC-1234: Bump Test coverage --- README.md | 56 ++++++-- .../ehealthid/relyingparty/ConfigReader.java | 131 ++++++++++++++++++ .../oviva/ehealthid/relyingparty/Main.java | 103 ++------------ .../relyingparty/cfg/RelyingPartyConfig.java | 2 +- .../oviva/ehealthid/relyingparty/ws/App.java | 19 ++- .../relyingparty/ws/AuthEndpoint.java | 7 +- .../relyingparty/ws/OpenIdEndpoint.java | 13 +- .../relyingparty/ConfigReaderTest.java | 73 ++++++++++ .../relyingparty/ws/AuthEndpointTest.java | 45 +++--- .../relyingparty/ws/OpenIdEndpointTest.java | 6 +- .../com/oviva/ehealthid/util/JsonCodec.java | 11 +- .../oviva/ehealthid/util/JsonCodecTest.java | 29 +++- .../oviva/ehealthid/util/JwksUtilsTest.java | 17 +++ .../resources/fixtures/jwks_utils_sample.json | 13 ++ 14 files changed, 361 insertions(+), 164 deletions(-) create mode 100644 ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ConfigReader.java create mode 100644 ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ConfigReaderTest.java create mode 100644 ehealthid/src/test/java/com/oviva/ehealthid/util/JwksUtilsTest.java create mode 100644 ehealthid/src/test/resources/fixtures/jwks_utils_sample.json diff --git a/README.md b/README.md index 8857e31..2fd3dda 100644 --- a/README.md +++ b/README.md @@ -35,28 +35,48 @@ 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 +# starts the relying party server ./start.sh + +# send in the generated XML to Gematik in order to register your IDP +cat federation_registration_form.xml ``` -# Configuration +Once the server is booted, it will: -Use environment variables to configure the relying party server. +1. Expose an OpenID Discovery document at `$EHEALTHID_RP_BASE_URI/.well-known/openid-configuration` + ```shell + curl $BASE_URI/.well-known/openid-configuration | jq . + ``` + +2. Expose an OpenID Federation entity configuration + at `$EHEALTHID_RP_BASE_URI/.well-known/openid-federation` + ```shell + curl $BASE_URI/.well-known/openid-federation | jwt decode -j - | jq .payload + ``` + **IMPORTANT:** Once the entity configuration is reachable in the internet it can be registered + with Gematik. You can directly send in the XML generated in the second step, the file is called `federation_registration_form.xml`. See documentation further below. -| 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` | +3. Be ready to handle OpenID Connect flows and handle them via Germany's GesundheitsID federation. +The discovery document can be used to configure the relying party in an existing identity provider. +# 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 @@ -85,6 +105,9 @@ export MEMBER_ID=FDmyDiGa0112TU --member-id="$MEMBER_ID" \ --organisation-name="My DiGA" \ --generate-keys + +# send in the generated XML to Gematik +cat federation_registration_form.xml ``` ### Re-use Existing Keys and Prepare Registration @@ -106,6 +129,9 @@ export ENVIRONMENT=RU --environment=$ENVIRONMENT \ --signing-jwks=./sig_jwks.json \ --encryption-jwks=./enc_jwks.json + +# send in the generated XML to Gematik +cat federation_registration_form.xml ``` ## Library IntegrationTest flow with Gematik Reference IDP diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ConfigReader.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ConfigReader.java new file mode 100644 index 0000000..8837e4f --- /dev/null +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ConfigReader.java @@ -0,0 +1,131 @@ +package com.oviva.ehealthid.relyingparty; + +import com.nimbusds.jose.jwk.JWKSet; +import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider; +import com.oviva.ehealthid.relyingparty.cfg.RelyingPartyConfig; +import com.oviva.ehealthid.relyingparty.fed.FederationConfig; +import com.oviva.ehealthid.relyingparty.util.Strings; +import com.oviva.ehealthid.util.JwksUtils; +import java.net.URI; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +public class ConfigReader { + + public static final String CONFIG_FEDERATION_ENC_JWKS_PATH = "federation_enc_jwks_path"; + public static final String CONFIG_FEDERATION_SIG_JWKS_PATH = "federation_sig_jwks_path"; + public static final String CONFIG_BASE_URI = "base_uri"; + public static final String CONFIG_HOST = "host"; + public static final String CONFIG_PORT = "port"; + public static final String CONFIG_REDIRECT_URIS = "redirect_uris"; + public static final String CONFIG_FEDERATION_MASTER = "federation_master"; + public static final String CONFIG_ES_TTL = "es_ttl"; + public static final String CONFIG_APP_NAME = "app_name"; + public static final String CONFIG_SCOPES = "scopes"; + + private final ConfigProvider configProvider; + + public ConfigReader(ConfigProvider configProvider) { + this.configProvider = configProvider; + } + + public Config read() { + + var federationEncJwksPath = loadJwks(CONFIG_FEDERATION_ENC_JWKS_PATH); + var federationSigJwksPath = loadJwks(CONFIG_FEDERATION_SIG_JWKS_PATH); + + var baseUri = + configProvider + .get(CONFIG_BASE_URI) + .map(URI::create) + .orElseThrow(() -> new IllegalArgumentException("no 'base_uri' configured")); + + var host = configProvider.get(CONFIG_HOST).orElse("0.0.0.0"); + var port = getPortConfig(); + + var fedmaster = + configProvider + .get(CONFIG_FEDERATION_MASTER) + .map(URI::create) + .orElse(URI.create("https://app-test.federationmaster.de")); + + var appName = + configProvider + .get(CONFIG_APP_NAME) + .orElseThrow(() -> new IllegalArgumentException("missing 'app_name' configuration")); + + var entityStatementTtl = + configProvider.get(CONFIG_ES_TTL).map(Duration::parse).orElse(Duration.ofHours(1)); + + var federationConfig = + FederationConfig.create() + .sub(baseUri) + .iss(baseUri) + .appName(appName) + .federationMaster(fedmaster) + .entitySigningKey(federationSigJwksPath.getKeys().get(0).toECKey()) + .entitySigningKeys(federationSigJwksPath.toPublicJWKSet()) + .relyingPartyEncKeys(federationEncJwksPath.toPublicJWKSet()) + .ttl(entityStatementTtl) + .scopes(getScopes()) + .redirectUris(List.of(baseUri.resolve("/auth/callback").toString())) + .build(); + + var supportedResponseTypes = List.of("code"); + + var relyingPartyConfig = + new RelyingPartyConfig(supportedResponseTypes, loadAllowedRedirectUrls()); + + return new Config(relyingPartyConfig, federationConfig, host, port, baseUri); + } + + private List loadAllowedRedirectUrls() { + return configProvider.get(CONFIG_REDIRECT_URIS).stream() + .flatMap(Strings::mustParseCommaList) + .map(URI::create) + .toList(); + } + + private List getScopes() { + + return configProvider + .get(CONFIG_SCOPES) + .or( + () -> + Optional.of( + "openid,urn:telematik:email,urn:telematik:versicherter,urn:telematik:display_name")) + .stream() + .flatMap(Strings::mustParseCommaList) + .toList(); + } + + private int getPortConfig() { + return configProvider.get(CONFIG_PORT).stream() + .mapToInt(Integer::parseInt) + .findFirst() + .orElse(1234); + } + + private JWKSet loadJwks(String configName) { + + var path = + configProvider + .get(configName) + .map(Path::of) + .orElseThrow( + () -> + new IllegalArgumentException( + "missing jwks path for '%s'".formatted(configName))); + + return JwksUtils.load(path); + } + + public record Config( + RelyingPartyConfig relyingParty, + FederationConfig federation, + String host, + int port, + URI baseUri) {} +} 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 133a0cf..d72376e 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 @@ -10,24 +10,17 @@ import com.oviva.ehealthid.fedclient.api.OpenIdClient; import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider; import com.oviva.ehealthid.relyingparty.cfg.EnvConfigProvider; -import com.oviva.ehealthid.relyingparty.cfg.RelyingPartyConfig; -import com.oviva.ehealthid.relyingparty.fed.FederationConfig; import com.oviva.ehealthid.relyingparty.poc.GematikHeaderDecoratorHttpClient; import com.oviva.ehealthid.relyingparty.svc.InMemoryCodeRepo; import com.oviva.ehealthid.relyingparty.svc.InMemorySessionRepo; import com.oviva.ehealthid.relyingparty.svc.KeyStore; import com.oviva.ehealthid.relyingparty.svc.TokenIssuerImpl; -import com.oviva.ehealthid.relyingparty.util.Strings; import com.oviva.ehealthid.relyingparty.ws.App; -import com.oviva.ehealthid.util.JwksUtils; import jakarta.ws.rs.SeBootstrap; import jakarta.ws.rs.SeBootstrap.Configuration; import java.net.URI; -import java.nio.file.Path; 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; @@ -60,77 +53,24 @@ public void run() throws ExecutionException, InterruptedException { logger.atInfo().log("\n" + BANNER); - var federationEncJwksPath = loadJwks("federation_enc_jwks_path"); - var federationSigJwksPath = loadJwks("federation_sig_jwks_path"); + var configReader = new ConfigReader(configProvider); - var validRedirectUris = loadAllowedRedirectUrls(); - - 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 config = configReader.read(); var keyStore = new KeyStore(); var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, new InMemoryCodeRepo()); var sessionRepo = new InMemorySessionRepo(); - // TODO - // 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 = - configProvider - .get("federation_master") - .map(URI::create) - .orElse(URI.create("https://app-test.federationmaster.de")); - - 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(appName) - .federationMaster(fedmaster) - .entitySigningKey(federationSigJwksPath.getKeys().get(0).toECKey()) - .entitySigningKeys(federationSigJwksPath.toPublicJWKSet()) - .relyingPartyEncKeys(federationEncJwksPath.toPublicJWKSet()) - .ttl(entityStatementTtl) - .scopes(scopes) - .redirectUris(List.of(baseUri.resolve("/auth/callback").toString())) - .build(); + var authFlow = + buildAuthFlow( + config.baseUri(), + config.federation().federationMaster(), + config.federation().relyingPartyEncKeys()); var instance = SeBootstrap.start( - new App(config, federationConfig, sessionRepo, keyStore, tokenIssuer, authFlow), - Configuration.builder().host(host).port(config.port()).build()) + new App(config, sessionRepo, keyStore, tokenIssuer, authFlow), + Configuration.builder().host(config.host()).port(config.port()).build()) .toCompletableFuture() .get(); @@ -141,24 +81,6 @@ public void run() throws ExecutionException, InterruptedException { Thread.currentThread().join(); } - private int getPortConfig() { - return configProvider.get("port").stream().mapToInt(Integer::parseInt).findFirst().orElse(1234); - } - - private JWKSet loadJwks(String configName) { - - var path = - configProvider - .get(configName) - .map(Path::of) - .orElseThrow( - () -> - new IllegalArgumentException( - "missing jwks path for '%s'".formatted(configName))); - - return JwksUtils.load(path); - } - private AuthenticationFlow buildAuthFlow(URI selfIssuer, URI fedmaster, JWKSet encJwks) { // setup the file `.env.properties` to provide the X-Authorization header for the Gematik @@ -183,11 +105,4 @@ private AuthenticationFlow buildAuthFlow(URI selfIssuer, URI fedmaster, JWKSet e return new AuthenticationFlow( selfIssuer, fedmasterClient, openIdClient, encJwks::getKeyByKeyId); } - - private List loadAllowedRedirectUrls() { - 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/cfg/RelyingPartyConfig.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/cfg/RelyingPartyConfig.java index ea83f26..3a9c1b7 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/cfg/RelyingPartyConfig.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/cfg/RelyingPartyConfig.java @@ -4,4 +4,4 @@ import java.util.List; public record RelyingPartyConfig( - int port, URI baseUri, List supportedResponseTypes, List validRedirectUris) {} + List supportedResponseTypes, List validRedirectUris) {} diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/App.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/App.java index 243168c..7f82d90 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/App.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/App.java @@ -4,8 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider; import com.oviva.ehealthid.auth.AuthenticationFlow; -import com.oviva.ehealthid.relyingparty.cfg.RelyingPartyConfig; -import com.oviva.ehealthid.relyingparty.fed.FederationConfig; +import com.oviva.ehealthid.relyingparty.ConfigReader.Config; import com.oviva.ehealthid.relyingparty.fed.FederationEndpoint; import com.oviva.ehealthid.relyingparty.svc.KeyStore; import com.oviva.ehealthid.relyingparty.svc.SessionRepo; @@ -16,8 +15,7 @@ public class App extends Application { - private final RelyingPartyConfig relyingPartyConfig; - private final FederationConfig federationConfig; + private final Config config; private final SessionRepo sessionRepo; private final KeyStore keyStore; @@ -26,14 +24,12 @@ public class App extends Application { private final AuthenticationFlow authenticationFlow; public App( - RelyingPartyConfig relyingPartyConfig, - FederationConfig federationConfig, + Config config, SessionRepo sessionRepo, KeyStore keyStore, TokenIssuer tokenIssuer, AuthenticationFlow authenticationFlow) { - this.relyingPartyConfig = relyingPartyConfig; - this.federationConfig = federationConfig; + this.config = config; this.sessionRepo = sessionRepo; this.keyStore = keyStore; this.tokenIssuer = tokenIssuer; @@ -44,9 +40,10 @@ public App( public Set getSingletons() { return Set.of( - new FederationEndpoint(federationConfig), - new AuthEndpoint(relyingPartyConfig, sessionRepo, tokenIssuer, authenticationFlow), - new OpenIdEndpoint(relyingPartyConfig, keyStore), + new FederationEndpoint(config.federation()), + new AuthEndpoint( + config.baseUri(), config.relyingParty(), sessionRepo, tokenIssuer, authenticationFlow), + new OpenIdEndpoint(config.baseUri(), config.relyingParty(), keyStore), new JacksonJsonProvider(configureObjectMapper())); } diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java index de4fdc4..1e50ad3 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java @@ -34,6 +34,7 @@ @Path("/auth") public class AuthEndpoint { + private final URI baseUri; private final RelyingPartyConfig relyingPartyConfig; private final SessionRepo sessionRepo; @@ -42,10 +43,12 @@ public class AuthEndpoint { private final AuthenticationFlow authenticationFlow; public AuthEndpoint( + URI baseUri, RelyingPartyConfig relyingPartyConfig, SessionRepo sessionRepo, TokenIssuer tokenIssuer, AuthenticationFlow authenticationFlow) { + this.baseUri = baseUri; this.relyingPartyConfig = relyingPartyConfig; this.sessionRepo = sessionRepo; this.tokenIssuer = tokenIssuer; @@ -132,12 +135,12 @@ public Response auth( var codeChallenge = calculateS256CodeChallenge(verifier); // ==== 1) start a new flow - var relyingPartyCallback = relyingPartyConfig.baseUri().resolve("/auth/callback"); + var relyingPartyCallback = baseUri.resolve("/auth/callback"); var step1 = authenticationFlow.start( new AuthenticationFlow.Session( - "test", "test", relyingPartyCallback, codeChallenge, scopes)); + state, nonce, relyingPartyCallback, codeChallenge, scopes)); // ==== 2) get the list of available IDPs var idps = step1.fetchIdpOptions(); diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpoint.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpoint.java index 7f09cb8..d7af575 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpoint.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpoint.java @@ -9,16 +9,19 @@ import jakarta.ws.rs.core.CacheControl; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.net.URI; import java.time.Duration; import java.util.List; @Path("/") public class OpenIdEndpoint { + private final URI baseUri; private final RelyingPartyConfig relyingPartyConfig; private final KeyStore keyStore; - public OpenIdEndpoint(RelyingPartyConfig relyingPartyConfig, KeyStore keyStore) { + public OpenIdEndpoint(URI baseUri, RelyingPartyConfig relyingPartyConfig, KeyStore keyStore) { + this.baseUri = baseUri; this.relyingPartyConfig = relyingPartyConfig; this.keyStore = keyStore; } @@ -30,10 +33,10 @@ public Response openIdConfiguration() { var body = new OpenIdConfiguration( - relyingPartyConfig.baseUri().toString(), - relyingPartyConfig.baseUri().resolve("/auth").toString(), - relyingPartyConfig.baseUri().resolve("/token").toString(), - relyingPartyConfig.baseUri().resolve("/jwks.json").toString(), + baseUri.toString(), + baseUri.resolve("/auth").toString(), + baseUri.resolve("/token").toString(), + baseUri.resolve("/jwks.json").toString(), List.of("openid"), relyingPartyConfig.supportedResponseTypes(), List.of("authorization_code"), diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ConfigReaderTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ConfigReaderTest.java new file mode 100644 index 0000000..5d5325b --- /dev/null +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ConfigReaderTest.java @@ -0,0 +1,73 @@ +package com.oviva.ehealthid.relyingparty; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class ConfigReaderTest { + + @Test + void read_defaults() { + var provider = mock(ConfigProvider.class); + + var sut = new ConfigReader(provider); + + var baseUri = "https://rp.example.com"; + var appName = "Awesome DiGA"; + + when(provider.get(ConfigReader.CONFIG_FEDERATION_ENC_JWKS_PATH)) + .thenReturn(Optional.of("./src/test/resources/fixtures/example_enc_jwks.json")); + when(provider.get(ConfigReader.CONFIG_FEDERATION_SIG_JWKS_PATH)) + .thenReturn(Optional.of("./src/test/resources/fixtures/example_sig_jwks.json")); + when(provider.get(ConfigReader.CONFIG_BASE_URI)).thenReturn(Optional.of(baseUri)); + when(provider.get(ConfigReader.CONFIG_APP_NAME)).thenReturn(Optional.of(appName)); + + // when + var config = sut.read(); + + // then + assertEquals(baseUri, config.baseUri().toString()); + assertEquals(appName, config.federation().appName()); + + assertEquals("0.0.0.0", config.host()); + assertEquals(1234, config.port()); + + assertEquals(List.of("code"), config.relyingParty().supportedResponseTypes()); + assertEquals(List.of(), config.relyingParty().validRedirectUris()); + + assertEquals(baseUri, config.federation().iss().toString()); + assertEquals(baseUri, config.federation().sub().toString()); + assertEquals( + List.of( + "openid", + "urn:telematik:email", + "urn:telematik:versicherter", + "urn:telematik:display_name"), + config.federation().scopes()); + + assertNotNull(config.federation().entitySigningKey()); + assertNotNull(config.federation().entitySigningKeys().getKeyByKeyId("test-sig")); + assertNotNull(config.federation().relyingPartyEncKeys().getKeyByKeyId("test-enc")); + } + + @Test + void read_missingJwks() { + var provider = mock(ConfigProvider.class); + + var sut = new ConfigReader(provider); + + var baseUri = "https://rp.example.com"; + var appName = "Awesome DiGA"; + + when(provider.get(ConfigReader.CONFIG_BASE_URI)).thenReturn(Optional.of(baseUri)); + when(provider.get(ConfigReader.CONFIG_APP_NAME)).thenReturn(Optional.of(appName)); + + // when + assertThrows(IllegalArgumentException.class, sut::read); + } +} diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java index 81dcbda..cc0b7db 100644 --- a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java @@ -30,9 +30,10 @@ class AuthEndpointTest { @Test void auth_badScopes() { - var config = new RelyingPartyConfig(0, BASE_URI, null, List.of(REDIRECT_URI)); - var sut = new AuthEndpoint(config, null, null, null); + var rpConfig = new RelyingPartyConfig(null, List.of(REDIRECT_URI)); + + var sut = new AuthEndpoint(BASE_URI, rpConfig, null, null, null); var scope = "openid email"; var state = UUID.randomUUID().toString(); @@ -54,9 +55,9 @@ void auth_badScopes() { @Test void auth_malformedRedirect() { - var config = new RelyingPartyConfig(0, BASE_URI, null, List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(null, List.of(REDIRECT_URI)); - var sut = new AuthEndpoint(config, null, null, null); + var sut = new AuthEndpoint(BASE_URI, config, null, null, null); var scope = "openid email"; var state = UUID.randomUUID().toString(); @@ -74,9 +75,9 @@ void auth_malformedRedirect() { @Test void auth_untrustedRedirect() { - var config = new RelyingPartyConfig(0, BASE_URI, null, List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(null, List.of(REDIRECT_URI)); - var sut = new AuthEndpoint(config, null, null, null); + var sut = new AuthEndpoint(BASE_URI, config, null, null, null); var scope = "openid email"; var state = UUID.randomUUID().toString(); @@ -97,10 +98,10 @@ void auth_untrustedRedirect() { @Test void auth_badResponseType() { - var config = new RelyingPartyConfig(0, BASE_URI, List.of("code"), List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(List.of("code"), List.of(REDIRECT_URI)); var sessionRepo = mock(SessionRepo.class); - var sut = new AuthEndpoint(config, sessionRepo, null, null); + var sut = new AuthEndpoint(BASE_URI, config, sessionRepo, null, null); var scope = "openid"; var state = UUID.randomUUID().toString(); @@ -126,7 +127,7 @@ void auth_badResponseType() { @Test void auth_success() { - var config = new RelyingPartyConfig(0, BASE_URI, List.of("code"), List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(List.of("code"), List.of(REDIRECT_URI)); var idpRedirectUrl = URI.create("https://federated-idp.example.com"); @@ -141,7 +142,7 @@ void auth_success() { when(authFlow.start(any())).thenReturn(selectIdpStep); var sessionRepo = mock(SessionRepo.class); - var sut = new AuthEndpoint(config, sessionRepo, null, authFlow); + var sut = new AuthEndpoint(BASE_URI, config, sessionRepo, null, authFlow); var scope = "openid"; var state = UUID.randomUUID().toString(); @@ -170,9 +171,9 @@ void auth_success() { @ValueSource(strings = {" ", " \n\t"}) void callback_noSessionId(String sessionId) { - var config = new RelyingPartyConfig(0, null, null, null); + var config = new RelyingPartyConfig(null, null); - var sut = new AuthEndpoint(config, null, null, null); + var sut = new AuthEndpoint(BASE_URI, config, null, null, null); // when try (var res = sut.callback(sessionId, "")) { @@ -185,11 +186,11 @@ void callback_noSessionId(String sessionId) { @Test void callback_unknownSession() { - var config = new RelyingPartyConfig(0, null, null, null); + var config = new RelyingPartyConfig(null, null); var sessionRepo = mock(SessionRepo.class); - var sut = new AuthEndpoint(config, sessionRepo, null, null); + var sut = new AuthEndpoint(BASE_URI, config, sessionRepo, null, null); var sessionId = UUID.randomUUID().toString(); @@ -206,12 +207,12 @@ void callback_unknownSession() { @Test void callback() { - var config = new RelyingPartyConfig(0, BASE_URI, List.of("code"), List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(List.of("code"), List.of(REDIRECT_URI)); var sessionRepo = mock(SessionRepo.class); var tokenIssuer = mock(TokenIssuer.class); - var sut = new AuthEndpoint(config, sessionRepo, tokenIssuer, null); + var sut = new AuthEndpoint(BASE_URI, config, sessionRepo, tokenIssuer, null); var sessionId = UUID.randomUUID().toString(); @@ -246,11 +247,11 @@ void callback() { @Test void token_badGrantType() { - var config = new RelyingPartyConfig(0, BASE_URI, List.of("code"), List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(List.of("code"), List.of(REDIRECT_URI)); var tokenIssuer = mock(TokenIssuer.class); - var sut = new AuthEndpoint(config, null, tokenIssuer, null); + var sut = new AuthEndpoint(BASE_URI, config, null, tokenIssuer, null); var clientId = "myapp"; @@ -271,11 +272,11 @@ void token_badGrantType() { @Test void token_badCode() { - var config = new RelyingPartyConfig(0, BASE_URI, List.of("code"), List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(List.of("code"), List.of(REDIRECT_URI)); var tokenIssuer = mock(TokenIssuer.class); - var sut = new AuthEndpoint(config, null, tokenIssuer, null); + var sut = new AuthEndpoint(BASE_URI, config, null, tokenIssuer, null); var clientId = "myapp"; @@ -296,11 +297,11 @@ void token_badCode() { @Test void token() { - var config = new RelyingPartyConfig(0, BASE_URI, List.of("code"), List.of(REDIRECT_URI)); + var config = new RelyingPartyConfig(List.of("code"), List.of(REDIRECT_URI)); var tokenIssuer = mock(TokenIssuer.class); - var sut = new AuthEndpoint(config, null, tokenIssuer, null); + var sut = new AuthEndpoint(BASE_URI, config, null, tokenIssuer, null); var clientId = "myapp"; diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpointTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpointTest.java index 839327e..cdc9e53 100644 --- a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpointTest.java +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/OpenIdEndpointTest.java @@ -20,8 +20,8 @@ class OpenIdEndpointTest { @Test void openIdConfiguration() { - var config = new RelyingPartyConfig(0, BASE_URI, null, null); - var sut = new OpenIdEndpoint(config, null); + var config = new RelyingPartyConfig(null, null); + var sut = new OpenIdEndpoint(BASE_URI, config, null); // when OpenIdConfiguration body; @@ -48,7 +48,7 @@ void jwks() throws ParseException { var keyStore = mock(KeyStore.class); when(keyStore.signingKey()).thenReturn(key); - var sut = new OpenIdEndpoint(null, keyStore); + var sut = new OpenIdEndpoint(BASE_URI, null, keyStore); try (var res = sut.jwks()) { var jwks = res.readEntity(JWKSet.class); diff --git a/ehealthid/src/main/java/com/oviva/ehealthid/util/JsonCodec.java b/ehealthid/src/main/java/com/oviva/ehealthid/util/JsonCodec.java index 160b676..c44921f 100644 --- a/ehealthid/src/main/java/com/oviva/ehealthid/util/JsonCodec.java +++ b/ehealthid/src/main/java/com/oviva/ehealthid/util/JsonCodec.java @@ -11,10 +11,11 @@ public class JsonCodec { private static ObjectMapper om; static { - var om = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + var om = new ObjectMapper(); om.registerModule(new JoseModule()); om.setSerializationInclusion(Include.NON_NULL); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); JsonCodec.om = om; } @@ -29,14 +30,6 @@ public static String writeValueAsString(Object value) { } } - public static T readValue(String in, Class clazz) { - try { - return om.readValue(in, clazz); - } catch (IOException e) { - throw new DeserializeException("failed to deserialize JSON", e); - } - } - public static T readValue(byte[] in, Class clazz) { try { return om.readValue(in, clazz); diff --git a/ehealthid/src/test/java/com/oviva/ehealthid/util/JsonCodecTest.java b/ehealthid/src/test/java/com/oviva/ehealthid/util/JsonCodecTest.java index 86a445d..8273ba5 100644 --- a/ehealthid/src/test/java/com/oviva/ehealthid/util/JsonCodecTest.java +++ b/ehealthid/src/test/java/com/oviva/ehealthid/util/JsonCodecTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import com.fasterxml.jackson.core.JsonProcessingException; @@ -10,8 +11,10 @@ import com.nimbusds.jose.jwk.JWKSet; import com.oviva.ehealthid.test.Fixtures; import com.oviva.ehealthid.util.JsonCodec.DeserializeException; +import com.oviva.ehealthid.util.JsonCodec.SerializeException; import java.nio.charset.StandardCharsets; import java.text.ParseException; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -28,17 +31,29 @@ static List testCasesEncode() throws ParseException { {"keys":[{"kty":"EC","d":"6RrmHekWp_RwY6FNlM46zwt1wFytfVQSYrS2-DDLj7g","use":"enc","crv":"P-256","kid":"test","x":"M4yMVgv6nV9AHvNCdrFUZ2zLnSD8yXFZBgbLgXU0vAc","y":"AvE4diGs4teOYHECACyi41UMxPGv8myq-Y7MBZGfwzY"}]}""")); } + static List testCasesFailingEncode() { + + var loop = new ArrayList<>(); + var b = new ArrayList<>(); + loop.add(b); + b.add(loop); + + return List.of( + new FailingEncodeTC(loop, SerializeException.class), + new FailingEncodeTC(new JsonCodecTest(), SerializeException.class)); + } + @Test void readBadJson() { var raw = """ {"a": bad } - """; + """.getBytes(StandardCharsets.UTF_8); assertThrows(DeserializeException.class, () -> JsonCodec.readValue(raw, Object.class)); } @Test void readJwksString() { - var raw = Fixtures.getUtf8String("json_codec_jwks.json"); + var raw = Fixtures.get("json_codec_jwks.json"); var jwks = JsonCodec.readValue(raw, JWKSet.class); assertThat(jwks.getKeys(), hasSize(1)); @@ -58,6 +73,14 @@ void readJwksBytes() { assertEquals("EC", key.getKeyType().getValue()); } + @ParameterizedTest + @MethodSource("testCasesFailingEncode") + void encode(FailingEncodeTC tc) { + + var exception = assertThrows(Exception.class, () -> JsonCodec.writeValueAsString(tc.in())); + assertInstanceOf(tc.expectedException(), exception); + } + @ParameterizedTest @MethodSource("testCasesEncode") void encode(EncodeTC tc) throws JsonProcessingException { @@ -72,4 +95,6 @@ void encode(EncodeTC tc) throws JsonProcessingException { } record EncodeTC(Object in, String expected) {} + + record FailingEncodeTC(Object in, Class expectedException) {} } diff --git a/ehealthid/src/test/java/com/oviva/ehealthid/util/JwksUtilsTest.java b/ehealthid/src/test/java/com/oviva/ehealthid/util/JwksUtilsTest.java new file mode 100644 index 0000000..2ed8ecc --- /dev/null +++ b/ehealthid/src/test/java/com/oviva/ehealthid/util/JwksUtilsTest.java @@ -0,0 +1,17 @@ +package com.oviva.ehealthid.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +class JwksUtilsTest { + + @Test + void load() { + var jwks = JwksUtils.load(Path.of("./src/test/resources/fixtures/jwks_utils_sample.json")); + + assertEquals(1, jwks.size()); + assertNotNull(jwks.getKeyByKeyId("test")); + } +} diff --git a/ehealthid/src/test/resources/fixtures/jwks_utils_sample.json b/ehealthid/src/test/resources/fixtures/jwks_utils_sample.json new file mode 100644 index 0000000..a68d8fd --- /dev/null +++ b/ehealthid/src/test/resources/fixtures/jwks_utils_sample.json @@ -0,0 +1,13 @@ +{ + "keys": [ + { + "kty": "EC", + "d": "6RrmHekWp_RwY6FNlM46zwt1wFytfVQSYrS2-DDLj7g", + "use": "enc", + "crv": "P-256", + "kid": "test", + "x": "M4yMVgv6nV9AHvNCdrFUZ2zLnSD8yXFZBgbLgXU0vAc", + "y": "AvE4diGs4teOYHECACyi41UMxPGv8myq-Y7MBZGfwzY" + } + ] +} \ No newline at end of file