Skip to content

Commit

Permalink
ARC-1217: Include fully working authentication flow.
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed Jan 31, 2024
1 parent 1461f98 commit 8751c92
Show file tree
Hide file tree
Showing 13 changed files with 762 additions and 379 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ public static RuntimeException missingOpenIdConfigurationInEntityStatement(Strin
"entity statement of '%s' lacks openid configuration".formatted(sub));
}

public static RuntimeException noEntityConfiguration() {
return new RuntimeException("no entity configuration for idp available");
public static RuntimeException badIdTokenSignature(String issuer) {
return new RuntimeException("bad ID token signature from sub=%s".formatted(issuer));
}

public static RuntimeException badIdToken(String issuer, Exception cause) {
return new RuntimeException("bad ID token from sub=%s".formatted(issuer), cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.oviva.gesundheitsid.auth;

import com.oviva.gesundheitsid.auth.internal.steps.SelectSectoralIdpStepImpl;
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 edu.umd.cs.findbugs.annotations.NonNull;
import java.net.URI;
import java.util.List;

Expand All @@ -17,19 +19,20 @@ public class AuthenticationFlow {
private final KeySupplier relyingPartyKeySupplier;

public AuthenticationFlow(
URI selfIssuer,
FederationMasterClient federationMasterClient,
OpenIdClient openIdClient,
KeySupplier relyingPartyKeySupplier) {
@NonNull URI selfIssuer,
@NonNull FederationMasterClient federationMasterClient,
@NonNull OpenIdClient openIdClient,
@NonNull KeySupplier relyingPartyKeySupplier) {
this.selfIssuer = selfIssuer;
this.federationMasterClient = federationMasterClient;
this.openIdClient = openIdClient;
this.relyingPartyKeySupplier = relyingPartyKeySupplier;
}

public SelectSectoralIdpStep start(Session session) {
@NonNull
public SelectSectoralIdpStep start(@NonNull Session session) {

return new SelectSectoralIdpStep(
return new SelectSectoralIdpStepImpl(
selfIssuer,
federationMasterClient,
openIdClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.nimbusds.jose.JWSObject;

public record IdTokenJWS(JWSObject jws, Payload payload) {
public record IdTokenJWS(JWSObject jws, IdToken body) {

@JsonIgnoreProperties(ignoreUnknown = true)
public record Payload(
public record IdToken(
@JsonProperty("iss") String iss,
@JsonProperty("sub") String sub,
@JsonProperty("aud") String aud,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.oviva.gesundheitsid.auth.internal.steps;

import com.oviva.gesundheitsid.auth.AuthExceptions;
import com.oviva.gesundheitsid.auth.steps.SelectSectoralIdpStep;
import com.oviva.gesundheitsid.auth.steps.TrustedSectoralIdpStep;
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 SelectSectoralIdpStepImpl implements 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 SelectSectoralIdpStepImpl(
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;
}

@NonNull
@Override
public List<IdpEntry> fetchIdpOptions() {
return fedMasterClient.listAvailableIdps();
}

@Override
public @NonNull 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 TrustedSectoralIdpStepImpl(
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.oviva.gesundheitsid.auth.internal.steps;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.crypto.MultiDecrypter;
import com.oviva.gesundheitsid.auth.AuthExceptions;
import com.oviva.gesundheitsid.auth.IdTokenJWS;
import com.oviva.gesundheitsid.auth.IdTokenJWS.IdToken;
import com.oviva.gesundheitsid.auth.steps.TrustedSectoralIdpStep;
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 TrustedSectoralIdpStepImpl implements 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 TrustedSectoralIdpStepImpl(
@NonNull OpenIdClient openIdClient,
@NonNull URI selfIssuer,
@NonNull URI idpRedirectUri,
@NonNull URI callbackUri,
@NonNull EntityStatementJWS trustedIdpEntityStatement,
@NonNull KeySupplier relyingPartyEncKeySupplier) {
this.openIdClient = openIdClient;
this.selfIssuer = selfIssuer;
this.idpRedirectUri = idpRedirectUri;
this.callbackUri = callbackUri;
this.trustedIdpEntityStatement = trustedIdpEntityStatement;
this.relyingPartyEncKeySupplier = relyingPartyEncKeySupplier;
}

@Override
public @NonNull URI idpRedirectUri() {
return idpRedirectUri;
}

@NonNull
@Override
public IdTokenJWS exchangeSectoralIdpCode(@NonNull String code, @NonNull String codeVerifier) {

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 MultiDecrypter(relyingPartyEncKeySupplier.apply(jweObject.getHeader().getKeyID()));
jweObject.decrypt(decrypter);

var signedJws = jweObject.getPayload().toJWSObject();

if (!JwsVerifier.verify(trustedIdpEntityStatement.body().jwks(), signedJws)) {
throw AuthExceptions.badIdTokenSignature(trustedIdpEntityStatement.body().sub());
}

var payload =
signedJws
.getPayload()
.toType(new JsonPayloadTransformer<>(IdToken.class, JsonCodec::readValue));
return new IdTokenJWS(signedJws, payload);

} catch (JOSEException | ParseException e) {
throw AuthExceptions.badIdToken(trustedIdpEntityStatement.body().sub(), e);
}
}
}
Loading

0 comments on commit 8751c92

Please sign in to comment.