-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
47dedac
commit 1461f98
Showing
17 changed files
with
549 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ target/ | |
gesundheitsid/env.properties | ||
*.iml | ||
gesundheitsid/dependency-reduced-pom.xml | ||
*_jwks.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[![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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
GEMATIK_AUTH_HEADER= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
gesundheitsid/src/main/java/com/oviva/gesundheitsid/auth/AuthExceptions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.oviva.gesundheitsid.auth; | ||
|
||
public class AuthExceptions { | ||
private AuthExceptions() {} | ||
|
||
public static RuntimeException invalidParRequestUri(String uri) { | ||
return new RuntimeException("invalid par request_uri '%s'".formatted(uri)); | ||
} | ||
|
||
public static RuntimeException missingAuthorizationUrl(String sub) { | ||
return new RuntimeException( | ||
"entity statement of '%s' has no authorization url configuration".formatted(sub)); | ||
} | ||
|
||
public static RuntimeException missingPARUrl(String sub) { | ||
return new RuntimeException( | ||
"entity statement of '%s' has no pushed authorization request configuration" | ||
.formatted(sub)); | ||
} | ||
|
||
public static RuntimeException missingOpenIdConfigurationInEntityStatement(String sub) { | ||
return new RuntimeException( | ||
"entity statement of '%s' lacks openid configuration".formatted(sub)); | ||
} | ||
|
||
public static RuntimeException noEntityConfiguration() { | ||
return new RuntimeException("no entity configuration for idp available"); | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
gesundheitsid/src/main/java/com/oviva/gesundheitsid/auth/AuthenticationFlow.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package com.oviva.gesundheitsid.auth; | ||
|
||
import com.oviva.gesundheitsid.auth.steps.SelectSectoralIdpStep; | ||
import com.oviva.gesundheitsid.crypto.KeySupplier; | ||
import com.oviva.gesundheitsid.fedclient.FederationMasterClient; | ||
import com.oviva.gesundheitsid.fedclient.api.OpenIdClient; | ||
import java.net.URI; | ||
import java.util.List; | ||
|
||
public class AuthenticationFlow { | ||
|
||
private final URI selfIssuer; | ||
private final FederationMasterClient federationMasterClient; | ||
|
||
private final OpenIdClient openIdClient; | ||
|
||
private final KeySupplier relyingPartyKeySupplier; | ||
|
||
public AuthenticationFlow( | ||
URI selfIssuer, | ||
FederationMasterClient federationMasterClient, | ||
OpenIdClient openIdClient, | ||
KeySupplier relyingPartyKeySupplier) { | ||
this.selfIssuer = selfIssuer; | ||
this.federationMasterClient = federationMasterClient; | ||
this.openIdClient = openIdClient; | ||
this.relyingPartyKeySupplier = relyingPartyKeySupplier; | ||
} | ||
|
||
public SelectSectoralIdpStep start(Session session) { | ||
|
||
return new SelectSectoralIdpStep( | ||
selfIssuer, | ||
federationMasterClient, | ||
openIdClient, | ||
relyingPartyKeySupplier, | ||
session.callbackUri(), | ||
session.nonce(), | ||
session.codeChallengeS256(), | ||
session.state(), | ||
session.scopes()); | ||
} | ||
|
||
public record Session( | ||
String state, String nonce, URI callbackUri, String codeChallengeS256, List<String> scopes) {} | ||
} |
27 changes: 27 additions & 0 deletions
27
gesundheitsid/src/main/java/com/oviva/gesundheitsid/auth/IdTokenJWS.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.oviva.gesundheitsid.auth; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.nimbusds.jose.JWSObject; | ||
|
||
public record IdTokenJWS(JWSObject jws, Payload payload) { | ||
|
||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public record Payload( | ||
@JsonProperty("iss") String iss, | ||
@JsonProperty("sub") String sub, | ||
@JsonProperty("aud") String aud, | ||
@JsonProperty("iat") long iat, | ||
@JsonProperty("exp") long exp, | ||
@JsonProperty("nbf") long nbf, | ||
@JsonProperty("nonce") String nonce, | ||
@JsonProperty("acr") String acr, | ||
@JsonProperty("amr") String amr, | ||
@JsonProperty("email") String email, | ||
@JsonProperty("urn:telematik:claims:profession") String telematikProfession, | ||
@JsonProperty("urn:telematik:claims:given_name") String telematikGivenName, | ||
|
||
/** vor insured person (IP) the immutable part of the Krankenversichertennummer (KVNR) * */ | ||
@JsonProperty("urn:telematik:claims:id") String telematikKvnr, | ||
@JsonProperty("urn:telematik:claims:email") String telematikEmail) {} | ||
} |
133 changes: 133 additions & 0 deletions
133
gesundheitsid/src/main/java/com/oviva/gesundheitsid/auth/steps/SelectSectoralIdpStep.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package com.oviva.gesundheitsid.auth.steps; | ||
|
||
import com.oviva.gesundheitsid.auth.AuthExceptions; | ||
import com.oviva.gesundheitsid.crypto.KeySupplier; | ||
import com.oviva.gesundheitsid.fedclient.FederationMasterClient; | ||
import com.oviva.gesundheitsid.fedclient.IdpEntry; | ||
import com.oviva.gesundheitsid.fedclient.api.EntityStatement; | ||
import com.oviva.gesundheitsid.fedclient.api.EntityStatement.OpenidProvider; | ||
import com.oviva.gesundheitsid.fedclient.api.OpenIdClient; | ||
import com.oviva.gesundheitsid.fedclient.api.OpenIdClient.ParResponse; | ||
import com.oviva.gesundheitsid.fedclient.api.ParBodyBuilder; | ||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import jakarta.ws.rs.core.UriBuilder; | ||
import java.net.URI; | ||
import java.util.List; | ||
|
||
/** | ||
* Official documentation: - | ||
* https://wiki.gematik.de/display/IDPKB/App-App+Flow#AppAppFlow-0-FederationMaster | ||
*/ | ||
public class SelectSectoralIdpStep { | ||
|
||
private final URI selfIssuer; | ||
private final FederationMasterClient fedMasterClient; | ||
private final OpenIdClient openIdClient; | ||
private final KeySupplier relyingPartyEncKeySupplier; | ||
|
||
private final URI callbackUri; | ||
private final String nonce; | ||
private final String codeChallengeS256; | ||
private final String state; | ||
private final List<String> scopes; | ||
|
||
public SelectSectoralIdpStep( | ||
URI selfIssuer, | ||
FederationMasterClient fedMasterClient, | ||
OpenIdClient openIdClient, | ||
KeySupplier relyingPartyEncKeySupplier1, | ||
URI callbackUri, | ||
String nonce, | ||
String codeChallengeS256, | ||
String state, | ||
List<String> scopes) { | ||
this.selfIssuer = selfIssuer; | ||
this.fedMasterClient = fedMasterClient; | ||
this.openIdClient = openIdClient; | ||
this.relyingPartyEncKeySupplier = relyingPartyEncKeySupplier1; | ||
this.callbackUri = callbackUri; | ||
this.nonce = nonce; | ||
this.codeChallengeS256 = codeChallengeS256; | ||
this.state = state; | ||
this.scopes = scopes; | ||
} | ||
|
||
public List<IdpEntry> fetchIdpOptions() { | ||
return fedMasterClient.listAvailableIdps(); | ||
} | ||
|
||
public TrustedSectoralIdpStep redirectToSectoralIdp(@NonNull String sectoralIdpIss) { | ||
|
||
var trustedIdpEntityStatement = fedMasterClient.establishIdpTrust(URI.create(sectoralIdpIss)); | ||
|
||
// start PAR with sectoral IdP | ||
// https://datatracker.ietf.org/doc/html/rfc9126 | ||
|
||
var parBody = | ||
ParBodyBuilder.create() | ||
.clientId(selfIssuer.toString()) | ||
.codeChallenge(codeChallengeS256) | ||
.codeChallengeMethod("S256") | ||
.redirectUri(callbackUri) | ||
.nonce(nonce) | ||
.state(state) | ||
.scopes(scopes) | ||
.acrValues("gematik-ehealth-loa-high") | ||
.responseType("code"); | ||
|
||
var res = doPushedAuthorizationRequest(parBody, trustedIdpEntityStatement.body()); | ||
|
||
var redirectUri = buildAuthorizationUrl(res.requestUri(), trustedIdpEntityStatement.body()); | ||
|
||
return new TrustedSectoralIdpStep( | ||
openIdClient, | ||
selfIssuer, | ||
redirectUri, | ||
callbackUri, | ||
trustedIdpEntityStatement, | ||
relyingPartyEncKeySupplier); | ||
} | ||
|
||
private URI buildAuthorizationUrl(String parRequestUri, EntityStatement trustedEntityStatement) { | ||
|
||
if (parRequestUri == null || parRequestUri.isBlank()) { | ||
throw AuthExceptions.invalidParRequestUri(parRequestUri); | ||
} | ||
|
||
var openidConfig = getIdpOpenIdProvider(trustedEntityStatement); | ||
var authzEndpoint = openidConfig.authorizationEndpoint(); | ||
|
||
if (authzEndpoint == null || authzEndpoint.isBlank()) { | ||
throw AuthExceptions.missingAuthorizationUrl(trustedEntityStatement.sub()); | ||
} | ||
|
||
return UriBuilder.fromUri(authzEndpoint) | ||
.queryParam("request_uri", parRequestUri) | ||
.queryParam("client_id", selfIssuer.toString()) | ||
.build(); | ||
} | ||
|
||
private ParResponse doPushedAuthorizationRequest( | ||
ParBodyBuilder builder, EntityStatement trustedEntityStatement) { | ||
|
||
var openidConfig = getIdpOpenIdProvider(trustedEntityStatement); | ||
var parEndpoint = openidConfig.pushedAuthorizationRequestEndpoint(); | ||
if (parEndpoint == null || parEndpoint.isBlank()) { | ||
throw AuthExceptions.missingPARUrl(trustedEntityStatement.sub()); | ||
} | ||
|
||
return openIdClient.requestPushedUri(URI.create(parEndpoint), builder); | ||
} | ||
|
||
private OpenidProvider getIdpOpenIdProvider( | ||
@NonNull EntityStatement trustedIdpEntityConfiguration) { | ||
|
||
if (trustedIdpEntityConfiguration.metadata() == null | ||
|| trustedIdpEntityConfiguration.metadata().openidProvider() == null) { | ||
throw AuthExceptions.missingOpenIdConfigurationInEntityStatement( | ||
trustedIdpEntityConfiguration.sub()); | ||
} | ||
|
||
return trustedIdpEntityConfiguration.metadata().openidProvider(); | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
gesundheitsid/src/main/java/com/oviva/gesundheitsid/auth/steps/TrustedSectoralIdpStep.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package com.oviva.gesundheitsid.auth.steps; | ||
|
||
import com.nimbusds.jose.JOSEException; | ||
import com.nimbusds.jose.JWEObject; | ||
import com.nimbusds.jose.crypto.MultiDecrypter; | ||
import com.oviva.gesundheitsid.auth.IdTokenJWS; | ||
import com.oviva.gesundheitsid.auth.IdTokenJWS.Payload; | ||
import com.oviva.gesundheitsid.crypto.JwsVerifier; | ||
import com.oviva.gesundheitsid.crypto.KeySupplier; | ||
import com.oviva.gesundheitsid.fedclient.api.EntityStatementJWS; | ||
import com.oviva.gesundheitsid.fedclient.api.OpenIdClient; | ||
import com.oviva.gesundheitsid.util.JsonCodec; | ||
import com.oviva.gesundheitsid.util.JsonPayloadTransformer; | ||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import java.net.URI; | ||
import java.text.ParseException; | ||
|
||
public class TrustedSectoralIdpStep { | ||
|
||
private final OpenIdClient openIdClient; | ||
|
||
private final URI selfIssuer; | ||
private final URI idpRedirectUri; | ||
private final URI callbackUri; | ||
private final EntityStatementJWS trustedIdpEntityStatement; | ||
private final KeySupplier relyingPartyEncKeySupplier; | ||
|
||
public TrustedSectoralIdpStep( | ||
OpenIdClient openIdClient, | ||
URI selfIssuer, | ||
URI idpRedirectUri, | ||
URI callbackUri, | ||
EntityStatementJWS trustedIdpEntityStatement, | ||
KeySupplier relyingPartyEncKeySupplier) { | ||
this.openIdClient = openIdClient; | ||
this.selfIssuer = selfIssuer; | ||
this.idpRedirectUri = idpRedirectUri; | ||
this.callbackUri = callbackUri; | ||
this.trustedIdpEntityStatement = trustedIdpEntityStatement; | ||
this.relyingPartyEncKeySupplier = relyingPartyEncKeySupplier; | ||
} | ||
|
||
public URI idpRedirectUri() { | ||
return idpRedirectUri; | ||
} | ||
|
||
public IdTokenJWS exchangeSectoralIdpCode(@NonNull String code, @NonNull String codeVerifier) { | ||
|
||
if (trustedIdpEntityStatement == null) { | ||
throw new IllegalStateException("flow has no trusted IDP statement, state not persisted?"); | ||
} | ||
|
||
if (callbackUri == null) { | ||
throw new IllegalStateException("flow has no callback_uri, state not persisted?"); | ||
} | ||
|
||
var tokenEndpoint = | ||
trustedIdpEntityStatement.body().metadata().openidProvider().tokenEndpoint(); | ||
var res = | ||
openIdClient.exchangePkceCode( | ||
URI.create(tokenEndpoint), | ||
code, | ||
callbackUri.toString(), | ||
selfIssuer.toString(), | ||
codeVerifier); | ||
|
||
try { | ||
var jweObject = JWEObject.parse(res.idToken()); | ||
// var decrypter = new ECDHDecrypter(relyingPartyEncKeySupplier.get().priv()); | ||
var decrypter = | ||
new MultiDecrypter(relyingPartyEncKeySupplier.apply(jweObject.getHeader().getKeyID())); | ||
jweObject.decrypt(decrypter); | ||
|
||
var signedJws = jweObject.getPayload().toJWSObject(); | ||
|
||
if (!JwsVerifier.verify(trustedIdpEntityStatement.body().jwks(), signedJws)) { | ||
throw new RuntimeException("bad signature from IDP on id token"); | ||
} | ||
|
||
var payload = | ||
signedJws | ||
.getPayload() | ||
.toType(new JsonPayloadTransformer<>(Payload.class, JsonCodec::readValue)); | ||
return new IdTokenJWS(signedJws, payload); | ||
|
||
} catch (JOSEException | ParseException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
gesundheitsid/src/main/java/com/oviva/gesundheitsid/crypto/ECKeyPair.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.oviva.gesundheitsid.crypto; | ||
|
||
import java.security.interfaces.ECPrivateKey; | ||
import java.security.interfaces.ECPublicKey; | ||
|
||
public record ECKeyPair(ECPublicKey pub, ECPrivateKey priv) {} |
6 changes: 6 additions & 0 deletions
6
gesundheitsid/src/main/java/com/oviva/gesundheitsid/crypto/KeySupplier.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.oviva.gesundheitsid.crypto; | ||
|
||
import com.nimbusds.jose.jwk.JWK; | ||
import java.util.function.Function; | ||
|
||
public interface KeySupplier extends Function<String, JWK> {} |
Oops, something went wrong.