-
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.
ARC-1217: Include fully working authentication flow.
- Loading branch information
1 parent
1461f98
commit 8751c92
Showing
13 changed files
with
762 additions
and
379 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
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
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
138 changes: 138 additions & 0 deletions
138
.../src/main/java/com/oviva/gesundheitsid/auth/internal/steps/SelectSectoralIdpStepImpl.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,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(); | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
...src/main/java/com/oviva/gesundheitsid/auth/internal/steps/TrustedSectoralIdpStepImpl.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,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); | ||
} | ||
} | ||
} |
Oops, something went wrong.