Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EPA-145: Generate eHealthID federation statement for productive environment #98

Merged
merged 2 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Use environment variables to configure the relying party server.
| `EHEALTHID_RP_HOST` | Host to bind to. | `0.0.0.0` |
| `EHEALTHID_RP_PORT` | Port to bind to. | `1234` |
| `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` |
| `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:versicherter` |
| `EHEALTHID_RP_SESSION_STORE_TTL` | The time to live for sessions. In ISO8601 format. | `PT20M` |
| `EHEALTHID_RP_SESSION_STORE_MAX_ENTRIES` | The maximum number of sessions to store. Keeps memory bounded. | `1000` |
| `EHEALTHID_RP_CODE_STORE_TTL` | The time to live for codes, i.e. successful logins where the code is not redeemed yet. In ISO8601 format. | `PT5M` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@ public class FedRegistrationCommand implements Callable<Integer> {

@Option(
names = {"-c", "--contact-email"},
description = "the technical contact email for the IdP",
required = true)
private String email;
description = "the technical contact email for the IdP")
private String contactEmail;

@Option(
names = {"--vfs-confirmation"},
description = "gematik-Verfahrensschlüssel (starts with VFS_DiGA_...)")
private String vfsConfirmation;

public static void main(String[] args) {

Expand All @@ -83,6 +87,8 @@ public static void main(String[] args) {

public Integer call() throws Exception {

parameterValidation();

var entityConfiguration = fetchEntityConfiguration();
for (var key : entityConfiguration.jwks().getKeys()) {
validateKey(key, KeyUse.SIGNATURE);
Expand All @@ -97,28 +103,43 @@ public Integer call() throws Exception {
return 0;
}

private void printRegistrationForm(EntityConfiguration entityConfiguration) {
var registrationForm = renderRegistrationForm(entityConfiguration);
System.out.println(registrationForm);
}

private void writeRegistrationForm(EntityConfiguration entityConfiguration) throws IOException {
var registrationForm = renderRegistrationForm(entityConfiguration);

logger.atInfo().log("writing registration form to '{}'", file);
Files.writeString(file, registrationForm);
}

private String renderRegistrationForm(EntityConfiguration entityConfiguration) {
return RegistratonFormRenderer.render(
new Model(
vfsConfirmation,
memberId,
entityConfiguration.orgName(),
email,
contactEmail,
issuerUri,
environment,
entityConfiguration.scopes(),
entityConfiguration.jwks()));
}

private void printRegistrationForm(EntityConfiguration entityConfiguration) {
var registrationForm = renderRegistrationForm(entityConfiguration);
System.out.println(registrationForm);
}

private void writeRegistrationForm(EntityConfiguration entityConfiguration) throws IOException {
var registrationForm = renderRegistrationForm(entityConfiguration);

logger.atInfo().log("writing registration form to '{}'", file);
Files.writeString(file, registrationForm);
private void parameterValidation() {
if (environment == Environment.PU) {
if (vfsConfirmation == null || vfsConfirmation.isBlank()) {
logger.atError().log("Verfahrensschlüssel is required for production (PU) environment");
throw new RuntimeException();
}
} else {
if (contactEmail == null || contactEmail.isBlank()) {
logger.atError().log("contact email is required for test environments");
throw new RuntimeException();
}
}
}

private EntityConfiguration fetchEntityConfiguration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,40 @@

public class RegistratonFormRenderer {

private static final String XML_TEMPLATE =
// the PU registration XML differs from others
private static final String XML_TEMPLATE_PROD =
"""
<?xml version="1.0" encoding="UTF-8"?>
<registrierungtifoederation>
<datendesantragstellers>
<vfsbestaetigung>{{vfsConfirmation}}</vfsbestaetigung>
<teilnehmertyp>Fachdienst</teilnehmertyp>
<organisationsname>{{organisationName}}</organisationsname>
<memberid>{{memberId}}</memberid>
<zwg>ORG-0001:BT-0144</zwg>
<issueruri>{{issuerUri}}</issueruri>
<scopes>
<scopealter>{{scopeAge}}</scopealter>
<scopeanzeigename>{{scopeDisplayName}}</scopeanzeigename>
<scopeemail>{{scopeEmail}}</scopeemail>
<scopegeschlecht>{{scopeGender}}</scopegeschlecht>
<scopegeburtsdatum>{{scopeDateOfBirth}}</scopegeburtsdatum>
<scopevorname>{{scopeFirstName}}</scopevorname>
<scopenachname>{{scopeLastName}}</scopenachname>
<scopeversicherter>{{scopeInsuredPerson}}</scopeversicherter>
</scopes>
{{#publicKeys}}
<publickeys>
<kidjwt>{{kid}}</kidjwt>
<pubkeyjwt>{{pem}}</pubkeyjwt>
<betriebsumgebung>{{environment}}</betriebsumgebung>
</publickeys>
{{/publicKeys}}
</datendesantragstellers>
</registrierungtifoederation>
""";

private static final String XML_TEMPLATE_TEST =
"""
<?xml version="1.0" encoding="UTF-8"?>
<registrierungtifoederation>
Expand Down Expand Up @@ -48,17 +81,34 @@ public class RegistratonFormRenderer {

public static String render(Model m) {

return switch (m.environment()) {
case PU -> renderProductiveEnvironment(m);
default -> renderTestEnvironment(m);
};
}

private static String renderProductiveEnvironment(Model m) {
return renderTemplate(XML_TEMPLATE_PROD, m);
}

private static String renderTestEnvironment(Model m) {
return renderTemplate(XML_TEMPLATE_TEST, m);
}

private static String renderTemplate(String template, Model m) {

var mf = new DefaultMustacheFactory();
var template = mf.compile(new StringReader(XML_TEMPLATE), "entity-statement-registration");
var compiledTemplate = mf.compile(new StringReader(template), "entity-statement-registration");

var w = new StringWriter();

var rm = RenderModel.fromModel(m);
template.execute(w, rm);
compiledTemplate.execute(w, rm);
return w.toString();
}

public record Model(
String vfsConfirmation,
String memberId,
String organisationName,
String contactEmail,
Expand Down Expand Up @@ -87,6 +137,7 @@ enum Scope {
}

record RenderModel(
String vfsConfirmation,
String memberId,
String organisationName,
String issuerUri,
Expand All @@ -105,6 +156,7 @@ record RenderModel(
public static RenderModel fromModel(Model m) {

return new RenderModel(
m.vfsConfirmation(),
m.memberId(),
m.organisationName(),
m.issuerUri().toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ void render() throws JOSEException {
var xml =
RegistratonFormRenderer.render(
new Model(
"VFS_DiGA_Test",
"FDmyDiGAMemb",
"My DiGA",
"[email protected]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,7 @@ private List<String> getScopes() {

return configProvider
.get(CONFIG_SCOPES)
.or(
() ->
Optional.of(
"openid,urn:telematik:email,urn:telematik:versicherter,urn:telematik:display_name"))
.or(() -> Optional.of("openid,urn:telematik:versicherter"))
.stream()
.flatMap(Strings::mustParseCommaList)
.toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class GematikHeaderDecoratorHttpClient implements HttpClient {
LoggerFactory.getLogger(GematikHeaderDecoratorHttpClient.class);

// RU: https://gsi-ref.dev.gematik.solutions/.well-known/openid-federation
// RU PAR mTLS: https://gsi-ref-mtls.dev.gematik.solutions/PAR_Auth
// TU: https://gsi.dev.gematik.solutions/.well-known/openid-federation
private static final Pattern HOST_GEMATIK_IDP =
Pattern.compile("gsi(-[-a-z0-9]+)?.dev.gematik.solutions");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,7 @@ void read_defaults() {

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());
assertEquals(List.of("openid", "urn:telematik:versicherter"), config.federation().scopes());

assertNotNull(config.federation().entitySigningKey());
assertNotNull(config.federation().entitySigningKeys().getKeyByKeyId("test-sig"));
Expand Down