Skip to content

Commit

Permalink
ARC-1234: Bump Test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed Feb 6, 2024
1 parent a0e52ba commit cbce0f7
Show file tree
Hide file tree
Showing 14 changed files with 361 additions and 164 deletions.
56 changes: 41 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<URI> loadAllowedRedirectUrls() {
return configProvider.get(CONFIG_REDIRECT_URIS).stream()
.flatMap(Strings::mustParseCommaList)
.map(URI::create)
.toList();
}

private List<String> 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) {}
}
103 changes: 9 additions & 94 deletions ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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
Expand All @@ -183,11 +105,4 @@ private AuthenticationFlow buildAuthFlow(URI selfIssuer, URI fedmaster, JWKSet e
return new AuthenticationFlow(
selfIssuer, fedmasterClient, openIdClient, encJwks::getKeyByKeyId);
}

private List<URI> loadAllowedRedirectUrls() {
return configProvider.get("redirect_uris").stream()
.flatMap(Strings::mustParseCommaList)
.map(URI::create)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
import java.util.List;

public record RelyingPartyConfig(
int port, URI baseUri, List<String> supportedResponseTypes, List<URI> validRedirectUris) {}
List<String> supportedResponseTypes, List<URI> validRedirectUris) {}
Loading

0 comments on commit cbce0f7

Please sign in to comment.