diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 7cba523..6e2deff 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,13 @@ +# Release 7.0.4 + +- add documentation for key rotation +- allow multiple tls certs in entity statement for key rotation +- refactoring of EntityStatementRpService into separate services +- add test identities +- add validation for optional claims parameter in par endpoint +- update configuration: mTLS at par endpoint now required for gsi.dev and gsi-ref.dev +- update dependencies + # Release 7.0.3 - skip jacoco by default diff --git a/gsi-coverage-report/pom.xml b/gsi-coverage-report/pom.xml index 4218caa..bb6d637 100644 --- a/gsi-coverage-report/pom.xml +++ b/gsi-coverage-report/pom.xml @@ -7,7 +7,7 @@ de.gematik.idp gemSekIdp-global - 7.0.3 + 7.0.4 ../pom.xml diff --git a/gsi-fedmaster/pom.xml b/gsi-fedmaster/pom.xml index c6189db..ca9b32d 100644 --- a/gsi-fedmaster/pom.xml +++ b/gsi-fedmaster/pom.xml @@ -7,12 +7,12 @@ de.gematik.idp gemSekIdp-global - 7.0.3 + 7.0.4 ../pom.xml gsi-fedmaster - 7.0.3 + 7.0.4 jar gsi-fedmaster @@ -95,7 +95,7 @@ org.jmdns jmdns - 3.5.9 + 3.5.12 diff --git a/gsi-server/pom.xml b/gsi-server/pom.xml index 88f83e8..f5f2142 100644 --- a/gsi-server/pom.xml +++ b/gsi-server/pom.xml @@ -7,12 +7,12 @@ de.gematik.idp gemSekIdp-global - 7.0.3 + 7.0.4 ../pom.xml gsi-server - 7.0.3 + 7.0.4 jar gsi-server @@ -60,7 +60,7 @@ org.mockito mockito-core - 5.12.0 + 5.14.2 test @@ -92,11 +92,10 @@ com.konghq unirest-java-core - - org.apache.httpcomponents.client5 - httpclient5 - ${version.httpclient} + org.apache.httpcomponents.core5 + httpcore5 + ${version.httpcore5} @@ -138,7 +137,13 @@ org.jmdns jmdns - 3.5.9 + 3.5.12 + + + org.mockito + mockito-inline + 5.2.0 + test diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/GsiServer.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/GsiServer.java index bdb7066..24e4cbf 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/GsiServer.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/GsiServer.java @@ -78,6 +78,8 @@ public void setGsiLogLevel() { final String loggerRequests = "org.springframework.web.filter.CommonsRequestLoggingFilter"; Configurator.setLevel(loggerServer, loglevel); Configurator.setLevel(loggerRequests, loglevel); + log.info("GSI_CLIENT_CERT_REQUIRED in env: " + System.getenv("GSI_CLIENT_CERT_REQUIRED")); + log.info("isClientCertRequired in config: " + gsiConfiguration.isClientCertRequired()); log.info("gsiConfiguration: {}", gsiConfiguration); final LoggerContext loggerContext = diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/configuration/GsiConfiguration.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/configuration/GsiConfiguration.java index 85cb55a..f53cd1d 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/configuration/GsiConfiguration.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/configuration/GsiConfiguration.java @@ -46,5 +46,5 @@ public class GsiConfiguration { private KeyConfig tokenSigPubKeyConfig; private String loglevel; private Integer requestUriTTL; - private boolean isRequiredClientCert; + private boolean clientCertRequired; } diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/controller/FedIdpController.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/controller/FedIdpController.java index 8601f59..5303b89 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/controller/FedIdpController.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/controller/FedIdpController.java @@ -21,33 +21,26 @@ import static de.gematik.idp.IdpConstants.FED_AUTH_ENDPOINT; import static de.gematik.idp.IdpConstants.TOKEN_ENDPOINT; import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; -import static de.gematik.idp.data.Oauth2ErrorCode.UNAUTHORIZED_CLIENT; -import static de.gematik.idp.gsi.server.data.GsiConstants.FEDIDP_PAR_AUTH_ENDPOINT; -import static de.gematik.idp.gsi.server.data.GsiConstants.FED_SIGNED_JWKS_ENDPOINT; -import static de.gematik.idp.gsi.server.data.GsiConstants.TLS_CLIENT_CERT_HEADER_NAME; +import static de.gematik.idp.gsi.server.data.GsiConstants.*; import static de.gematik.idp.gsi.server.util.ClaimHelper.getClaimsForScopeSet; import com.fasterxml.jackson.databind.ObjectMapper; import de.gematik.idp.authentication.IdpJwtProcessor; -import de.gematik.idp.crypto.CryptoLoader; import de.gematik.idp.crypto.Nonce; -import de.gematik.idp.crypto.exceptions.IdpCryptoException; import de.gematik.idp.data.FederationPrivKey; import de.gematik.idp.data.JwtHelper; import de.gematik.idp.data.ParResponse; import de.gematik.idp.data.TokenResponse; -import de.gematik.idp.field.ClientUtilities; import de.gematik.idp.gsi.server.configuration.GsiConfiguration; -import de.gematik.idp.gsi.server.data.ClaimsResponse; -import de.gematik.idp.gsi.server.data.FedIdpAuthSession; -import de.gematik.idp.gsi.server.data.QRCodeGenerator; +import de.gematik.idp.gsi.server.data.*; import de.gematik.idp.gsi.server.exceptions.GsiException; import de.gematik.idp.gsi.server.services.AuthenticationService; import de.gematik.idp.gsi.server.services.EntityStatementBuilder; -import de.gematik.idp.gsi.server.services.EntityStatementRpService; import de.gematik.idp.gsi.server.services.JwksBuilder; +import de.gematik.idp.gsi.server.services.RequestValidator; import de.gematik.idp.gsi.server.services.SektoralIdpAuthenticator; import de.gematik.idp.gsi.server.services.ServerUrlService; +import de.gematik.idp.gsi.server.services.TokenRepositoryRp; import de.gematik.idp.gsi.server.token.IdTokenBuilder; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; @@ -55,18 +48,12 @@ import java.io.Serial; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jose4j.lang.JoseException; @@ -94,7 +81,7 @@ public class FedIdpController { private static final int MAX_AUTH_SESSION_AMOUNT = 10000; public static final int ID_TOKEN_TTL_SECONDS = 300; - private final EntityStatementRpService entityStatementRpService; + private final TokenRepositoryRp rpTokenRepository; private final EntityStatementBuilder entityStatementBuilder; private final SektoralIdpAuthenticator sektoralIdpAuthenticator; private final AuthenticationService authenticationService; @@ -182,16 +169,24 @@ public ParResponse postPar( regexp = "urn:telematik:auth:eGK|urn:telematik:auth:eID|urn:telematik:auth:sso|urn:telematik:auth:mEW|urn:telematik:auth:guest:eGK|urn:telematik:auth:other") final String amr, - @RequestParam(name = "claims", required = false) final String claims, + @RequestParam(name = "claims", defaultValue = "") ClaimsInfo claimsInfo, @RequestHeader(name = TLS_CLIENT_CERT_HEADER_NAME, required = false) final String clientCert, final HttpServletResponse respMsgNr3) { + log.info( "App2App-Flow: RX message nr 2 (Pushed Authorization Request) received at {}", serverUrlService.determineServerUrl()); - validateCertificate(clientCert, fachdienstClientId); + claimsInfo.addClaimsFromScopeToClaimsSet( + getClaimsForScopeSet(Arrays.stream(scope.split(" ")).collect(Collectors.toSet()))); + + final RpToken entityStmntRp = rpTokenRepository.getEntityStatementRp(fachdienstClientId); + log.info("Autoregistration done"); - entityStatementRpService.doAutoregistration(fachdienstClientId, fachdienstRedirectUri, scope); + RequestValidator.validateCertificate( + clientCert, entityStmntRp, gsiConfiguration.isClientCertRequired()); + + RequestValidator.validateParParams(entityStmntRp, fachdienstRedirectUri, scope); log.info("Amount of stored fedIdpAuthSessions: {}", fedIdpAuthSessions.size()); @@ -208,7 +203,10 @@ public ParResponse postPar( .fachdienstCodeChallenge(fachdienstCodeChallenge) .fachdienstCodeChallengeMethod(fachdienstCodeChallengeMethod) .fachdienstNonce(fachdienstNonce) - .requestedScopes(Arrays.stream(scope.split(" ")).collect(Collectors.toSet())) + .requestedOptionalClaims(claimsInfo.getOptionalClaims()) + .requestedEssentialClaims(claimsInfo.getEssentialClaims()) + .essentialRequestedAcr(claimsInfo.getAcrValues()) + .essentialRequestedAmr(claimsInfo.getAmrValues()) .fachdienstRedirectUri(fachdienstRedirectUri) .authorizationCode(Nonce.getNonceAsHex(AUTH_CODE_LENGTH)) .expiresAt( @@ -242,7 +240,7 @@ public String getLandingPage( final Model model) { final String thisEndpointUrl = serverUrlService.determineServerUrl() + FED_AUTH_ENDPOINT; log.info("App2App-Flow: RX message nr 6 (Authorization Request) at {}", thisEndpointUrl); - validateAuthRequestParams(requestUri, clientId); + RequestValidator.validateAuthRequestParams(getSessionByRequestUri(requestUri), clientId); log.info("request_uri: {}, client_id: {}", requestUri, clientId); model.addAttribute("requestUri", requestUri); @@ -263,14 +261,20 @@ public ClaimsResponse getRequestedClaims( final HttpServletResponse respMsgNr6a) { final String thisEndpointUrl = serverUrlService.determineServerUrl() + FED_AUTH_ENDPOINT; log.info( - "App2App-Flow: RX message nr 6 (Authorization Request, getRequetedClaims) at {}", + "App2App-Flow: RX message nr 6 (Authorization Request, getRequestedClaims) at {}", thisEndpointUrl); final FedIdpAuthSession session = getSessionByRequestUri(requestUri); - final Set requestedScopes = session.getRequestedScopes(); - final Set requestedClaims = getClaimsForScopeSet(requestedScopes); + respMsgNr6a.setStatus(HttpStatus.OK.value()); - return ClaimsResponse.builder().requestedClaims(requestedClaims.toArray(new String[0])).build(); + return ClaimsResponse.builder() + .requestedClaims( + Stream.concat( + session.getRequestedOptionalClaims().stream(), + session.getRequestedEssentialClaims().stream()) + .distinct() + .toArray(String[]::new)) + .build(); } @ResponseBody @@ -289,8 +293,11 @@ public void getAuthorizationCode( serverUrlService.determineServerUrl()); final FedIdpAuthSession session = getSessionByRequestUri(requestUri); - final Set requestedScopes = session.getRequestedScopes(); - final Set requestedClaims = getClaimsForScopeSet(requestedScopes); + final Set requestedClaims = + Stream.concat( + session.getRequestedOptionalClaims().stream(), + session.getRequestedEssentialClaims().stream()) + .collect(Collectors.toSet()); final Set selectedClaimsSet; selectedClaimsSet = getSelectedClaimsSet(selectedClaims, requestedClaims); @@ -332,15 +339,18 @@ public TokenResponse getTokensForCode( "App2App-Flow: RX message nr 10 (Authorization Code) at {}", serverUrlService.determineServerUrl()); - validateCertificate(clientCert, clientId); - final String sessionKey = getSessionKeyByAuthCode(URLDecoder.decode(code, StandardCharsets.UTF_8)); final FedIdpAuthSession session = fedIdpAuthSessions.get(sessionKey); - verifyRedirectUri(redirectUri, session.getFachdienstRedirectUri()); - verifyCodeVerifier(codeVerifier, session.getFachdienstCodeChallenge()); - verifyClientId(clientId, session.getFachdienstClientId()); + RequestValidator.verifyRedirectUri(redirectUri, session.getFachdienstRedirectUri()); + RequestValidator.verifyCodeVerifier(codeVerifier, session.getFachdienstCodeChallenge()); + RequestValidator.verifyClientId(clientId, session.getFachdienstClientId()); + + final RpToken token = rpTokenRepository.getEntityStatementRp(clientId); + + RequestValidator.validateCertificate( + clientCert, token, gsiConfiguration.isClientCertRequired()); setNoCacheHeader(respMsgNr11); respMsgNr11.setStatus(HttpStatus.OK.value()); @@ -355,7 +365,7 @@ public TokenResponse getTokensForCode( clientId, session.getUserData()) .buildIdToken() - .encryptAsJwt(entityStatementRpService.getRpEncKey(clientId)) + .encryptAsJwt(token.getRpEncKey()) .getRawString(); } catch (final JoseException e) { throw new GsiException(e); @@ -371,24 +381,6 @@ public TokenResponse getTokensForCode( .build(); } - private static void verifyRedirectUri(final String redirectUri, final String sessionRedirectUri) { - if (!redirectUri.equals(sessionRedirectUri)) { - throw new GsiException(INVALID_REQUEST, "invalid redirect_uri", HttpStatus.BAD_REQUEST); - } - } - - private static void verifyCodeVerifier(final String codeVerifier, final String codeChallenge) { - if (!ClientUtilities.generateCodeChallenge(codeVerifier).equals(codeChallenge)) { - throw new GsiException(INVALID_REQUEST, "invalid code_verifier", HttpStatus.BAD_REQUEST); - } - } - - private static void verifyClientId(final String clientId, final String sessionClientId) { - if (!sessionClientId.equals(clientId)) { - throw new GsiException(INVALID_REQUEST, "invalid client_id", HttpStatus.BAD_REQUEST); - } - } - private FedIdpAuthSession getSessionByRequestUri(final String requestUri) { final FedIdpAuthSession session = Optional.ofNullable(fedIdpAuthSessions.get(requestUri)) @@ -420,14 +412,6 @@ private String getSessionKeyByAuthCode(final String authorizationCode) { INVALID_REQUEST, "unknown code, no session found", HttpStatus.BAD_REQUEST)); } - private void validateAuthRequestParams(final String requestUri, final String clientId) { - final FedIdpAuthSession session = getSessionByRequestUri(requestUri); - final boolean clientIdBelongsToRequestUri = session.getFachdienstClientId().equals(clientId); - if (!clientIdBelongsToRequestUri) { - throw new GsiException(INVALID_REQUEST, "unknown client_id", HttpStatus.BAD_REQUEST); - } - } - private static Set getSelectedClaimsSet( final String selectedClaims, final Set requestedClaims) { final Set selectedClaimsSet; @@ -442,35 +426,4 @@ private static Set getSelectedClaimsSet( } return selectedClaimsSet; } - - private void validateCertificate(final String clientCert, final String clientId) { - if (clientCert == null) { - if (gsiConfiguration.isRequiredClientCert()) { - throw new GsiException( - INVALID_REQUEST, "client certificate is missing", HttpStatus.BAD_REQUEST); - } - } else { - try { - final X509Certificate certFromRequest = - CryptoLoader.getCertificateFromPem( - java.net.URLDecoder.decode(clientCert, StandardCharsets.UTF_8).getBytes()); - final X509Certificate certFromEntityStatement = - entityStatementRpService.getRpTlsClientCert(clientId); - if (!certFromRequest.equals(certFromEntityStatement)) { - throw new GsiException( - UNAUTHORIZED_CLIENT, - "client certificate in tls handshake does not match certificate in entity" - + " statement/signed_jwks", - HttpStatus.UNAUTHORIZED); - } - } catch (final IdpCryptoException e) { - throw new GsiException( - UNAUTHORIZED_CLIENT, - "client certificate in tls handshake is not a valid x509 certificate", - HttpStatus.UNAUTHORIZED); - } catch (final JoseException e) { - throw new RuntimeException(); - } - } - } } diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/ClaimsInfo.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/ClaimsInfo.java new file mode 100644 index 0000000..a1875a0 --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/ClaimsInfo.java @@ -0,0 +1,118 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.data; + +import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; +import static de.gematik.idp.gsi.server.data.GsiConstants.VALID_CLAIMS; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.gsi.server.services.RequestValidator; +import java.util.HashSet; +import java.util.Set; +import lombok.*; +import org.springframework.http.HttpStatus; + +@Getter +@NoArgsConstructor +public class ClaimsInfo { + private final Set amrValues = new HashSet<>(); + private final Set acrValues = new HashSet<>(); + private final Set essentialClaims = new HashSet<>(); + private final Set optionalClaims = new HashSet<>(); + + public ClaimsInfo(final String claims) { + if (claims == null || claims.isEmpty()) return; + try { + final JsonObject claimsForIdToken = + JsonParser.parseString(claims).getAsJsonObject().getAsJsonObject("id_token"); + if (claimsForIdToken == null) { + throw new GsiException( + INVALID_REQUEST, "parameter claims has invalid structure", HttpStatus.BAD_REQUEST); + } + setAmrAcrSets(claimsForIdToken); + setClaimSets(claimsForIdToken); + } catch (JsonSyntaxException | IllegalStateException | NullPointerException e) { + throw new GsiException( + INVALID_REQUEST, "parameter claims is not a JSON object", HttpStatus.BAD_REQUEST); + } + } + + public void addClaimsFromScopeToClaimsSet(final Set claimSetFromScope) { + claimSetFromScope.forEach( + claim -> { + if (!essentialClaims.contains(claim)) { + optionalClaims.add(claim); + } + }); + } + + private void setAmrAcrSets(final JsonObject claimsForIdToken) { + setValueIfEssential(claimsForIdToken.get("amr"), amrValues); + setValueIfEssential(claimsForIdToken.get("acr"), acrValues); + RequestValidator.validateAmrAcrCombination(acrValues, amrValues); + } + + private void setClaimSets(final JsonObject claimsForIdToken) { + claimsForIdToken.entrySet().stream() + .filter( + claimEntry -> !(claimEntry.getKey().equals("amr") || claimEntry.getKey().equals("acr"))) + .forEach( + claimEntry -> { + final String claimName = claimEntry.getKey(); + if (!VALID_CLAIMS.contains(claimName)) { + throw new GsiException( + INVALID_REQUEST, + "claim " + claimName + " is not supported", + HttpStatus.BAD_REQUEST); + } + final JsonObject subClaims = claimEntry.getValue().getAsJsonObject(); + final JsonElement claimIsEssential = subClaims.get("essential"); + final JsonElement value = subClaims.get("value"); + final JsonElement values = subClaims.get("values"); + if (value != null || values != null) { + throw new GsiException( + INVALID_REQUEST, + "claim " + claimName + " should not have value or values set", + HttpStatus.BAD_REQUEST); + } + if (claimIsEssential != null && claimIsEssential.getAsBoolean()) { + essentialClaims.add(claimName); + } else { + optionalClaims.add(claimName); + } + }); + } + + private void setValueIfEssential(final JsonElement claim, final Set claimSet) { + if (claim == null) return; + final JsonObject claimObject = claim.getAsJsonObject(); + final JsonElement isEssential = claimObject.get("essential"); + if (isEssential != null && isEssential.getAsBoolean()) { + final JsonElement value = claimObject.get("value"); + final JsonElement values = claimObject.get("values"); + if (value != null) { + claimSet.add(value.getAsString()); + } else if (values != null) { + values.getAsJsonArray().asList().forEach(v -> claimSet.add(v.getAsString())); + } + } + } +} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/FedIdpAuthSession.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/FedIdpAuthSession.java index 2c24702..7b2bdf9 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/FedIdpAuthSession.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/FedIdpAuthSession.java @@ -34,7 +34,10 @@ public class FedIdpAuthSession { private final String fachdienstCodeChallenge; private final String fachdienstCodeChallengeMethod; private final String fachdienstNonce; - private final Set requestedScopes; + private final Set requestedOptionalClaims; + private final Set requestedEssentialClaims; + private final Set essentialRequestedAcr; + private final Set essentialRequestedAmr; // will be sent in message nr.7 private final String fachdienstRedirectUri; private final String authorizationCode; @@ -55,8 +58,14 @@ public String toString() { + fachdienstCodeChallengeMethod + "\n fachdienstNonce: " + fachdienstNonce - + "\n requestedScopes: " - + requestedScopes + + "\n requestedOptionalClaims: " + + requestedOptionalClaims + + "\n requestedEssentialClaims: " + + requestedEssentialClaims + + "\n essentialRequestedAcr: " + + essentialRequestedAcr + + "\n essentialRequestedAmr: " + + essentialRequestedAmr + "\n fachdienstRedirectUri: " + fachdienstRedirectUri + "\n authorizationCode: " diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/GsiConstants.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/GsiConstants.java index 9cf0111..e54a8a4 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/GsiConstants.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/GsiConstants.java @@ -16,13 +16,28 @@ package de.gematik.idp.gsi.server.data; +import de.gematik.idp.field.ClaimName; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class GsiConstants { + public static final Set VALID_CLAIMS = + Set.of( + ClaimName.TELEMATIK_ALTER.getJoseName(), + ClaimName.TELEMATIK_DISPLAY_NAME.getJoseName(), + ClaimName.TELEMATIK_GIVEN_NAME.getJoseName(), + ClaimName.TELEMATIK_GESCHLECHT.getJoseName(), + ClaimName.TELEMATIK_EMAIL.getJoseName(), + ClaimName.TELEMATIK_PROFESSION.getJoseName(), + ClaimName.TELEMATIK_ID.getJoseName(), + ClaimName.TELEMATIK_ORGANIZATION.getJoseName(), + ClaimName.TELEMATIK_FAMILY_NAME.getJoseName()); + public static final Set SCOPES_SUPPORTED = Set.of( "urn:telematik:geburtsdatum", @@ -35,6 +50,25 @@ public final class GsiConstants { "urn:telematik:family_name", "openid"); + public static final Set AMR_VALUES_HIGH = + Set.of( + "urn:telematik:auth:eGK", + "urn:telematik:auth:eID", + "urn:telematik:auth:sso", + "urn:telematik:auth:guest:eGK", + "urn:telematik:auth:other"); + + public static final Set AMR_VALUES_SUBSTANTIAL = Set.of("urn:telematik:auth:mEW"); + + public static final Set AMR_VALUES = + Stream.concat(AMR_VALUES_HIGH.stream(), AMR_VALUES_SUBSTANTIAL.stream()) + .collect(Collectors.toSet()); + + public static final String ACR_HIGH = "gematik-ehealth-loa-high"; + public static final String ACR_SUBSTANTIAL = "gematik-ehealth-loa-substantial"; + + public static final Set ACR_VALUES = Set.of(ACR_HIGH, ACR_SUBSTANTIAL); + public static final int IDTOKEN_TTL_MINUTES = 5; public static final String FEDIDP_PAR_AUTH_ENDPOINT = "/PAR_Auth"; diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/RpToken.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/RpToken.java new file mode 100644 index 0000000..b61bb5f --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/data/RpToken.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.data; + +import de.gematik.idp.gsi.server.services.EntityStatementRpReader; +import de.gematik.idp.gsi.server.services.EntityStatementRpVerifier; +import de.gematik.idp.token.JsonWebToken; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.jwk.PublicJsonWebKey; +import org.jose4j.lang.JoseException; + +@Getter +@RequiredArgsConstructor +public class RpToken { + + private final JsonWebToken token; + + public boolean isExpired() { + return !token.getExpiresAt().isBefore(ZonedDateTime.now()); + } + + public void verify(final JsonWebKeySet jwks) { + EntityStatementRpVerifier.verifyEntityStmntRp(token, jwks); + } + + public List getRpTlsClientCertificates() { + return EntityStatementRpReader.getRpTlsClientCerts(token); + } + + public PublicJsonWebKey getRpEncKey() throws JoseException { + return EntityStatementRpReader.getRpEncKey(token); + } + + public void verifyRedirectUriExistsInEntityStmnt(final String redirectUri) { + EntityStatementRpVerifier.verifyRedirectUriExistsInEntityStmnt(token, redirectUri); + } + + public void verifyRequestedScopesListedInEntityStmnt(final String scopeParameter) { + EntityStatementRpVerifier.verifyRequestedScopesListedInEntityStmnt(token, scopeParameter); + } +} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/exceptions/handler/GsiExceptionHandler.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/exceptions/handler/GsiExceptionHandler.java index a8ac2b0..6ee3191 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/exceptions/handler/GsiExceptionHandler.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/exceptions/handler/GsiExceptionHandler.java @@ -34,6 +34,7 @@ import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @ControllerAdvice @RequiredArgsConstructor @@ -50,7 +51,8 @@ public ResponseEntity handleGsiException(final GsiException @ExceptionHandler({ ConstraintViolationException.class, ValidationException.class, - MethodArgumentNotValidException.class + MethodArgumentNotValidException.class, + MethodArgumentTypeMismatchException.class }) public ResponseEntity handleValidationException(final Exception exc) { return handleGsiException( diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpReader.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpReader.java new file mode 100644 index 0000000..89268ce --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpReader.java @@ -0,0 +1,241 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; + +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.token.JsonWebToken; +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import lombok.extern.slf4j.Slf4j; +import org.jose4j.jwk.PublicJsonWebKey; +import org.jose4j.lang.JoseException; +import org.springframework.http.HttpStatus; + +@Slf4j +public abstract class EntityStatementRpReader { + + public static List getRedirectUrisEntityStatementRp(final JsonWebToken entityStmntRp) { + final Map openidRelyingParty = getOpenidRelyingParty(entityStmntRp); + return Objects.requireNonNull( + (List) openidRelyingParty.get("redirect_uris"), "missing claim: redirect_uris"); + } + + public static List getScopesFromEntityStatementRp(final JsonWebToken entityStmntRp) { + final Map openidRelyingParty = getOpenidRelyingParty(entityStmntRp); + return Arrays.stream( + Objects.requireNonNull((String) openidRelyingParty.get("scope"), "missing claim: scope") + .split(" ")) + .toList(); + } + + public static PublicJsonWebKey getRpEncKey(final JsonWebToken entityStmntRp) { + final Optional encKeyFromEntityStatement = + EntityStatementRpReader.getRpEncKeyFromEntityStatement(entityStmntRp); + if (encKeyFromEntityStatement.isPresent()) { + return encKeyFromEntityStatement.get(); + } + final Supplier gsiExceptionSupplier = + () -> + new GsiException( + INVALID_REQUEST, + "Encryption key for relying party not found", + HttpStatus.BAD_REQUEST); + final JsonWebToken signedJwks = getSignedJwks(entityStmntRp).orElseThrow(gsiExceptionSupplier); + return getRpEncKeyFromSignedJwks(signedJwks).orElseThrow(gsiExceptionSupplier); + } + + public static List getRpTlsClientCerts(final JsonWebToken entityStmntRp) { + final Optional> tlsClientCertsFromEntityStatement = + getRpTlsClientCertsFromEntityStatement(entityStmntRp); + if (tlsClientCertsFromEntityStatement.isPresent()) { + return tlsClientCertsFromEntityStatement.get(); + } + final Supplier gsiExceptionSupplier = + () -> + new GsiException( + INVALID_REQUEST, + "No TLS client certificate for relying party found", + HttpStatus.BAD_REQUEST); + final JsonWebToken signedJwks = getSignedJwks(entityStmntRp).orElseThrow(gsiExceptionSupplier); + return getRpTlsClientCertsFromSignedJwks(signedJwks).orElseThrow(gsiExceptionSupplier); + } + + private static Optional getRpEncKeyFromEntityStatement( + final JsonWebToken entityStmntRp) { + final String sub = (String) entityStmntRp.getBodyClaims().get("sub"); + log.debug("Search encryption key in entity statement of RP [{}]).", sub); + final Optional>> keyList = + getKeyListFromEntityStatement(entityStmntRp); + final Optional encKeyFromKeyList = getEncKeyFromKeyList(keyList); + if (encKeyFromKeyList.isPresent()) + log.debug("Found encryption key in entity statement of RP [{}]).", sub); + return encKeyFromKeyList; + } + + private static Optional> getRpTlsClientCertsFromEntityStatement( + final JsonWebToken entityStmntRp) { + final String sub = (String) entityStmntRp.getBodyClaims().get("sub"); + log.debug("Search TLS client certificate in entity statement of RP [{}]).", sub); + final Optional>> keyList = + getKeyListFromEntityStatement(entityStmntRp); + final Optional> certsFromKeyList = getCertsFromKeyList(keyList); + if (certsFromKeyList.isPresent()) + log.debug("Found client certificates in entity statement of RP [{}]).", sub); + return certsFromKeyList; + } + + private static Optional getRpEncKeyFromSignedJwks( + final JsonWebToken signedJwks) { + final String sub = (String) signedJwks.getBodyClaims().get("sub"); + log.debug("Search encryption key in signed JWKS of RP [{}]).", sub); + final Optional>> keyList = getKeyListFromSignedJwks(signedJwks); + final Optional encKeyFromKeyList = getEncKeyFromKeyList(keyList); + if (encKeyFromKeyList.isPresent()) + log.debug("Found encryption key in signed JWKS of RP [{}]).", sub); + return encKeyFromKeyList; + } + + private static Optional> getRpTlsClientCertsFromSignedJwks( + final JsonWebToken signedJwks) { + final String sub = (String) signedJwks.getBodyClaims().get("sub"); + log.debug("Search TLS client certificate in signed JWKS of RP [{}]).", sub); + final Optional>> keyList = getKeyListFromSignedJwks(signedJwks); + final Optional> certsFromKeyList = getCertsFromKeyList(keyList); + if (certsFromKeyList.isPresent()) + log.debug("Found client certificates in signed JWKS of RP [{}]).", sub); + return certsFromKeyList; + } + + private static Optional>> getKeyListFromEntityStatement( + final JsonWebToken entityStmntRp) { + final Map metadata = + (Map) entityStmntRp.getBodyClaims().get("metadata"); + final Map openidRelyingParty = + (Map) metadata.get("openid_relying_party"); + if (openidRelyingParty.containsKey("jwks")) { + log.debug( + "Key [jwks] found in openid_relying_party (inside Entitystatement of RP [{}]).", + entityStmntRp.getBodyClaims().get("sub")); + final Map jwksMap = (Map) openidRelyingParty.get("jwks"); + final List> keyList = (List>) jwksMap.get("keys"); + return Optional.of(keyList); + } + return Optional.empty(); + } + + private static Optional>> getKeyListFromSignedJwks( + final JsonWebToken signedJwks) { + final List> keyList = + (List>) signedJwks.getBodyClaims().get("keys"); + return Optional.of(keyList); + } + + private static Optional> getCertsFromKeyList( + final Optional>> keyList) { + if (keyList.isPresent()) { + final List> certKeys = + keyList.get().stream() + .filter(key -> key.containsKey("use") && key.containsKey("x5c")) + .filter(key -> key.get("use").equals("sig")) + .toList(); + if (!certKeys.isEmpty()) { + final List x5cList = + certKeys.stream().map(EntityStatementRpReader::extractX5cValueFromCert).toList(); + return Optional.of(x5cList); + } + } + return Optional.empty(); + } + + private static Optional getEncKeyFromKeyList( + final Optional>> keyList) { + if (keyList.isPresent()) { + try { + + final Optional> encKeyAsMap = + keyList.get().stream() + .filter(key -> key.containsKey("use")) + .filter(key -> key.get("use").equals("enc")) + .findFirst(); + if (encKeyAsMap.isPresent()) { + return Optional.of(PublicJsonWebKey.Factory.newPublicJwk(encKeyAsMap.get())); + } + } catch (JoseException e) { + return Optional.empty(); + } + } + return Optional.empty(); + } + + private static X509Certificate extractX5cValueFromCert(Map certKey) { + List x5cValues = (List) certKey.get("x5c"); + if (x5cValues.isEmpty()) { + throw new GsiException( + INVALID_REQUEST, "No x5c certificate found in jwk", HttpStatus.UNAUTHORIZED); + } else if (x5cValues.size() > 1) { + throw new GsiException( + INVALID_REQUEST, "More than one x5c certificate found in jwk", HttpStatus.UNAUTHORIZED); + } else return transformStringToX509Certificate(x5cValues.getFirst()); + } + + private static Optional getSignedJwks(final JsonWebToken entityStmntRp) { + final Optional rpSignedJwksUri = ServerUrlService.determineSignedJwksUri(entityStmntRp); + if (rpSignedJwksUri.isPresent()) { + return HttpClient.fetchSignedJwks(rpSignedJwksUri.get()); + } + return Optional.empty(); + } + + private static Map getOpenidRelyingParty(final JsonWebToken entityStmntRp) { + final Map bodyClaims = entityStmntRp.getBodyClaims(); + final Map metadata = + Objects.requireNonNull( + (Map) bodyClaims.get("metadata"), "missing claim: metadata"); + return Objects.requireNonNull( + (Map) metadata.get("openid_relying_party"), + "missing claim: openid_relying_party"); + } + + private static X509Certificate transformStringToX509Certificate(final String certAsString) { + final byte[] encodedCert = Base64.getDecoder().decode(certAsString); + final ByteArrayInputStream inputStream = new ByteArrayInputStream(encodedCert); + + final CertificateFactory certFactory; + final X509Certificate cert; + try { + certFactory = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate) certFactory.generateCertificate(inputStream); + } catch (final CertificateException e) { + throw new GsiException( + INVALID_REQUEST, + "entry of x5c-element in signed_jwks/entity statement is not a valid x509-certificate", + HttpStatus.UNAUTHORIZED); + } + return cert; + } +} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpService.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpService.java deleted file mode 100644 index 7139287..0000000 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpService.java +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Copyright 2024 gematik GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.gematik.idp.gsi.server.services; - -import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; -import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_SCOPE; - -import de.gematik.idp.IdpConstants; -import de.gematik.idp.gsi.server.data.GsiConstants; -import de.gematik.idp.gsi.server.exceptions.GsiException; -import de.gematik.idp.token.JsonWebToken; -import de.gematik.idp.token.TokenClaimExtraction; -import java.io.ByteArrayInputStream; -import java.security.PublicKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.Base64; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import kong.unirest.core.HttpResponse; -import kong.unirest.core.Unirest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.jose4j.jwk.JsonWebKeySet; -import org.jose4j.jwk.PublicJsonWebKey; -import org.jose4j.lang.JoseException; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; - -/** EntityStmntRpService = managed EntityStatement of relying parties */ -@Slf4j -@Service -@RequiredArgsConstructor -public class EntityStatementRpService { - - private final PublicKey fedmasterSigKey; - private final ServerUrlService serverUrlService; - - /** Entity statements delivered by Fachdienst */ - private final Map entityStatementsOfFachdienst = new HashMap<>(); - - /** Entity statements about Fachdienste. Delivered by Fedmaster. */ - private final Map entityStatementsFedmasterAboutFachdienst = - new HashMap<>(); - - /** - * no exception -> client is registered - * - * @param clientId URL of relying party - */ - public void doAutoregistration( - final String clientId, final String redirectUri, final String scope) { - // Msg 2a and 2b - // Msg 2c and 2d - log.debug("Autoregistration started..."); - getEntityStatementRp(clientId); - verifyRedirectUriExistsInEntityStmnt(clientId, redirectUri); - verifyRequestedScopesListedInEntityStmnt(clientId, scope); - verifyIdpDoesSupportRequestedScopes(scope); - log.debug("Autoregistration done."); - } - - /** - * @param issuerRp name and url of the fachdienst/relying party - * @return the entity statement issued by the fachdienst/relying party - */ - public JsonWebToken getEntityStatementRp(final String issuerRp) { - log.debug("Entitystatement of RP [{}] requested.", issuerRp); - updateStatementRpIfExpiredAndNewIsAvailable(issuerRp); - log.debug( - "Entitystatement of RP [{}] stored. JWT: {}", - issuerRp, - entityStatementsOfFachdienst.get(issuerRp).getRawString()); - return entityStatementsOfFachdienst.get(issuerRp); - } - - /** - * Update entity statement about a relying party, from Fedmaster. - * - * @param sub identifier of the fachdienst/relying party - * @return the entity statement about the fachdienst/relying party issued by the fed master - */ - public JsonWebToken getEntityStatementAboutRp(final String sub) { - updateStatementAboutRpIfExpiredAndNewIsAvailable(sub); - return entityStatementsFedmasterAboutFachdienst.get(sub); - } - - public X509Certificate getRpTlsClientCert(final String sub) throws JoseException { - final Optional tlsClientCertFromEntityStatement = - getRpTlsClientCertFromEntityStatement(sub); - if (tlsClientCertFromEntityStatement.isPresent()) { - log.debug("Found TLS client certificate in entity statement of [{}].", sub); - return tlsClientCertFromEntityStatement.get(); - } - return getRpTlsClientCertFromSignedJwks(sub) - .orElseThrow( - () -> - new GsiException( - INVALID_REQUEST, - "TLS client certificate for relying party not found", - HttpStatus.BAD_REQUEST)); - } - - private Optional getRpTlsClientCertFromEntityStatement(final String sub) { - try { - final Optional>> keyList = getKeyListFromEntityStatement(sub); - if (keyList.isPresent()) { - final Optional> certKeyAsMap = - keyList.get().stream() - .filter(key -> key.containsKey("use") && key.containsKey("x5c")) - .filter(key -> key.get("use").equals("sig")) - .findFirst(); - if (certKeyAsMap.isPresent()) { - final List certList = (List) certKeyAsMap.get().get("x5c"); - if (certList.size() > 1) { - throw new GsiException( - INVALID_REQUEST, "More than one x5c certificate found", HttpStatus.UNAUTHORIZED); - } - return certList.stream().findFirst().map(this::transformStringToX509Certificate); - } - } - return Optional.empty(); - } catch (final NullPointerException | ClassCastException e) { - return Optional.empty(); - } - } - - private Optional getRpTlsClientCertFromSignedJwks(final String sub) { - try { - log.debug("Search TLS client certificate in signed JWKS of RP [{}]).", sub); - final Optional>> keyList = getKeyListFromSignedJwks(sub); - if (keyList.isPresent()) { - final Optional> certKeyAsMap = - keyList.get().stream() - .filter(key -> key.containsKey("use") && key.containsKey("x5c")) - .filter(key -> key.get("use").equals("sig")) - .findFirst(); - if (certKeyAsMap.isPresent()) { - log.debug("Found client certificate in signed JWKS of RP [{}]).", sub); - final List certList = (List) certKeyAsMap.get().get("x5c"); - if (certList.size() > 1) { - throw new GsiException( - INVALID_REQUEST, "More than one x5c certificate", HttpStatus.UNAUTHORIZED); - } - return certList.stream().findFirst().map(this::transformStringToX509Certificate); - } - } - return Optional.empty(); - } catch (final NullPointerException | ClassCastException e) { - return Optional.empty(); - } - } - - private X509Certificate transformStringToX509Certificate(final String certAsString) { - final byte[] encodedCert = Base64.getDecoder().decode(certAsString); - final ByteArrayInputStream inputStream = new ByteArrayInputStream(encodedCert); - - final CertificateFactory certFactory; - final X509Certificate cert; - try { - certFactory = CertificateFactory.getInstance("X.509"); - cert = (X509Certificate) certFactory.generateCertificate(inputStream); - } catch (final CertificateException e) { - throw new GsiException( - INVALID_REQUEST, - "entry of x5c-element in signed_jwks/entity statement is not a valid x509-certificate", - HttpStatus.UNAUTHORIZED); - } - return cert; - } - - public PublicJsonWebKey getRpEncKey(final String sub) throws JoseException { - final Optional encKeyFromEntityStatement = - getRpEncKeyFromEntityStatement(sub); - if (encKeyFromEntityStatement.isPresent()) { - log.debug("Found encryption key in entity statement of [{}].", sub); - return encKeyFromEntityStatement.get(); - } - return getRpEncKeyFromSignedJwks(sub) - .orElseThrow( - () -> - new GsiException( - INVALID_REQUEST, - "Encryption key for relying party not found", - HttpStatus.BAD_REQUEST)); - } - - private Optional getRpEncKeyFromEntityStatement(final String sub) { - try { - final Optional>> keyList = getKeyListFromEntityStatement(sub); - if (keyList.isPresent()) { - final Optional> encKeyAsMap = - keyList.get().stream() - .filter(key -> key.containsKey("use")) - .filter(key -> key.get("use").equals("enc")) - .findFirst(); - if (encKeyAsMap.isPresent()) { - return Optional.of(PublicJsonWebKey.Factory.newPublicJwk(encKeyAsMap.get())); - } - } - return Optional.empty(); - } catch (final JoseException | NullPointerException | ClassCastException e) { - return Optional.empty(); - } - } - - private Optional getRpEncKeyFromSignedJwks(final String sub) { - try { - log.debug("Search encryption key in signed JWKS of RP [{}]).", sub); - final Optional>> keyList = getKeyListFromSignedJwks(sub); - if (keyList.isPresent()) { - final Optional> encKeyAsMap = - keyList.get().stream() - .filter(key -> key.containsKey("use")) - .filter(key -> key.get("use").equals("enc")) - .findFirst(); - if (encKeyAsMap.isPresent()) { - log.debug("Found encryption key in signed JWKS of RP [{}]).", sub); - return Optional.of(PublicJsonWebKey.Factory.newPublicJwk(encKeyAsMap.get())); - } - } - return Optional.empty(); - } catch (final JoseException | NullPointerException | ClassCastException e) { - return Optional.empty(); - } - } - - private Optional getSignedJwks(final String sub) { - final Optional rpSignedJwksUri = - serverUrlService.determineSignedJwksUri(getEntityStatementRp(sub)); - if (rpSignedJwksUri.isPresent()) { - final HttpResponse resp = Unirest.get(rpSignedJwksUri.get()).asString(); - if (resp.isSuccess()) { - // TODO check signature - return Optional.of(new JsonWebToken(resp.getBody())); - } - } - return Optional.empty(); - } - - private Optional>> getKeyListFromEntityStatement(final String sub) { - final JsonWebToken entityStmntRp = getEntityStatementRp(sub); - final Map metadata = - (Map) entityStmntRp.getBodyClaims().get("metadata"); - final Map openidRelyingParty = - (Map) metadata.get("openid_relying_party"); - if (openidRelyingParty.containsKey("jwks")) { - log.debug( - "Key [jwks] found in openid_relying_party (inside Entitystatement of RP [{}]).", sub); - final Map jwksMap = (Map) openidRelyingParty.get("jwks"); - final List> keyList = (List>) jwksMap.get("keys"); - return Optional.of(keyList); - } - return Optional.empty(); - } - - private Optional>> getKeyListFromSignedJwks(final String sub) { - final Optional signedJwks = getSignedJwks(sub); - if (signedJwks.isPresent()) { - final List> keyList = - (List>) signedJwks.get().getBodyClaims().get("keys"); - return Optional.of(keyList); - } - return Optional.empty(); - } - - private static List getRedirectUrisEntityStatementRp(final JsonWebToken entityStmntRp) { - final Map bodyClaims = entityStmntRp.getBodyClaims(); - final Map metadata = - Objects.requireNonNull( - (Map) bodyClaims.get("metadata"), "missing claim: metadata"); - final Map openidRelyingParty = - Objects.requireNonNull( - (Map) metadata.get("openid_relying_party"), - "missing claim: openid_relying_party"); - return Objects.requireNonNull( - (List) openidRelyingParty.get("redirect_uris"), "missing claim: redirect_uris"); - } - - private static List getScopesFromEntityStatementRp(final JsonWebToken entityStmntRp) { - final Map bodyClaims = entityStmntRp.getBodyClaims(); - final Map metadata = - Objects.requireNonNull( - (Map) bodyClaims.get("metadata"), "missing claim: metadata"); - final Map openidRelyingParty = - Objects.requireNonNull( - (Map) metadata.get("openid_relying_party"), - "missing claim: openid_relying_party"); - return Arrays.stream( - Objects.requireNonNull((String) openidRelyingParty.get("scope"), "missing claim: scope") - .split(" ")) - .toList(); - } - - /** - * @param fachdienstClientId - * @param redirectUri - */ - private void verifyRedirectUriExistsInEntityStmnt( - final String fachdienstClientId, final String redirectUri) { - if (getRedirectUrisEntityStatementRp(getEntityStatementRp(fachdienstClientId)).stream() - .noneMatch(entry -> entry.equals(redirectUri))) { - throw new GsiException( - INVALID_REQUEST, - "Content of parameter redirect_uri [" + redirectUri + "] not found in entity statement. ", - HttpStatus.BAD_REQUEST); - } - } - - private void verifyRequestedScopesListedInEntityStmnt( - final String fachdienstClientId, final String scopeParameter) { - final List scopesFromEntityStatementRp = - getScopesFromEntityStatementRp(getEntityStatementRp(fachdienstClientId)); - if (Arrays.stream(scopeParameter.split(" ")) - .anyMatch(scope -> !scopesFromEntityStatementRp.contains(scope))) { - throw new GsiException( - INVALID_SCOPE, - "Content of parameter scope [" - + scopeParameter - + "] exceeds scopes found in entity statement. ", - HttpStatus.BAD_REQUEST); - } - } - - private void verifyIdpDoesSupportRequestedScopes(final String scopeParameter) { - final Set requestedScopes = - Arrays.stream(scopeParameter.split(" ")).collect(Collectors.toSet()); - - if (!(GsiConstants.SCOPES_SUPPORTED.containsAll(requestedScopes))) { - throw new GsiException( - INVALID_SCOPE, "More scopes requested in PAR than supported.", HttpStatus.BAD_REQUEST); - } - } - - private void updateStatementRpIfExpiredAndNewIsAvailable(final String issuer) { - if (entityStatementsOfFachdienst.containsKey(issuer)) { - if (stmntIsEpired(entityStatementsOfFachdienst.get(issuer))) { - log.debug("Entitystatement of RP [{}] is in storage but expired. Fetching...", issuer); - fetchEntityStatementRp(issuer); - } else { - log.debug("Entitystatement of RP [{}] is in storage and not expired.", issuer); - } - return; - } - log.debug("Entitystatement of RP [{}] not found in storage. Fetching...", issuer); - fetchEntityStatementRp(issuer); - } - - private void updateStatementAboutRpIfExpiredAndNewIsAvailable(final String sub) { - if (entityStatementsFedmasterAboutFachdienst.containsKey(sub)) { - if (stmntIsEpired(entityStatementsFedmasterAboutFachdienst.get(sub))) { - log.debug("Entitystatement about RP [{}] is in storage but expired. Fetching...", sub); - fetchEntityStatementAboutRp(sub); - } else { - log.debug("Entitystatement about RP [{}] is in storage and not expired.", sub); - } - return; - } - log.debug("Entitystatement about RP [{}] not found in storage. Fetching...", sub); - fetchEntityStatementAboutRp(sub); - } - - private boolean stmntIsEpired(final JsonWebToken entityStmnt) { - final Map bodyClaims = entityStmnt.getBodyClaims(); - final Long exp = (Long) bodyClaims.get("exp"); - return isExpired(exp); - } - - private boolean isExpired(final Long exp) { - final ZonedDateTime currentUtcTime = ZonedDateTime.now(ZoneOffset.UTC); - final ZonedDateTime expiredUtcTime = - ZonedDateTime.ofInstant(Instant.ofEpochSecond(exp), ZoneOffset.UTC); - return currentUtcTime.isAfter(expiredUtcTime); - } - - private void fetchEntityStatementRp(final String issuer) { - final HttpResponse resp = - Unirest.get(issuer + IdpConstants.ENTITY_STATEMENT_ENDPOINT).asString(); - if (resp.getStatus() == HttpStatus.OK.value()) { - final JsonWebToken entityStmnt = new JsonWebToken(resp.getBody()); - verifyEntityStmntRp(entityStmnt, issuer); - entityStatementsOfFachdienst.put(issuer, entityStmnt); - log.debug( - "Entitystatement of RP [{}] stored. JWT: {}", - issuer, - entityStatementsOfFachdienst.get(issuer).getRawString()); - } else { - log.info(resp.getBody()); - throw new GsiException( - INVALID_REQUEST, - "No entity statement of relying party [" - + issuer - + "] available. Reason: " - + resp.getBody() - + HttpStatus.valueOf(resp.getStatus()), - HttpStatus.BAD_REQUEST); - } - } - - private void verifyEntityStmntRp(final JsonWebToken entityStmnt, final String issuer) { - final String keyIdSigEntStmnt = (String) entityStmnt.getHeaderClaims().get("kid"); - final JsonWebToken esAboutRp = getEntityStatementAboutRp(issuer); - final JsonWebKeySet jwks = TokenClaimExtraction.extractJwksFromBody(esAboutRp.getRawString()); - entityStmnt.verify(TokenClaimExtraction.getECPublicKey(jwks, keyIdSigEntStmnt)); - } - - private void fetchEntityStatementAboutRp(final String sub) { - final String entityIdentifierFedmaster = serverUrlService.determineFedmasterUrl(); - log.info("FedmasterUrl: " + entityIdentifierFedmaster); - final HttpResponse resp = - Unirest.get(serverUrlService.determineFetchEntityStatementEndpoint()) - .queryString("iss", entityIdentifierFedmaster) - .queryString("sub", sub) - .asString(); - if (resp.getStatus() == HttpStatus.OK.value()) { - final JsonWebToken entityStatementAboutRp = new JsonWebToken(resp.getBody()); - entityStatementAboutRp.verify(fedmasterSigKey); - entityStatementsFedmasterAboutFachdienst.put(sub, entityStatementAboutRp); - log.debug( - "Entitystatement about RP [{}] stored. JWT: {}", - sub, - entityStatementsFedmasterAboutFachdienst.get(sub).getRawString()); - } else { - log.info(resp.getBody()); - throw new GsiException( - INVALID_REQUEST, - "No entity statement about relying party [" - + sub - + "] at Fedmaster iss: " - + entityIdentifierFedmaster - + " available. Reason: " - + resp.getBody() - + HttpStatus.valueOf(resp.getStatus()), - HttpStatus.BAD_REQUEST); - } - } -} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpVerifier.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpVerifier.java new file mode 100644 index 0000000..4226d9d --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/EntityStatementRpVerifier.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; +import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_SCOPE; + +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.token.JsonWebToken; +import de.gematik.idp.token.TokenClaimExtraction; +import java.util.Arrays; +import java.util.List; +import org.jose4j.jwk.JsonWebKeySet; +import org.springframework.http.HttpStatus; + +public abstract class EntityStatementRpVerifier { + + public static void verifyEntityStmntRp(final JsonWebToken entityStmnt, final JsonWebKeySet jwks) { + final String keyIdSigEntStmnt = (String) entityStmnt.getHeaderClaims().get("kid"); + entityStmnt.verify(TokenClaimExtraction.getECPublicKey(jwks, keyIdSigEntStmnt)); + } + + public static void verifyRedirectUriExistsInEntityStmnt( + final JsonWebToken entityStmntRp, final String redirectUri) { + if (EntityStatementRpReader.getRedirectUrisEntityStatementRp(entityStmntRp).stream() + .noneMatch(entry -> entry.equals(redirectUri))) { + throw new GsiException( + INVALID_REQUEST, + "Content of parameter redirect_uri [" + redirectUri + "] not found in entity statement. ", + HttpStatus.BAD_REQUEST); + } + } + + public static void verifyRequestedScopesListedInEntityStmnt( + final JsonWebToken entityStmntRp, final String scopeParameter) { + final List scopesFromEntityStatementRp = + EntityStatementRpReader.getScopesFromEntityStatementRp(entityStmntRp); + if (Arrays.stream(scopeParameter.split(" ")) + .anyMatch(scope -> !scopesFromEntityStatementRp.contains(scope))) { + throw new GsiException( + INVALID_SCOPE, + "Content of parameter scope [" + + scopeParameter + + "] exceeds scopes found in entity statement. ", + HttpStatus.BAD_REQUEST); + } + } +} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/HttpClient.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/HttpClient.java new file mode 100644 index 0000000..44f1e29 --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/HttpClient.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; + +import de.gematik.idp.IdpConstants; +import de.gematik.idp.gsi.server.data.RpToken; +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.token.JsonWebToken; +import java.util.Optional; +import kong.unirest.core.HttpResponse; +import kong.unirest.core.Unirest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +@Slf4j +public abstract class HttpClient { + + public static Optional fetchSignedJwks(final String signedJwksUri) { + final HttpResponse resp = Unirest.get(signedJwksUri).asString(); + if (resp.isSuccess()) { + // TODO check signature + return Optional.of(new JsonWebToken(resp.getBody())); + } + return Optional.empty(); + } + + public static RpToken fetchEntityStatementRp(final String issuer) { + final HttpResponse resp = + Unirest.get(issuer + IdpConstants.ENTITY_STATEMENT_ENDPOINT).asString(); + if (resp.getStatus() == HttpStatus.OK.value()) { + return new RpToken(new JsonWebToken(resp.getBody())); + } else { + log.info(resp.getBody()); + throw new GsiException( + INVALID_REQUEST, + "No entity statement of relying party [" + + issuer + + "] available. Reason: " + + resp.getBody() + + HttpStatus.valueOf(resp.getStatus()), + HttpStatus.BAD_REQUEST); + } + } + + public static JsonWebToken fetchEntityStatementAboutRp( + final String sub, final String fedmasterUrl, final String entityStmntEndpoint) { + log.info("FedmasterUrl: " + fedmasterUrl); + final HttpResponse resp = + Unirest.get(entityStmntEndpoint) + .queryString("iss", fedmasterUrl) + .queryString("sub", sub) + .asString(); + if (resp.getStatus() == HttpStatus.OK.value()) { + return new JsonWebToken(resp.getBody()); + } else { + log.info(resp.getBody()); + throw new GsiException( + INVALID_REQUEST, + "No entity statement about relying party [" + + sub + + "] at Fedmaster iss: " + + fedmasterUrl + + " available. Reason: " + + resp.getBody() + + HttpStatus.valueOf(resp.getStatus()), + HttpStatus.BAD_REQUEST); + } + } +} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/RequestValidator.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/RequestValidator.java new file mode 100644 index 0000000..1067d7b --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/RequestValidator.java @@ -0,0 +1,161 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.data.Oauth2ErrorCode.*; +import static de.gematik.idp.gsi.server.data.GsiConstants.*; + +import de.gematik.idp.crypto.CryptoLoader; +import de.gematik.idp.crypto.exceptions.IdpCryptoException; +import de.gematik.idp.field.ClientUtilities; +import de.gematik.idp.gsi.server.data.FedIdpAuthSession; +import de.gematik.idp.gsi.server.data.GsiConstants; +import de.gematik.idp.gsi.server.data.RpToken; +import de.gematik.idp.gsi.server.exceptions.GsiException; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +@Slf4j +public abstract class RequestValidator { + + public static void validateParParams( + final RpToken entityStmntRp, final String redirectUri, final String scope) { + // Msg 2a and 2b + // Msg 2c and 2d + entityStmntRp.verifyRedirectUriExistsInEntityStmnt(redirectUri); + entityStmntRp.verifyRequestedScopesListedInEntityStmnt(scope); + verifyIdpDoesSupportRequestedScopes(scope); + } + + public static void validateCertificate( + final String clientCert, final RpToken entityStmntRp, final boolean isRequiredClientCert) { + if (clientCert == null) { + if (isRequiredClientCert) { + throw new GsiException( + INVALID_REQUEST, "client certificate is missing", HttpStatus.BAD_REQUEST); + } + } else { + try { + final X509Certificate certFromRequest = + CryptoLoader.getCertificateFromPem( + java.net.URLDecoder.decode(clientCert, StandardCharsets.UTF_8).getBytes()); + final List certsFromEntityStatement = + entityStmntRp.getRpTlsClientCertificates(); + if (certsFromEntityStatement.stream() + .noneMatch(certFromEs -> certFromEs.equals(certFromRequest))) { + throw new GsiException( + UNAUTHORIZED_CLIENT, + "client certificate in tls handshake does not match any certificate in entity" + + " statement/signed_jwks", + HttpStatus.UNAUTHORIZED); + } + } catch (final IdpCryptoException e) { + throw new GsiException( + UNAUTHORIZED_CLIENT, + "client certificate in tls handshake is not a valid x509 certificate", + HttpStatus.UNAUTHORIZED); + } + } + } + + public static void validateAuthRequestParams( + final FedIdpAuthSession session, final String clientId) { + final boolean clientIdBelongsToRequestUri = session.getFachdienstClientId().equals(clientId); + if (!clientIdBelongsToRequestUri) { + throw new GsiException(INVALID_REQUEST, "unknown client_id", HttpStatus.BAD_REQUEST); + } + } + + public static void verifyRedirectUri(final String redirectUri, final String sessionRedirectUri) { + if (!redirectUri.equals(sessionRedirectUri)) { + throw new GsiException(INVALID_REQUEST, "invalid redirect_uri", HttpStatus.BAD_REQUEST); + } + } + + public static void verifyCodeVerifier(final String codeVerifier, final String codeChallenge) { + if (!ClientUtilities.generateCodeChallenge(codeVerifier).equals(codeChallenge)) { + throw new GsiException(INVALID_REQUEST, "invalid code_verifier", HttpStatus.BAD_REQUEST); + } + } + + public static void verifyClientId(final String clientId, final String sessionClientId) { + if (!sessionClientId.equals(clientId)) { + throw new GsiException(INVALID_REQUEST, "invalid client_id", HttpStatus.BAD_REQUEST); + } + } + + protected static void verifyIdpDoesSupportRequestedScopes(final String scopeParameter) { + final Set requestedScopes = + Arrays.stream(scopeParameter.split(" ")).collect(Collectors.toSet()); + + if (!(GsiConstants.SCOPES_SUPPORTED.containsAll(requestedScopes))) { + throw new GsiException( + INVALID_SCOPE, "More scopes requested in PAR than supported.", HttpStatus.BAD_REQUEST); + } + } + + /** + * work in progress validates amr and acr values and their combinations + * + * @param acr set contains values if they were set as essential in claims param + * @param amr set contains values if they were set as essential in claims param + */ + public static void validateAmrAcrCombination(final Set acr, final Set amr) { + acr.forEach( + acrValue -> { + if (!ACR_VALUES.contains(acrValue)) { + throw new GsiException( + INVALID_REQUEST, "invalid acr value: " + acrValue, HttpStatus.BAD_REQUEST); + } + }); + amr.forEach( + amrValue -> { + if (!AMR_VALUES.contains(amrValue)) { + throw new GsiException( + INVALID_REQUEST, "invalid amr value: " + amrValue, HttpStatus.BAD_REQUEST); + } + }); + if (acr.isEmpty() || amr.isEmpty()) return; + if (acr.contains(ACR_HIGH) && !acr.contains(ACR_SUBSTANTIAL)) { + amr.forEach( + value -> { + if (!AMR_VALUES_HIGH.contains(value)) { + throw new GsiException( + INVALID_REQUEST, + "invalid combination of essential values acr and amr", + HttpStatus.BAD_REQUEST); + } + }); + } else if (acr.contains(ACR_SUBSTANTIAL) && !acr.contains(ACR_HIGH)) { + amr.forEach( + value -> { + if (!AMR_VALUES_SUBSTANTIAL.contains(value)) { + throw new GsiException( + INVALID_REQUEST, + "invalid combination of essential values acr and amr", + HttpStatus.BAD_REQUEST); + } + }); + } + } +} diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/ServerUrlService.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/ServerUrlService.java index dd2ae04..f738422 100644 --- a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/ServerUrlService.java +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/ServerUrlService.java @@ -90,7 +90,7 @@ private static String readFederationFetchEndpointFromEntityStatement( return Objects.requireNonNull((String) federationEntity.get("federation_fetch_endpoint")); } - public Optional determineSignedJwksUri(final JsonWebToken entityStmntRp) { + public static Optional determineSignedJwksUri(final JsonWebToken entityStmntRp) { final Map bodyClaims = entityStmntRp.getBodyClaims(); final Map metadata = Objects.requireNonNull( @@ -99,6 +99,6 @@ public Optional determineSignedJwksUri(final JsonWebToken entityStmntRp) Objects.requireNonNull( (Map) metadata.get("openid_relying_party"), "missing claim: openid_relying_party"); - return Optional.of((String) openidRelyingParty.get("signed_jwks_uri")); + return Optional.ofNullable((String) openidRelyingParty.getOrDefault("signed_jwks_uri", null)); } } diff --git a/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/TokenRepositoryRp.java b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/TokenRepositoryRp.java new file mode 100644 index 0000000..2e88157 --- /dev/null +++ b/gsi-server/src/main/java/de/gematik/idp/gsi/server/services/TokenRepositoryRp.java @@ -0,0 +1,112 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import de.gematik.idp.gsi.server.data.RpToken; +import de.gematik.idp.token.JsonWebToken; +import de.gematik.idp.token.TokenClaimExtraction; +import java.security.PublicKey; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jose4j.jwk.JsonWebKeySet; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenRepositoryRp { + + private final Map entityStmtsOfRp = new HashMap<>(); + private final Map entityStmtsAboutRp = new HashMap<>(); + private final ServerUrlService serverUrlService; + private final PublicKey fedmasterSigKey; + + public RpToken getEntityStatementRp(final String issuerRp) { + log.debug("Entitystatement of RP [{}] requested.", issuerRp); + updateStatementRpIfExpiredAndNewIsAvailable(issuerRp); + log.debug( + "Entitystatement of RP [{}] stored. JWT: {}", + issuerRp, + entityStmtsOfRp.get(issuerRp).getToken().getRawString()); + return entityStmtsOfRp.get(issuerRp); + } + + public JsonWebToken getEntityStatementAboutRp(final String sub) { + updateStatementAboutRpIfExpiredAndNewIsAvailable(sub); + return entityStmtsAboutRp.get(sub); + } + + private void updateStatementRpIfExpiredAndNewIsAvailable(final String issuer) { + if (entityStmtsOfRp.containsKey(issuer)) { + if (entityStmtsOfRp.get(issuer).isExpired()) { + log.debug("Entitystatement of RP [{}] is in storage but expired. Fetching...", issuer); + fetchAndStoreEntityStmnt(issuer); + } else { + log.debug("Entitystatement of RP [{}] is in storage and not expired.", issuer); + } + return; + } + log.debug("Entitystatement of RP [{}] not found in storage. Fetching...", issuer); + fetchAndStoreEntityStmnt(issuer); + } + + private void updateStatementAboutRpIfExpiredAndNewIsAvailable(final String sub) { + if (entityStmtsAboutRp.containsKey(sub)) { + if (!entityStmtsAboutRp.get(sub).getExpiresAt().isBefore(ZonedDateTime.now())) { + log.debug("Entitystatement about RP [{}] is in storage but expired. Fetching...", sub); + fetchAndStoreEntityStmntAboutRp(sub); + } else { + log.debug("Entitystatement about RP [{}] is in storage and not expired.", sub); + } + return; + } + log.debug("Entitystatement about RP [{}] not found in storage. Fetching...", sub); + fetchAndStoreEntityStmntAboutRp(sub); + } + + private void fetchAndStoreEntityStmnt(final String issuer) { + final RpToken entityStmnt = HttpClient.fetchEntityStatementRp(issuer); + + final JsonWebToken esAboutRp = getEntityStatementAboutRp(issuer); + final JsonWebKeySet jwks = TokenClaimExtraction.extractJwksFromBody(esAboutRp.getRawString()); + entityStmnt.verify(jwks); + + entityStmtsOfRp.put(issuer, entityStmnt); + log.debug( + "Entitystatement of RP [{}] stored. JWT: {}", + issuer, + entityStmtsOfRp.get(issuer).getToken().getRawString()); + } + + private void fetchAndStoreEntityStmntAboutRp(final String sub) { + final JsonWebToken entityStmntAboutRp = + HttpClient.fetchEntityStatementAboutRp( + sub, + serverUrlService.determineFedmasterUrl(), + serverUrlService.determineFetchEntityStatementEndpoint()); + + entityStmntAboutRp.verify(fedmasterSigKey); + entityStmtsAboutRp.put(sub, entityStmntAboutRp); + log.debug( + "Entitystatement about RP [{}] stored. JWT: {}", + sub, + entityStmtsAboutRp.get(sub).getRawString()); + } +} diff --git a/gsi-server/src/main/resources/templates/landingTemplate.html b/gsi-server/src/main/resources/templates/landingTemplate.html index 22fb979..1dc5ec8 100644 --- a/gsi-server/src/main/resources/templates/landingTemplate.html +++ b/gsi-server/src/main/resources/templates/landingTemplate.html @@ -23,6 +23,37 @@ function submitForm(){ let kvnrValue = document.getElementById("kvnr").value.trim(); document.getElementById("userid").value = kvnrValue != "" ? kvnrValue : "X110411675"; + + document.getElementById("acrValue").value = (document.getElementById("acrValue").value === "") ? "gematik-ehealth-loa-high" : document.getElementById("acrValue").value + document.getElementById("amrValue").value = (document.getElementById("amrValue").value === "") ? "urn:telematik:auth:eGK" : document.getElementById("amrValue").value + } + + function setAmrOptions(acr){ + document.getElementById("acrValue").value = acr + let amrValues = ["urn:telematik:auth:mEW", "urn:telematik:auth:eGK", "urn:telematik:auth:eID", "urn:telematik:auth:sso", "urn:telematik:auth:guest:eGK", "urn:telematik:auth:other"] + let amrSelectElement = document.getElementById("amr-select") + while (amrSelectElement.lastChild) { + if(amrSelectElement.children.length === 1) break + amrSelectElement.removeChild(amrSelectElement.lastChild); + } + if(acr === "gematik-ehealth-loa-substantial"){ + addAmrOption(amrValues[0], amrSelectElement) + } else if (acr === "gematik-ehealth-loa-high"){ + for(let i = 1; i < amrValues.length; i++){ + addAmrOption(amrValues[i], amrSelectElement) + } + } + } + + function addAmrOption(optionValue, amrSelectElement){ + let amrOption = document.createElement("option") + amrOption.value = optionValue + amrOption.text = optionValue + amrSelectElement.add(amrOption) + } + + function setAmrValue() { + document.getElementById("amrValue").value = document.getElementById("amr-select").value; } @@ -42,10 +73,23 @@

gematik IDP Authenication Page (Test only)



+

+
+ + + + +

+
+

+ +
diff --git a/gsi-server/src/main/resources/versicherte.gesundheitsid.json b/gsi-server/src/main/resources/versicherte.gesundheitsid.json index 4c1ba95..ca88c4a 100644 --- a/gsi-server/src/main/resources/versicherte.gesundheitsid.json +++ b/gsi-server/src/main/resources/versicherte.gesundheitsid.json @@ -1450,5 +1450,65 @@ "urn:telematik:claims:profession": "1.2.276.0.76.4.49", "urn:telematik:claims:id": "X110596703", "urn:telematik:claims:organization": "109500969" + }, + { + "birthdate": "1984-09-16", + "urn:telematik:claims:alter": "39", + "urn:telematik:claims:family_name": "Heckhausén", + "urn:telematik:claims:display_name": "Dr. Fedilio Ubbo J.-D. HeckhausénTEST-ONLY", + "urn:telematik:claims:given_name": "Fedilio Ubbo Jörg-Dietrich", + "urn:telematik:claims:geschlecht": "W", + "urn:telematik:claims:email": "fedilio@mail.heckhau.de", + "urn:telematik:claims:profession": "1.2.276.0.76.4.49", + "urn:telematik:claims:id": "X110591068", + "urn:telematik:claims:organization": "109500969" + }, + { + "birthdate": "1984-09-16", + "urn:telematik:claims:alter": "39", + "urn:telematik:claims:family_name": "Gõdofský-Witzigmann", + "urn:telematik:claims:display_name": "Claudette Freifrau Gõdofský-WitzigmannTEST-ONLY", + "urn:telematik:claims:given_name": "Claudette Freifrau", + "urn:telematik:claims:geschlecht": "W", + "urn:telematik:claims:email": "freifrau.claudette@mail.witzigmann.de", + "urn:telematik:claims:profession": "1.2.276.0.76.4.49", + "urn:telematik:claims:id": "X110581992", + "urn:telematik:claims:organization": "109500969" + }, + { + "birthdate": "1984-09-16", + "urn:telematik:claims:alter": "39", + "urn:telematik:claims:family_name": "Tóboggen", + "urn:telematik:claims:display_name": "Prof. Dr. Maude Gräfin TóboggenTEST-ONLY", + "urn:telematik:claims:given_name": "Maude Gräfin", + "urn:telematik:claims:geschlecht": "W", + "urn:telematik:claims:email": "maude@mail.toboggen.de", + "urn:telematik:claims:profession": "1.2.276.0.76.4.49", + "urn:telematik:claims:id": "X110417514", + "urn:telematik:claims:organization": "109500969" + }, + { + "birthdate": "1984-09-16", + "urn:telematik:claims:alter": "39", + "urn:telematik:claims:family_name": "Sänder", + "urn:telematik:claims:display_name": "Grace Adelheid Andrea T. Gräfin SänderTEST-ONLY", + "urn:telematik:claims:given_name": "Grace Adelheid Andrea Tatjana Gräfin", + "urn:telematik:claims:geschlecht": "W", + "urn:telematik:claims:email": "Grace@mail.saender.de", + "urn:telematik:claims:profession": "1.2.276.0.76.4.49", + "urn:telematik:claims:id": "X110522695", + "urn:telematik:claims:organization": "109500969" + }, + { + "birthdate": "1984-09-16", + "urn:telematik:claims:alter": "39", + "urn:telematik:claims:family_name": "Caner", + "urn:telematik:claims:display_name": "Dr. Lukas CanerTEST-ONLY", + "urn:telematik:claims:given_name": "Lukas", + "urn:telematik:claims:geschlecht": "W", + "urn:telematik:claims:email": "Lukas@mail.Caner.de", + "urn:telematik:claims:profession": "1.2.276.0.76.4.49", + "urn:telematik:claims:id": "X110503240", + "urn:telematik:claims:organization": "109500969" } ] diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/common/Constants.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/common/Constants.java index 51f4b25..82eaf25 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/common/Constants.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/common/Constants.java @@ -44,4 +44,7 @@ public final class Constants { public static final String SIGNED_JWKS = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InB1a19mZF9zaWcifQ.eyJpc3MiOiJodHRwczovL2lkcGZhZGkuZGV2LmdlbWF0aWsuc29sdXRpb25zIiwiaWF0IjoxNjk3MjAyNDg4LCJrZXlzIjpbeyJ1c2UiOiJzaWciLCJraWQiOiJwdWtfZmRfc2lnIiwia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiI5YkpzMjdZQWZsTVVXSzVueHVpRjZYQUcwSmF6dXZ3UmkxRXBGSzBYS2lrIiwieSI6IlA4bHpOVlJPZ1R1d2JEcXNkOHJUMUFJM3plejk0SEJzVERwT3ZhalAwclkifSx7InVzZSI6ImVuYyIsImtpZCI6InB1a19mZF9lbmMiLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik5RTGFXYnVRREhnU0hhaHFiOXp4bERkaU1DSFhTZ1kwTDlxbDFrN0JWVUUiLCJ5IjoiX1VTZ21xaGxNM3B2YWJrWjJTU19ZRTJRNTd0VHM2cEs5Y0VfdVpCLXUzYyJ9LHsieDVjIjpbIk1JSUNHakNDQWNDZ0F3SUJBZ0lVVEd5TG0wZFhDU3dVdW5TK0M3WTRkclpnRzVrd0NnWUlLb1pJemowRUF3SXdmekVMTUFrR0ExVUVCaE1DUkVVeER6QU5CZ05WQkFnTUJrSmxjbXhwYmpFUE1BMEdBMVVFQnd3R1FtVnliR2x1TVJvd0dBWURWUVFLREJGblpXMWhkR2xySUU1UFZDMVdRVXhKUkRFUE1BMEdBMVVFQ3d3R1VGUWdTVVJOTVNFd0h3WURWUVFEREJobVlXTm9aR2xsYm5OMFZHeHpReUJVUlZOVUxVOU9URmt3SGhjTk1qTXdNakV3TVRJek5UTTFXaGNOTWpRd01qRXdNVEl6TlRNMVdqQi9NUXN3Q1FZRFZRUUdFd0pFUlRFUE1BMEdBMVVFQ0F3R1FtVnliR2x1TVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4R2pBWUJnTlZCQW9NRVdkbGJXRjBhV3NnVGs5VUxWWkJURWxFTVE4d0RRWURWUVFMREFaUVZDQkpSRTB4SVRBZkJnTlZCQU1NR0daaFkyaGthV1Z1YzNSVWJITkRJRlJGVTFRdFQwNU1XVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT1JxcVN1cisySFpUaEZxQTdFR3E4YmJGMi81d0w1bWpjL0J4b09kb3Q3cnQwUUwwRG5LMjBlcjRwS1R4cml5MCtOUHN4UUZrdm1LZUVLYlY0RWlKNlNqR2pBWU1Ba0dBMVVkRXdRQ01BQXdDd1lEVlIwUEJBUURBZ1hnTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUQwVGl2VitubFROMDZ2akJadDFQVVFkNkdoUWtheUVKK2FEcVMwUjJaL3hBaUVBMUt4RkhRN0dMRFNsLzZPb2dXRnN4S2FmWFEreVpLazl2dEsvUG9oZm0zbz0iXSwidXNlIjoic2lnIiwia2lkIjoicHVrX3Rsc19zaWciLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjVHcXBLNnY3WWRsT0VXb0RzUWFyeHRzWGJfbkF2bWFOejhIR2c1MmkzdXMiLCJ5IjoidDBRTDBEbksyMGVyNHBLVHhyaXkwLU5Qc3hRRmt2bUtlRUtiVjRFaUo2USJ9XX0.BYCS-xn-jSpIFq501Jb7B0kewaO7UeOrg7LJmCLyQI3xz76rXJ7PiDtBTyAeTQ1S5jyQfS_-t2oDtWv5UJQh3w"; + + public static final String SIGNED_JWKS_TWO_CERTS = + "eyJhbGciOiJFUzI1NiIsInR5cCI6Imp3ay1zZXQranNvbiIsImtpZCI6InB1a19mZF9zaWcifQ.eyJpc3MiOiJodHRwczovL2lkcGZhZGkuZGV2LmdlbWF0aWsuc29sdXRpb25zIiwiaWF0IjoxNzI0MzE0MDc5LCJrZXlzIjpbeyJ4NWMiOlsiTUlJQ09UQ0NBZCtnQXdJQkFnSVVCZmpUN1pXS2JjeUZObVpZUE5reFF2dGlJT013Q2dZSUtvWkl6ajBFQXdJd2Z6RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWdNQmtKbGNteHBiakVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUm93R0FZRFZRUUtEQkZuWlcxaGRHbHJJRTVQVkMxV1FVeEpSREVQTUEwR0ExVUVDd3dHVUZRZ1NVUk5NU0V3SHdZRFZRUUREQmhtWVdOb1pHbGxibk4wVkd4elF5QlVSVk5VTFU5T1RGa3dIaGNOTWpRd01URTJNRGcwT1RVMldoY05Namd3TkRJNU1EZzBPVFUyV2pCL01Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVDQXdHUW1WeWJHbHVNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhHakFZQmdOVkJBb01FV2RsYldGMGFXc2dUazlVTFZaQlRFbEVNUTh3RFFZRFZRUUxEQVpRVkNCSlJFMHhJVEFmQmdOVkJBTU1HR1poWTJoa2FXVnVjM1JVYkhORElGUkZVMVF0VDA1TVdUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJPUnFxU3VyKzJIWlRoRnFBN0VHcThiYkYyLzV3TDVtamMvQnhvT2RvdDdydDBRTDBEbksyMGVyNHBLVHhyaXkwK05Qc3hRRmt2bUtlRUtiVjRFaUo2U2pPVEEzTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRREFnWGdNQjBHQTFVZERnUVdCQlIvekdmdHZaT3R0Uk9LRzNJVldqdHhlNVBjekRBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlCQVdxSDloUGh1TEJXanZTYm9FRW1qY2dnVXhablR2R1pJeldaUjdTa3g1Z0loQUpyMFJQNERXeEl2WkRON1JNeXFjdEQ2QTY0cjRKS0NXb3cvS3RvaC9nUkkiXSwidXNlIjoic2lnIiwia2lkIjoicHVrX3Rsc19zaWciLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjVHcXBLNnY3WWRsT0VXb0RzUWFyeHRzWGJfbkF2bWFOejhIR2c1MmkzdXMiLCJ5IjoidDBRTDBEbksyMGVyNHBLVHhyaXkwLU5Qc3hRRmt2bUtlRUtiVjRFaUo2USIsImFsZyI6IkVTMjU2In0seyJ4NWMiOlsiTUlJQ1B6Q0NBZVdnQXdJQkFnSVVSdmlGTDdJQ2h2NVYzVFBmNEtaWnNSOG00azR3Q2dZSUtvWkl6ajBFQXdJd2dZRXhDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFJREFaQ1pYSnNhVzR4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVhTUJnR0ExVUVDZ3dSWjJWdFlYUnBheUJPVDFRdFZrRk1TVVF4RHpBTkJnTlZCQXNNQmxCVUlFbEVUVEVqTUNFR0ExVUVBd3dhWm1GamFHUnBaVzV6ZEZSc2MwTWdNaUJVUlZOVUxVOU9URmt3SGhjTk1qUXdPREUwTURjd09USTRXaGNOTWpnd09USXlNRGN3T1RJNFdqQ0JnVEVMTUFrR0ExVUVCaE1DUkVVeER6QU5CZ05WQkFnTUJrSmxjbXhwYmpFUE1BMEdBMVVFQnd3R1FtVnliR2x1TVJvd0dBWURWUVFLREJGblpXMWhkR2xySUU1UFZDMVdRVXhKUkRFUE1BMEdBMVVFQ3d3R1VGUWdTVVJOTVNNd0lRWURWUVFEREJwbVlXTm9aR2xsYm5OMFZHeHpReUF5SUZSRlUxUXRUMDVNV1RCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkNOc0tmRmI4T3BheExHcmtGUDQvLzNTaVhhYUc3TEo1OGxNQW9EMUtBZEw0N1N6U1dxRS80VlN3bEdJVXBrRXJ1dWlQbjNqbk5CMWZzQU1JUFJ5SWh5ak9UQTNNQWtHQTFVZEV3UUNNQUF3Q3dZRFZSMFBCQVFEQWdYZ01CMEdBMVVkRGdRV0JCVDJPbmhLRk95ZVdEajBacXIrVzY2bnFVQTRXakFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUFydERKQWMvY3NPT0phTysxVXo3bDlhd2pRQ2lhalJnR1E2TFRPN3NzbWJnSWhBTk1zclMyOWU3bzhHaFhzTk9tTGIxMVZyYndnY2J1NzZUQnliNEdHUWVsZyJdLCJ1c2UiOiJzaWciLCJraWQiOiJwdWtfdGxzX3NpZ19yb3RhdGlvbiIsImt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiSTJ3cDhWdnc2bHJFc2F1UVVfal9fZEtKZHBvYnNzbm55VXdDZ1BVb0IwcyIsInkiOiI0N1N6U1dxRV80VlN3bEdJVXBrRXJ1dWlQbjNqbk5CMWZzQU1JUFJ5SWh3IiwiYWxnIjoiRVMyNTYifSx7InVzZSI6ImVuYyIsImtpZCI6InB1a19mZF9lbmMiLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik5RTGFXYnVRREhnU0hhaHFiOXp4bERkaU1DSFhTZ1kwTDlxbDFrN0JWVUUiLCJ5IjoiX1VTZ21xaGxNM3B2YWJrWjJTU19ZRTJRNTd0VHM2cEs5Y0VfdVpCLXUzYyIsImFsZyI6IkVTMjU2In1dfQ.gapTnBwi0g-IUHrLVZxTKN9Vo6wj1evL6YPpZXKS0rFuuYHENE2pEOLBEIZc3f2RsNEYZKH1YCO4pZEg7a6Uig"; } diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksControllerTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksControllerTest.java index 3dc4967..8c90dd0 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksControllerTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksControllerTest.java @@ -37,7 +37,7 @@ class AssetLinksControllerTest { @LocalServerPort private int serverPort; @Test - void getAssetLinksAndroid_ok() { + void test_getAssetLinksAndroid_200() { final String testHostUrl = "http://localhost:" + serverPort; final HttpResponse resp = Unirest.get(testHostUrl + ASSET_LINKS_ENDPOINT_ANDROID).asString(); @@ -45,7 +45,7 @@ void getAssetLinksAndroid_ok() { } @Test - void getAssetLinksIos_ok() { + void test_getAssetLinksIos_200() { final String testHostUrl = "http://localhost:" + serverPort; final HttpResponse resp = Unirest.get(testHostUrl + ASSET_LINKS_ENDPOINT_IOS).asString(); diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksNegativeControllerTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksNegativeControllerTest.java index 642e1bb..5ae9390 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksNegativeControllerTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/AssetLinksNegativeControllerTest.java @@ -41,7 +41,7 @@ class AssetLinksNegativeControllerTest { @InjectMocks private AssetLinksController assetLinksController; @Test - void getAssetLinksAndroid_fileNotFound() { + void test_getAssetLinksAndroid_404() { when(resourceLoader.getResource("classpath:assetlinks.json")).thenReturn(resource); when(resource.exists()).thenReturn(false); @@ -52,7 +52,7 @@ void getAssetLinksAndroid_fileNotFound() { } @Test - void getAssetLinksIos_fileNotFound() { + void test_getAssetLinksIos_404() { when(resourceLoader.getResource("classpath:apple-app-site-association")).thenReturn(resource); when(resource.exists()).thenReturn(false); diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTest.java index 666b756..a223860 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTest.java @@ -18,34 +18,35 @@ import static de.gematik.idp.IdpConstants.FED_AUTH_ENDPOINT; import static de.gematik.idp.IdpConstants.TOKEN_ENDPOINT; +import static de.gematik.idp.data.Oauth2ErrorCode.INVALID_REQUEST; import static de.gematik.idp.data.Oauth2ErrorCode.UNAUTHORIZED_CLIENT; -import static de.gematik.idp.gsi.server.data.GsiConstants.FEDIDP_PAR_AUTH_ENDPOINT; -import static de.gematik.idp.gsi.server.data.GsiConstants.FED_SIGNED_JWKS_ENDPOINT; -import static de.gematik.idp.gsi.server.data.GsiConstants.TLS_CLIENT_CERT_HEADER_NAME; +import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; +import static de.gematik.idp.gsi.server.data.GsiConstants.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.jayway.jsonpath.JsonPath; import de.gematik.idp.IdpConstants; import de.gematik.idp.authentication.UriUtils; -import de.gematik.idp.crypto.CryptoLoader; import de.gematik.idp.field.ClientUtilities; import de.gematik.idp.field.CodeChallengeMethod; import de.gematik.idp.gsi.server.GsiServer; import de.gematik.idp.gsi.server.configuration.GsiConfiguration; import de.gematik.idp.gsi.server.data.ClaimsResponse; -import de.gematik.idp.gsi.server.services.EntityStatementRpService; +import de.gematik.idp.gsi.server.data.RpToken; +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.gsi.server.services.*; import de.gematik.idp.token.IdpJwe; import de.gematik.idp.token.JsonWebToken; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.TimeUnit; import kong.unirest.core.HttpResponse; import kong.unirest.core.HttpStatus; @@ -53,43 +54,36 @@ import kong.unirest.core.Unirest; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; import org.awaitility.Awaitility; import org.jose4j.jwk.PublicJsonWebKey; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.annotation.DirtiesContext.ClassMode; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; @Slf4j -@ActiveProfiles("test-controller") @SpringBootTest(classes = GsiServer.class, webEnvironment = WebEnvironment.RANDOM_PORT) -@DirtiesContext(classMode = ClassMode.AFTER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExtendWith(SpringExtension.class) class FedIdpControllerTest { - @DynamicPropertySource - static void dynamicProperties(final DynamicPropertyRegistry registry) { - registry.add("gsi.requestUriTTL", () -> 5); - } - - @Autowired private EntityStatementRpService entityStatementRpService; - static final List OPENID_PROVIDER_CLAIMS = + private static final List OPENID_PROVIDER_CLAIMS = List.of( "issuer", "signed_jwks_uri", @@ -114,82 +108,129 @@ static void dynamicProperties(final DynamicPropertyRegistry registry) { "claims_supported", "claims_parameter_supported"); - @LocalServerPort private int serverPort; + @DynamicPropertySource + static void dynamicProperties(final DynamicPropertyRegistry registry) { + registry.add("gsi.requestUriTTL", () -> 5); + } + + @Autowired private GsiConfiguration gsiConfiguration; + + private MockMvc mockMvc; + @Autowired private WebApplicationContext context; + @MockBean private TokenRepositoryRp rpTokenRepository; + private static MockedStatic requestValidatorMockedStatic; + private static MockedStatic esReaderMockedStatic; - private TestInfo testInfo; private String testHostUrl; + @LocalServerPort private int serverPort; + private String codeVerifier; + private String redirectUri; + private String fachdienstClientId; + + private static final RpToken VALID_RPTOKEN = + new RpToken(new JsonWebToken(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); + private HttpResponse entityStatementResponseGood; private JsonWebToken entityStatement; - - private HttpResponse sigendJwksResponseGood; - private JsonWebToken sigendJwks; private Map entityStatementbodyClaims; - @Autowired private GsiConfiguration gsiConfiguration; - private final String certFromRequest = + + private HttpResponse signedJwksResponseGood; + private JsonWebToken signedJwks; + + private static final String CERT1_FROM_REQUEST = "-----BEGIN%20CERTIFICATE-----%0AMIIDszCCApugAwIBAgIUY%2FqefKABeWr36nT%2Brw9hJsbYFu8wDQYJKoZIhvcNAQEL%0ABQAwdjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy%0AbGluMRkwFwYDVQQKDBBnZW1hdGlrVEVTVC1PTkxZMQ8wDQYDVQQLDAZQVCBJRE0x%0AGTAXBgNVBAMMEGZhZGlUbHNDbGllbnRSc2EwHhcNMjQwNjEzMDcxNjUyWhcNMjUw%0ANjEzMDcxNjUyWjB2MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYD%0AVQQHDAZCZXJsaW4xGTAXBgNVBAoMEGdlbWF0aWtURVNULU9OTFkxDzANBgNVBAsM%0ABlBUIElETTEZMBcGA1UEAwwQZmFkaVRsc0NsaWVudFJzYTCCASIwDQYJKoZIhvcN%0AAQEBBQADggEPADCCAQoCggEBAKiQaMTyY%2FlTTO9V4YJq7xsfN8l0%2BSqe2rRRasVU%0A8wenG8eohk99d1i5%2Fh08%2B%2BK1A5FX9GxgWh0RXGotpvbVvM7kzdOWxBJIK7j68R9g%0A%2F6B%2BKKO89rywLiJkxRT%2BOA4dusqocGDKmqFYZC1ntt2nSsSLlX3OuDC%2F1Thlhz2i%0AEGtweuYRL3zPeDXiegdyjRCY%2F9Xe%2FwaC4amuuJ5JkE5EsM0mL09kfkZCzdx8j2KK%0AqYTH2TYmiOG16CIVyZi9pE%2BKEHw95MIIcrzrO6QLWXcl7Y82rwVeeoUSicLBEydd%0A4YmsZ6pp%2BKGH0b9ycQO%2Bxs2uv79%2B5Zza9Q4OazEka4N0LyMCAwEAAaM5MDcwCQYD%0AVR0TBAIwADALBgNVHQ8EBAMCBeAwHQYDVR0OBBYEFMmogwgia7kONxur5UWBDX5g%0ABP0HMA0GCSqGSIb3DQEBCwUAA4IBAQAFK6nct1YVLMR6Tznh6ZrsvYs0UzCElUGM%0AnJtYaeCTgQPVKigQC4SPf%2FJp9qychooSbS7gbponndXgGIz8VFmt9y4d4q0uZKOr%0ALp7qcK%2BgQdvBts5TDZH20IiwW5b6VyGp%2Fos8fqR8WIt7fHdNz6Mu1fh2HsB4YjV9%0AxbbXTcKSzS6TROzh9bt2ubFX4ex56j6Mniy3DNF6zsW4kdh7naB%2FLfXvtH276Gj%2B%0AInhaF1sBLI8IIyQ5K2q2MJaly%2F8wiOys7FuG7duD1Lmh2kRO0FZkXsaQJmbZncUs%0A%2B4tgmnpEVgZ0FlKQ1BDAl0o0e7QbVRMiI2gjz7itOWFiUXvnMNIA%0A-----END%20CERTIFICATE-----%0A"; - private X509Certificate certFromEntityStmtRpService; - private X509Certificate otherCert; + + private static final String KEY_ID = "puk_fd_enc"; @SneakyThrows @BeforeAll void setup() { testHostUrl = "http://localhost:" + serverPort; + codeVerifier = ClientUtilities.generateCodeVerifier(); + redirectUri = testHostUrl + "/AS"; + fachdienstClientId = testHostUrl; entityStatementResponseGood = retrieveEntityStatement(); - sigendJwksResponseGood = retrieveSignedJwks(); assertThat(entityStatementResponseGood.getStatus()).isEqualTo(HttpStatus.OK); entityStatement = new JsonWebToken(entityStatementResponseGood.getBody()); - sigendJwks = new JsonWebToken(sigendJwksResponseGood.getBody()); entityStatementbodyClaims = entityStatement.extractBodyClaims(); - certFromEntityStmtRpService = - CryptoLoader.getCertificateFromPem( - java.net.URLDecoder.decode(certFromRequest, StandardCharsets.UTF_8).getBytes()); - - otherCert = - CryptoLoader.getCertificateFromPem( - FileUtils.readFileToByteArray( - new File("src/test/resources/keys/fachdienstTlsC_TEST_ONLY.pem"))); + signedJwksResponseGood = retrieveSignedJwks(); + signedJwks = new JsonWebToken(signedJwksResponseGood.getBody()); } + @SneakyThrows @BeforeEach void init(final TestInfo testInfo) { - this.testInfo = testInfo; + mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); log.info("START UNIT TEST: {}", testInfo.getDisplayName()); + + Mockito.doReturn(VALID_RPTOKEN).when(rpTokenRepository).getEntityStatementRp(any()); + + requestValidatorMockedStatic = Mockito.mockStatic(RequestValidator.class); + esReaderMockedStatic = Mockito.mockStatic(EntityStatementRpReader.class); + + // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem + final String JWK_AS_STRING_PUK_FED_ENC = + "{\"use\": \"enc\",\"kid\": \"" + + KEY_ID + + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" + + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" + + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; + + esReaderMockedStatic + .when(() -> EntityStatementRpReader.getRpEncKey(any())) + .thenReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)); + } + + @AfterEach + void tearDown() { + requestValidatorMockedStatic.close(); + esReaderMockedStatic.close(); } /************************** ENTITY_STATEMENT_ENDPOINT *****************/ + @SuppressWarnings("unchecked") + private Map getInnerClaimMap( + final Map claimMap, final String key) { + return Objects.requireNonNull((Map) claimMap.get(key), "missing claim: " + key); + } + + private HttpResponse retrieveEntityStatement() { + return Unirest.get(testHostUrl + IdpConstants.ENTITY_STATEMENT_ENDPOINT).asString(); + } + @Test - void entityStatementResponse_ContentTypeEntityStatement() { + void test_entityStatementResponse_ContentTypeEntityStatement() { assertThat(entityStatementResponseGood.getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0)) .isEqualTo("application/entity-statement+jwt;charset=UTF-8"); } @Test - void entityStatementResponse_JoseHeader() { + void test_entityStatementResponse_JoseHeader() { assertThat(entityStatement.extractHeaderClaims()).containsOnlyKeys("typ", "alg", "kid"); } @Test - void entityStatement_BodyClaimsComplete() { + void test_entityStatement_BodyClaimsComplete() { assertThat(entityStatementbodyClaims) .containsOnlyKeys("iss", "sub", "iat", "exp", "jwks", "authority_hints", "metadata"); } @Test - void entityStatement_ContainsJwks() { + void test_entityStatement_ContainsJwks() { assertThat(entityStatementbodyClaims.get("jwks")).isNotNull(); } @Test - void entityStatement_MetadataClaims() { + void test_entityStatement_MetadataClaims() { final Map metadata = getInnerClaimMap(entityStatementbodyClaims, "metadata"); assertThat(metadata).containsOnlyKeys("openid_provider", "federation_entity"); } @Test - void entityStatement_OpenidProviderClaimsComplete() { + void test_entityStatement_OpenidProviderClaimsComplete() { final Map metadata = getInnerClaimMap(entityStatementbodyClaims, "metadata"); final Map openidProvider = Objects.requireNonNull( @@ -201,7 +242,7 @@ void entityStatement_OpenidProviderClaimsComplete() { @SuppressWarnings("unchecked") @Test - void entityStatement_OpenidProviderClaimsContentCorrect() { + void test_entityStatement_OpenidProviderClaimsContentCorrect() { final String gsiServerUrl = "https://gsi.dev.gematik.solutions"; final Map metadata = getInnerClaimMap(entityStatementbodyClaims, "metadata"); @@ -261,7 +302,7 @@ void entityStatement_OpenidProviderClaimsContentCorrect() { @SuppressWarnings("unchecked") @Test - void entityStatement_FederationEntityClaimsContentCorrect() { + void test_entityStatement_FederationEntityClaimsContentCorrect() { final Map metadata = getInnerClaimMap(entityStatementbodyClaims, "metadata"); final Map federationEntity = Objects.requireNonNull( @@ -276,53 +317,45 @@ void entityStatement_FederationEntityClaimsContentCorrect() { .containsEntry("homepage_uri", "https://idp4711.de"); } - @SuppressWarnings("unchecked") - private Map getInnerClaimMap( - final Map claimMap, final String key) { - return Objects.requireNonNull((Map) claimMap.get(key), "missing claim: " + key); - } + /************************** SIGNED_JWKS_ENDPOINT *****************/ - private HttpResponse retrieveEntityStatement() { - return Unirest.get(testHostUrl + IdpConstants.ENTITY_STATEMENT_ENDPOINT).asString(); + private HttpResponse retrieveSignedJwks() { + return Unirest.get(testHostUrl + FED_SIGNED_JWKS_ENDPOINT).asString(); } - /************************** SIGNED_JWKS_ENDPOINT *****************/ @Test - void sigendJwksResponse_ContentTypeEntityStatement() { - assertThat(sigendJwksResponseGood.getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0)) + void test_sigendJwksResponse_ContentTypeEntityStatement() { + assertThat(signedJwksResponseGood.getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0)) .isEqualTo("application/jwk-set+json;charset=UTF-8"); } @Test - void signedJwksResponse_JoseHeader() { - assertThat(sigendJwks.extractHeaderClaims()).containsOnlyKeys("typ", "alg", "kid"); + void test_signedJwksResponse_JoseHeader() { + assertThat(signedJwks.extractHeaderClaims()).containsOnlyKeys("typ", "alg", "kid"); } @Test - void signedJwksResponse_BodyClaims() { - assertThat(sigendJwks.extractBodyClaims()).containsOnlyKeys("keys", "iss", "iat"); + void test_signedJwksResponse_BodyClaims() { + assertThat(signedJwks.extractBodyClaims()).containsOnlyKeys("keys", "iss", "iat"); } @Test - void signedJwksResponse_Keys() { + void test_signedJwksResponse_Keys() { final List> keyList = - (List>) sigendJwks.getBodyClaims().get("keys"); + (List>) signedJwks.getBodyClaims().get("keys"); assertThat(keyList.get(0).keySet()) .containsExactlyInAnyOrder("use", "kid", "kty", "crv", "x", "y", "alg"); } @Test - void signedJwksResponse_NumberOfKeys() { + void test_signedJwksResponse_NumberOfKeys() { final List> keyList = - (List>) sigendJwks.getBodyClaims().get("keys"); + (List>) signedJwks.getBodyClaims().get("keys"); assertThat(keyList).hasSize(2); } - private HttpResponse retrieveSignedJwks() { - return Unirest.get(testHostUrl + FED_SIGNED_JWKS_ENDPOINT).asString(); - } - /************************** FEDIDP_PUSHED AUTH_ENDPOINT *****************/ + @SneakyThrows @ValueSource( strings = { "urn:telematik:geburtsdatum urn:telematik:alter openid", @@ -331,144 +364,152 @@ private HttpResponse retrieveSignedJwks() { "urn:telematik:geschlecht urn:telematik:versicherter urn:telematik:email" }) @ParameterizedTest(name = "parRequest_validScope_ResponseStatus_CREATED scope: {0}") - void parRequest_validScope_ResponseStatus_CREATED(final String scope) { - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", scope); - - final HttpResponse resp = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", testHostUrl) - .field("state", "state_Fachdienst") - .field("redirect_uri", testHostUrl + "/AS") - .field("code_challenge", "P62rd1KSUnScGIEs1WrpYj3g_poTqmx8mM4msxehNdk") - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", scope) - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(resp.getStatus()).isEqualTo(HttpStatus.CREATED); + void test_postPar_validScope_201(final String scope) { + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()) + .andReturn(); } + @SneakyThrows @ValueSource(strings = {"gematik-ehealth-loa-high", "gematik-ehealth-loa-substantial"}) @ParameterizedTest(name = "parRequest_validAcrValue_ResponseStatus_CREATED acr: {0}") - void parRequest_validAcrValue_ResponseStatus_CREATED(final String acr_value) { - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - final HttpResponse resp = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", testHostUrl) - .field("state", "state_Fachdienst") - .field("redirect_uri", testHostUrl + "/AS") - .field("code_challenge", "P62rd1KSUnScGIEs1WrpYj3g_poTqmx8mM4msxehNdk") - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", acr_value) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(resp.getStatus()).isEqualTo(HttpStatus.CREATED); + void test_postPar_validAcrValue_201(final String acr_value) { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", acr_value) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()); } - /* - * message nr.2 ... message nr.7 - * test auto registration and session handling - */ + @SneakyThrows @Test - void parRequest_authRequestUriPar() { - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", testHostUrl) - .field("state", "state_Fachdienst") - .field("redirect_uri", testHostUrl + "/AS") - .field("code_challenge", "P62rd1KSUnScGIEs1WrpYj3g_poTqmx8mM4msxehNdk") - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); - - Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg7 = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("client_id", testHostUrl) - .asString(); - - assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.OK); + void test_postPar_validClaims_201() { + final JsonObject idToken = new JsonObject(); + + final JsonObject amr = new JsonObject(); + final JsonArray amrValues = new JsonArray(); + amrValues.add("urn:telematik:auth:eGK"); + amr.add("values", amrValues); + amr.addProperty("essential", true); + + final JsonObject acr = new JsonObject(); + final JsonArray acrValues = new JsonArray(); + acrValues.add(ACR_HIGH); + acr.add("values", acrValues); + acr.addProperty("essential", true); + + final JsonObject email = new JsonObject(); + email.addProperty("essential", true); + final JsonObject name = new JsonObject(); + name.addProperty("essential", false); + + idToken.add("amr", amr); + idToken.add("acr", acr); + idToken.add("urn:telematik:claims:email", email); + idToken.add("urn:telematik:claims:given_name", name); + + final JsonObject claims = new JsonObject(); + claims.add("id_token", idToken); + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:display_name") + .param("acr_values", "gematik-ehealth-loa-high") + .param("claims", claims.toString()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()) + .andReturn(); } - /* - * message nr.2 ... message nr.7 - * do auto registration and send invalid authorization request - */ + @SneakyThrows @Test - void authRequest_invalidParameter() { - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", testHostUrl) - .field("state", "state_Fachdienst") - .field("redirect_uri", testHostUrl + "/AS") - .field("code_challenge", "P62rd1KSUnScGIEs1WrpYj3g_poTqmx8mM4msxehNdk") - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); - - Unirest.config().reset().followRedirects(false); - - // variant invalid request_uri - final HttpResponse respMsg7_a = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", "InvalidRequestUri") - .queryString("client_id", testHostUrl) - .asString(); - assertThat(respMsg7_a.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + void test_postPar_claimsParamNotAJsonObject_400() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:display_name") + .param("acr_values", "gematik-ehealth-loa-high") + .param("claims", "invalidJsonStruct") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(JsonPath.read(respMsg3.getContentAsString(), "error_description").toString()) + .hasToString("parameter claims is not a JSON object"); + } - // variant invalid client_id - final HttpResponse respMsg7_b = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("client_id", "InvalidClientId") - .asString(); - assertThat(respMsg7_b.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + @SneakyThrows + @Test + void test_postPar_claimsParamJsonButNotClaims_400() { + + final JsonObject claims = new JsonObject(); + claims.addProperty("invalid", "any"); + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:display_name") + .param("acr_values", "gematik-ehealth-loa-high") + .param("claims", claims.toString()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(JsonPath.read(respMsg3.getContentAsString(), "error_description").toString()) + .hasToString("parameter claims has invalid structure"); } @Test - void parRequest_missingParameterResponseType_ResponseStatus_BAD_REQUEST() { + void test_postPar_missingParameterResponseType_400() { final HttpResponse resp = Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", testHostUrl) .field("client_id", testHostUrl) .field("state", "state_Fachdienst") .field("redirect_uri", testHostUrl + "/AS") @@ -483,7 +524,7 @@ void parRequest_missingParameterResponseType_ResponseStatus_BAD_REQUEST() { } @Test - void parRequest_InvalidGetOnPostMapping_ResponseStatus_BAD_REQUEST() { + void test_postPar_invalidGetOnPostMapping_405() { final HttpResponse resp = Unirest.get(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) .queryString("client_id", testHostUrl) @@ -499,72 +540,356 @@ void parRequest_InvalidGetOnPostMapping_ResponseStatus_BAD_REQUEST() { assertThat(resp.getStatus()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED); } + /** Increase Test coverage of Landing page endpoint */ + @SneakyThrows + @Test + void test_postPar_invalidClientId_400() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", "invalidClientId") + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isBadRequest()); + } + + /** + * ClientID may contain just http and not https. This should work as well for local development + * environment. + */ + @SneakyThrows + @Test + void test_postPar_validClientId_201() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", "http://127.0.0.1:8084") + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()); + } + + @SneakyThrows + @Test + void test_postPar_validAmr_201() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .param("amr", "urn:telematik:auth:eGK") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()); + } + + @SneakyThrows + @Test + void test_postPar_invalidAmr_400() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .param("amr", "urn:telematik:auth:invalid") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error_description").value(containsString("amr: must match"))); + } + + @SneakyThrows + @Test + void test_postPar_validPrompt_201() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .param("prompt", "login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()); + } + + @SneakyThrows + @Test + void test_postPar_validMaxAge_201() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .param("max_age", "0") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()); + } + + @SneakyThrows + @Test + void test_postPar_validTlsCertificateHeader_201() { + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .header(TLS_CLIENT_CERT_HEADER_NAME, CERT1_FROM_REQUEST) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isCreated()); + } + + @SneakyThrows + @Test + void test_postPar_invalidTlsCertificateHeader_anyString_401() { + + requestValidatorMockedStatic + .when( + () -> + RequestValidator.validateCertificate( + "AnyInvalidCert", VALID_RPTOKEN, gsiConfiguration.isClientCertRequired())) + .thenThrow( + new GsiException( + UNAUTHORIZED_CLIENT, + "client certificate in tls handshake is not a valid x509 certificate", + org.springframework.http.HttpStatus.UNAUTHORIZED)); + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .header(TLS_CLIENT_CERT_HEADER_NAME, "AnyInvalidCert") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value(equalTo(UNAUTHORIZED_CLIENT.getSerializationValue()))) + .andExpect( + jsonPath("$.error_description") + .value("client certificate in tls handshake is not a valid x509 certificate")); + } + + @SneakyThrows + @Test + void test_postPar_certInHeaderAndInEntityStatementDontMatch_401() { + + requestValidatorMockedStatic + .when( + () -> + RequestValidator.validateCertificate( + CERT1_FROM_REQUEST, VALID_RPTOKEN, gsiConfiguration.isClientCertRequired())) + .thenThrow( + new GsiException( + UNAUTHORIZED_CLIENT, + "client certificate in tls handshake does not match any certificate in entity" + + " statement/signed_jwks", + org.springframework.http.HttpStatus.UNAUTHORIZED)); + + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .header(TLS_CLIENT_CERT_HEADER_NAME, CERT1_FROM_REQUEST) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value(equalTo(UNAUTHORIZED_CLIENT.getSerializationValue()))) + .andExpect( + jsonPath("$.error_description") + .value( + "client certificate in tls handshake does not match any certificate in entity" + + " statement/signed_jwks")); + } + /************************** FEDIDP AUTH_ENDPOINT *****************/ + @SneakyThrows @Test - void authRequest_invalidRequestUri_ResponseStatus_BAD_REQUEST() { + void test_getLandingPage_200() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", testHostUrl) + .param("state", "state_Fachdienst") + .param("redirect_uri", testHostUrl + "/AS") + .param("code_challenge", "P62rd1KSUnScGIEs1WrpYj3g_poTqmx8mM4msxehNdk") + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); + + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); final HttpResponse resp = Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", "myInvalidRequestUri") + .queryString("request_uri", requestUri) .queryString("client_id", testHostUrl) .asString(); - assertThat(resp.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(resp.getStatus()).isEqualTo(HttpStatus.OK); } + /* + * message nr.2 ... message nr.7 + * do auto registration and send invalid authorization request + */ @SneakyThrows @Test - void parRequest_authRequestUriPar_claimsResponse_contains_httpStatus_200() { - - final String KEY_ID = "puk_fd_enc"; - // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem - final String JWK_AS_STRING_PUK_FED_ENC = - "{\"use\": \"enc\",\"kid\": \"" - + KEY_ID - + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" - + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" - + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; + void test_getLandingPage_invalidClientId_invalidRequestUri_400() { + + requestValidatorMockedStatic + .when(() -> RequestValidator.validateAuthRequestParams(any(), any())) + .thenThrow( + new GsiException( + INVALID_REQUEST, + "invalid code_verifier", + org.springframework.http.HttpStatus.BAD_REQUEST)); + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", testHostUrl) + .param("state", "state_Fachdienst") + .param("redirect_uri", testHostUrl + "/AS") + .param("code_challenge", "P62rd1KSUnScGIEs1WrpYj3g_poTqmx8mM4msxehNdk") + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration( - testHostUrl, - testHostUrl + "/AS", - "urn:telematik:given_name urn:telematik:versicherter openid"); + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); - Mockito.doReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)) - .when(entityStatementRpService) - .getRpEncKey(any()); + // variant invalid request_uri + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", "InvalidRequestUri") + .param("client_id", testHostUrl)) + .andExpect(status().isBadRequest()); - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; + // variant invalid client_id + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("client_id", "InvalidClientId")) + .andExpect(status().isBadRequest()); + } - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); + @SneakyThrows + @Test + void test_getRequestedClaims_200() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg6a = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("device_type", "unittest") - .asString(); + final MockHttpServletResponse respMsg6a = + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("device_type", "unittest")) + .andReturn() + .getResponse(); assertThat(respMsg6a.getStatus()).isEqualTo(HttpStatus.OK); + final ClaimsResponse claimsResponse = - new ObjectMapper().readValue(respMsg6a.getBody(), ClaimsResponse.class); + new ObjectMapper().readValue(respMsg6a.getContentAsString(), ClaimsResponse.class); + assertThat(claimsResponse).isNotNull(); assertThat(claimsResponse.getRequestedClaims()) .containsExactlyInAnyOrder( @@ -576,64 +901,122 @@ void parRequest_authRequestUriPar_claimsResponse_contains_httpStatus_200() { @SneakyThrows @Test - void request_uri_expired_httpStatus_400() { + void test_getRequestedClaims_requestUriExpired_400() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - final String KEY_ID = "puk_fd_enc"; - // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem - final String JWK_AS_STRING_PUK_FED_ENC = - "{\"use\": \"enc\",\"kid\": \"" - + KEY_ID - + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" - + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" - + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration( - testHostUrl, - testHostUrl + "/AS", - "urn:telematik:given_name urn:telematik:versicherter openid"); + waitForSeconds(gsiConfiguration.getRequestUriTTL() + 2); - Mockito.doReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)) - .when(entityStatementRpService) - .getRpEncKey(any()); + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("device_type", "unittest")) + .andExpect(status().isBadRequest()); + } - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; + @SneakyThrows + @Test + void test_getAuthorizationCode_validSelectedClaims_302() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "request_uri"); + + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("device_type", "unittest")) + .andExpect(status().isOk()); + + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("user_id", "12345678") + .param("selected_claims", "urn:telematik:claims:id")) + .andExpect(status().isFound()); + } - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); + @SneakyThrows + @Test + void test_getAuthorizationCode_invalidSelectedClaims_400() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); + assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - waitForSeconds(gsiConfiguration.getRequestUriTTL() + 2); - - Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg6a = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("device_type", "unittest") - .asString(); - - assertThat(respMsg6a.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); - } + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "request_uri"); + + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("device_type", "unittest")) + .andExpect(status().isOk()); + + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("user_id", "12345678") + .param("selected_claims", "urn:telematik:claims:given_name")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error_description").value("selected claims exceed scopes in PAR")); + } /************************** FEDIDP_TOKEN_ENDPOINT *****************/ @Test - void tokenRequest_invalidCode_ResponseStatus_BAD_REQUEST() { + void test_getTokensForCode_invalidCode_400() { final HttpResponse httpResponse = Unirest.post(testHostUrl + TOKEN_ENDPOINT) .field("grant_type", "authorization_code") @@ -648,7 +1031,7 @@ void tokenRequest_invalidCode_ResponseStatus_BAD_REQUEST() { } @Test - void tokenRequest_invalidGrantType_ResponseStatus_BAD_REQUEST() { + void test_getTokensForCode_invalidGrantType_400() { final HttpResponse httpResponse = Unirest.post(testHostUrl + TOKEN_ENDPOINT) .field("grant_type", "auth") @@ -663,7 +1046,7 @@ void tokenRequest_invalidGrantType_ResponseStatus_BAD_REQUEST() { } @Test - void tokenRequest_InvalidGetOnPostMapping_ResponseStatus_BAD_REQUEST() { + void test_getTokensForCode_invalidGetOnPostMapping_405() { final HttpResponse resp = Unirest.get(testHostUrl + TOKEN_ENDPOINT) .queryString("client_id", testHostUrl) @@ -678,72 +1061,61 @@ void tokenRequest_InvalidGetOnPostMapping_ResponseStatus_BAD_REQUEST() { @SneakyThrows @Test - void parRequest_authRequestUriPar_tokenResponse_contains_httpStatus_200() { - - final String KEY_ID = "puk_fd_enc"; - // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem - final String JWK_AS_STRING_PUK_FED_ENC = - "{\"use\": \"enc\",\"kid\": \"" - + KEY_ID - + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" - + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" - + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; + void test_getTokensForCode_200() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - Mockito.doReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)) - .when(entityStatementRpService) - .getRpEncKey(any()); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); - Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg7 = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("user_id", "12345678") - .asString(); + final MockHttpServletResponse respMsg7 = + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("user_id", "12345678")) + .andReturn() + .getResponse(); assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.FOUND); - final String code = - UriUtils.extractParameterValue(respMsg7.getHeaders().get("Location").get(0), "code"); - final HttpResponse httpResponse = - Unirest.post(testHostUrl + TOKEN_ENDPOINT) - .field("grant_type", "authorization_code") - .field("code", code) - .field("code_verifier", codeVerifier) - .field("client_id", fachdienstClientId) - .field("redirect_uri", redirectUri) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .header(HttpHeaders.USER_AGENT, "IdP-Client") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .asJson(); - assertThat(httpResponse.getStatus()).isEqualTo(HttpStatus.OK); - - final String idTokenEncrypted = httpResponse.getBody().getObject().getString("id_token"); + final String code = + UriUtils.extractParameterValue(respMsg7.getHeaders("Location").get(0), "code"); + + final MockHttpServletResponse resp = + mockMvc + .perform( + post(testHostUrl + TOKEN_ENDPOINT) + .param("grant_type", "authorization_code") + .param("code", code) + .param("code_verifier", codeVerifier) + .param("client_id", fachdienstClientId) + .param("redirect_uri", redirectUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(HttpHeaders.USER_AGENT, "IdP-Client") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)) + .andReturn() + .getResponse(); + + assertThat(resp.getStatus()).isEqualTo(HttpStatus.OK); + + final String idTokenEncrypted = JsonPath.read(resp.getContentAsString(), "$.id_token"); final IdpJwe idpJwe = new IdpJwe(idTokenEncrypted); // verify that token is encrypted and check kid @@ -752,520 +1124,221 @@ void parRequest_authRequestUriPar_tokenResponse_contains_httpStatus_200() { @SneakyThrows @Test - void parRequest_authRequestUriPar_userConsent_tokenResponse_contains_httpStatus_200() { + void test_getTokensForCode_withSelectedClaims_200() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - final String KEY_ID = "puk_fd_enc"; - // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem - final String JWK_AS_STRING_PUK_FED_ENC = - "{\"use\": \"enc\",\"kid\": \"" - + KEY_ID - + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" - + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" - + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:versicherter openid"); - - Mockito.doReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)) - .when(entityStatementRpService) - .getRpEncKey(any()); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); - Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg6a = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("device_type", "unittest") - .asString(); + final MockHttpServletResponse respMsg6a = + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("device_type", "unittest")) + .andReturn() + .getResponse(); - final HttpResponse respMsg7 = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("user_id", "12345678") - .queryString( - "selected_claims", "urn:telematik:claims:profession urn:telematik:claims:id") - .asString(); + assertThat(respMsg6a.getStatus()).isEqualTo(HttpStatus.OK); - assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.FOUND); - final String code = - UriUtils.extractParameterValue(respMsg7.getHeaders().get("Location").get(0), "code"); + final MockHttpServletResponse respMsg7 = + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("user_id", "12345678") + .param( + "selected_claims", + "urn:telematik:claims:profession urn:telematik:claims:id")) + .andReturn() + .getResponse(); - final HttpResponse httpResponse = - Unirest.post(testHostUrl + TOKEN_ENDPOINT) - .field("grant_type", "authorization_code") - .field("code", code) - .field("code_verifier", codeVerifier) - .field("client_id", fachdienstClientId) - .field("redirect_uri", redirectUri) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .header(HttpHeaders.USER_AGENT, "IdP-Client") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .asJson(); - assertThat(httpResponse.getStatus()).isEqualTo(HttpStatus.OK); + assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.FOUND); - final String idTokenEncrypted = httpResponse.getBody().getObject().getString("id_token"); + final String code = + UriUtils.extractParameterValue(respMsg7.getHeaders("Location").get(0), "code"); + + final MockHttpServletResponse resp = + mockMvc + .perform( + post(testHostUrl + TOKEN_ENDPOINT) + .param("grant_type", "authorization_code") + .param("code", code) + .param("code_verifier", codeVerifier) + .param("client_id", fachdienstClientId) + .param("redirect_uri", redirectUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(HttpHeaders.USER_AGENT, "IdP-Client") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)) + .andReturn() + .getResponse(); + + assertThat(resp.getStatus()).isEqualTo(HttpStatus.OK); + + final String idTokenEncrypted = JsonPath.read(resp.getContentAsString(), "$.id_token"); final IdpJwe idpJwe = new IdpJwe(idTokenEncrypted); // verify that token is encrypted and check kid assertThat(idpJwe.extractHeaderClaims()).containsEntry("kid", KEY_ID); } - @Test - void parRequest_validAmr_httpsStatus_201() { - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .field("amr", "urn:telematik:auth:eGK") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - } - - @Test - void parRequest_invalidAmr_httpsStatus_400_message() { - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .field("amr", "urn:telematik:auth:invalid") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asJson(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(respMsg3.getBody().getObject().getString("error_description")) - .contains("amr: must match"); - } - - @Test - void parRequest_prompt_httpsStatus_201() { - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .field("prompt", "login") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asJson(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - } - - @Test - void parRequest_max_age_httpsStatus_201() { - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .field("max_age", "0") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asJson(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - } - - @SneakyThrows - @Test - void parRequest_authRequestUriPar_invalidUserConsent_httpStatus_400() { - - final String KEY_ID = "puk_fd_enc"; - // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem - final String JWK_AS_STRING_PUK_FED_ENC = - "{\"use\": \"enc\",\"kid\": \"" - + KEY_ID - + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" - + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" - + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:versicherter openid"); - - Mockito.doReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)) - .when(entityStatementRpService) - .getRpEncKey(any()); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); - - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("device_type", "unittest") - .asString(); - - final HttpResponse respMsg7 = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("user_id", "12345678") - .queryString("selected_claims", "urn:telematik:claims:given_name") - .asJson(); - - assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(respMsg7.getBody().toString()).contains("selected claims exceed scopes in PAR"); - } - - /** Increase Test coverage of Landing page endpoint */ - @Test - void parRequest_authRequestUriPar_invalidClientId_httpStatus_400() { - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", "invalidClientId") - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); - } - - /** - * ClientID may contain just http and not https. This should work as well for local development - * environment. - */ - @Test - void parRequest_authRequestUriPar_ClientIdHttp_httpStatus_201() { - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", "http://127.0.0.1:8084") - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - } - /** Increase Test coverage of Token endpoint */ + @SneakyThrows @Test - void - parRequest_authRequestUriPar_tokenRequest_invalidRedirectUri_invalidCodeVerifier_contains_httpStatus_400() { - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:given_name openid"); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; + void test_getTokensForCode_invalidRedirectUri_invalidCodeVerifier_400() { + + requestValidatorMockedStatic + .when(() -> RequestValidator.verifyRedirectUri("invalidRedirectUri", redirectUri)) + .thenThrow( + new GsiException( + INVALID_REQUEST, + "invalid redirect_uri", + org.springframework.http.HttpStatus.BAD_REQUEST)); + + requestValidatorMockedStatic + .when( + () -> + RequestValidator.verifyCodeVerifier( + "invalidCodeVerifier", ClientUtilities.generateCodeChallenge(codeVerifier))) + .thenThrow( + new GsiException( + INVALID_REQUEST, + "invalid redirect_uri", + org.springframework.http.HttpStatus.BAD_REQUEST)); + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:given_name openid") + .param("acr_values", "gematik-ehealth-loa-high") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:given_name openid") - .field("acr_values", "gematik-ehealth-loa-high") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "request_uri"); - Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg7 = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("user_id", "12345678") - .asString(); + final MockHttpServletResponse respMsg7 = + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("user_id", "12345678")) + .andReturn() + .getResponse(); assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.FOUND); - final String code = - UriUtils.extractParameterValue(respMsg7.getHeaders().get("Location").get(0), "code"); - final HttpResponse httpResponse1 = - Unirest.post(testHostUrl + TOKEN_ENDPOINT) - .field("grant_type", "authorization_code") - .field("code", code) - .field("code_verifier", "invalidCodeVerifier") - .field("client_id", fachdienstClientId) - .field("redirect_uri", redirectUri) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .header(HttpHeaders.USER_AGENT, "IdP-Client") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .asJson(); - assertThat(httpResponse1.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); - - final HttpResponse httpResponse2 = - Unirest.post(testHostUrl + TOKEN_ENDPOINT) - .field("grant_type", "authorization_code") - .field("code", code) - .field("code_verifier", codeVerifier) - .field("client_id", fachdienstClientId) - .field("redirect_uri", "invalidRedirectUri") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .header(HttpHeaders.USER_AGENT, "IdP-Client") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .asJson(); - assertThat(httpResponse2.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + final String code = + UriUtils.extractParameterValue(respMsg7.getHeaders("Location").get(0), "code"); + + // invalid code verifier + mockMvc + .perform( + post(testHostUrl + TOKEN_ENDPOINT) + .param("grant_type", "authorization_code") + .param("code", code) + .param("code_verifier", "invalidCodeVerifier") + .param("client_id", fachdienstClientId) + .param("redirect_uri", redirectUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(HttpHeaders.USER_AGENT, "IdP-Client") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + + // invalid redirect uri + mockMvc + .perform( + post(testHostUrl + TOKEN_ENDPOINT) + .param("grant_type", "authorization_code") + .param("code", code) + .param("code_verifier", codeVerifier) + .param("client_id", fachdienstClientId) + .param("redirect_uri", "invalidRedirectUri") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(HttpHeaders.USER_AGENT, "IdP-Client")) + .andExpect(status().isBadRequest()); } @SneakyThrows @Test - void parRequest_tlsCertificateHeader() { - - Mockito.doReturn(certFromEntityStmtRpService) - .when(entityStatementRpService) - .getRpTlsClientCert(any()); + void test_getTokensForCode_tlsCertificateHeader_200() { + + final MockHttpServletResponse respMsg3 = + mockMvc + .perform( + post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) + .param("client_id", fachdienstClientId) + .param("state", "state_Fachdienst") + .param("redirect_uri", redirectUri) + .param("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) + .param("code_challenge_method", CodeChallengeMethod.S256.toString()) + .param("response_type", "code") + .param("nonce", "42") + .param("scope", "urn:telematik:versicherter openid") + .param("acr_values", "gematik-ehealth-loa-high") + .header(TLS_CLIENT_CERT_HEADER_NAME, CERT1_FROM_REQUEST) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andReturn() + .getResponse(); - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .header(TLS_CLIENT_CERT_HEADER_NAME, certFromRequest) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - } - - @SneakyThrows - @Test - void parRequest_invalidTlsCertificateHeader_anyString() { - Mockito.doReturn(certFromEntityStmtRpService) - .when(entityStatementRpService) - .getRpTlsClientCert(any()); + final String requestUri = JsonPath.read(respMsg3.getContentAsString(), "$.request_uri"); - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .header(TLS_CLIENT_CERT_HEADER_NAME, "AnyInvalidString") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asJson(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(respMsg3.getBody().getObject().getString("error")) - .isEqualTo(UNAUTHORIZED_CLIENT.getSerializationValue()); - assertThat(respMsg3.getBody().getObject().getString("error_description")) - .contains("client certificate in tls handshake is not a valid x509 certificate"); - } - - @SneakyThrows - @Test - void parRequest_certInHeaderAndInEntityStatementDontMatch() { - - Mockito.doReturn(otherCert).when(entityStatementRpService).getRpTlsClientCert(any()); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .header(TLS_CLIENT_CERT_HEADER_NAME, certFromRequest) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asJson(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(respMsg3.getBody().getObject().getString("error")) - .isEqualTo(UNAUTHORIZED_CLIENT.getSerializationValue()); - assertThat(respMsg3.getBody().getObject().getString("error_description")) - .contains( - "client certificate in tls handshake does not match certificate in entity" - + " statement/signed_jwks"); - } - - @SneakyThrows - @Test - void tokenRequest_tlsCertificateHeader() { - final String KEY_ID = "puk_fd_enc"; - // key from gra-server/src/main/resources/keys/gras-enc-pubkey.pem - final String JWK_AS_STRING_PUK_FED_ENC = - "{\"use\": \"enc\",\"kid\": \"" - + KEY_ID - + "\",\"kty\": \"EC\",\"crv\": \"P-256\",\"x\":" - + " \"NQLaWbuQDHgSHahqb9zxlDdiMCHXSgY0L9ql1k7BVUE\",\"y\":" - + " \"_USgmqhlM3pvabkZ2SS_YE2Q57tTs6pK9cE_uZB-u3c\"}"; - - Mockito.doNothing() - .when(entityStatementRpService) - .doAutoregistration(testHostUrl, testHostUrl + "/AS", "urn:telematik:versicherter openid"); - - Mockito.doReturn(PublicJsonWebKey.Factory.newPublicJwk(JWK_AS_STRING_PUK_FED_ENC)) - .when(entityStatementRpService) - .getRpEncKey(any()); - - Mockito.doReturn(certFromEntityStmtRpService) - .when(entityStatementRpService) - .getRpTlsClientCert(any()); - - final String codeVerifier = ClientUtilities.generateCodeVerifier(); - final String redirectUri = testHostUrl + "/AS"; - final String fachdienstClientId = testHostUrl; - final HttpResponse respMsg3 = - Unirest.post(testHostUrl + FEDIDP_PAR_AUTH_ENDPOINT) - .field("client_id", fachdienstClientId) - .field("state", "state_Fachdienst") - .field("redirect_uri", redirectUri) - .field("code_challenge", ClientUtilities.generateCodeChallenge(codeVerifier)) - .field("code_challenge_method", CodeChallengeMethod.S256.toString()) - .field("response_type", "code") - .field("nonce", "42") - .field("scope", "urn:telematik:versicherter openid") - .field("acr_values", "gematik-ehealth-loa-high") - .header(TLS_CLIENT_CERT_HEADER_NAME, certFromRequest) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .asString(); - assertThat(respMsg3.getStatus()).isEqualTo(HttpStatus.CREATED); - final String requestUri = - ((JsonObject) JsonParser.parseString(respMsg3.getBody())).get("request_uri").getAsString(); - - Unirest.config().reset().followRedirects(false); - final HttpResponse respMsg7 = - Unirest.get(testHostUrl + FED_AUTH_ENDPOINT) - .queryString("request_uri", requestUri) - .queryString("user_id", "12345678") - .asString(); + final MockHttpServletResponse respMsg7 = + mockMvc + .perform( + get(testHostUrl + FED_AUTH_ENDPOINT) + .param("request_uri", requestUri) + .param("user_id", "12345678")) + .andReturn() + .getResponse(); assertThat(respMsg7.getStatus()).isEqualTo(HttpStatus.FOUND); final String code = - UriUtils.extractParameterValue(respMsg7.getHeaders().get("Location").get(0), "code"); - - final HttpResponse httpResponse = - Unirest.post(testHostUrl + TOKEN_ENDPOINT) - .field("grant_type", "authorization_code") - .field("code", code) - .field("code_verifier", codeVerifier) - .field("client_id", fachdienstClientId) - .field("redirect_uri", redirectUri) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .header(HttpHeaders.USER_AGENT, "IdP-Client") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .header(TLS_CLIENT_CERT_HEADER_NAME, certFromRequest) - .asJson(); - assertThat(httpResponse.getStatus()).isEqualTo(HttpStatus.OK); + UriUtils.extractParameterValue(respMsg7.getHeaders("Location").get(0), "code"); + + mockMvc + .perform( + post(testHostUrl + TOKEN_ENDPOINT) + .param("grant_type", "authorization_code") + .param("code", code) + .param("code_verifier", codeVerifier) + .param("client_id", fachdienstClientId) + .param("redirect_uri", redirectUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(HttpHeaders.USER_AGENT, "IdP-Client") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(TLS_CLIENT_CERT_HEADER_NAME, CERT1_FROM_REQUEST)) + .andExpect(status().isOk()); } private void waitForSeconds(final int seconds) { diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTestConfiguration.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTestConfiguration.java deleted file mode 100644 index c3b2f76..0000000 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/controller/FedIdpControllerTestConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 gematik GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.gematik.idp.gsi.server.controller; - -import de.gematik.idp.gsi.server.services.EntityStatementRpService; -import org.mockito.Mockito; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; - -@Profile("test-controller") -@Configuration -public class FedIdpControllerTestConfiguration { - @Bean - @Primary - public EntityStatementRpService entityStmntRpServiceMock() { - return Mockito.mock(EntityStatementRpService.class); - } -} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/ClaimsInfoTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/ClaimsInfoTest.java new file mode 100644 index 0000000..5963d05 --- /dev/null +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/ClaimsInfoTest.java @@ -0,0 +1,250 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.data; + +import static de.gematik.idp.gsi.server.data.GsiConstants.ACR_HIGH; +import static de.gematik.idp.gsi.server.data.GsiConstants.ACR_SUBSTANTIAL; +import static de.gematik.idp.gsi.server.util.ClaimHelper.getClaimsForScopeSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import de.gematik.idp.gsi.server.exceptions.GsiException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ClaimsInfoTest { + + final JsonObject validClaims = new JsonObject(); + + @BeforeAll + void setUp() { + final JsonObject idToken = new JsonObject(); + + final JsonObject amr = new JsonObject(); + final JsonArray amrValues = new JsonArray(); + amrValues.add("urn:telematik:auth:eGK"); + amr.add("values", amrValues); + amr.addProperty("essential", true); + + final JsonObject acr = new JsonObject(); + final JsonArray acrValues = new JsonArray(); + acrValues.add(ACR_HIGH); + acr.add("values", acrValues); + acr.addProperty("essential", true); + + final JsonObject email = new JsonObject(); + email.addProperty("essential", true); + final JsonObject name = new JsonObject(); + name.addProperty("essential", false); + + idToken.add("amr", amr); + idToken.add("acr", acr); + idToken.add("urn:telematik:claims:email", email); + idToken.add("urn:telematik:claims:given_name", name); + + validClaims.add("id_token", idToken); + } + + @Test + void test_constructor_VALID() { + assertDoesNotThrow(() -> new ClaimsInfo(validClaims.toString())); + } + + @Test + void test_constructor_onlyAcrIsEssential_VALID() { + + final JsonObject validClaimsAcrEssential = validClaims.deepCopy(); + validClaimsAcrEssential.getAsJsonObject("id_token").getAsJsonObject("amr").remove("essential"); + assertDoesNotThrow(() -> new ClaimsInfo(validClaimsAcrEssential.toString())); + } + + @Test + void test_constructor_multipleAcrAndAmrValues_VALID() { + final JsonObject validClaimsMultipleAcrAmrValues = validClaims.deepCopy(); + validClaimsMultipleAcrAmrValues + .getAsJsonObject("id_token") + .getAsJsonObject("amr") + .addProperty("value", "urn:telematik:auth:mEW"); + validClaimsMultipleAcrAmrValues + .getAsJsonObject("id_token") + .getAsJsonObject("acr") + .addProperty("value", ACR_SUBSTANTIAL); + assertDoesNotThrow(() -> new ClaimsInfo(validClaimsMultipleAcrAmrValues.toString())); + } + + @Test + void test_constructor_claimsIsNull_VALID() { + assertDoesNotThrow(() -> new ClaimsInfo(null)); + } + + @Test + void test_constructor_claimsIsEmpty_VALID() { + assertDoesNotThrow(() -> new ClaimsInfo("")); + } + + @Test + void test_constructor_claimsShouldNotHaveValues_INVALID() { + + final JsonObject invalidClaims = validClaims.deepCopy(); + invalidClaims + .getAsJsonObject("id_token") + .getAsJsonObject("urn:telematik:claims:given_name") + .addProperty("value", "anyName"); + assertThatThrownBy(() -> new ClaimsInfo(invalidClaims.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining( + "claim urn:telematik:claims:given_name should not have value or values set"); + } + + @Test + void test_constructor_invalidClaimName_INVALID() { + + final JsonObject invalidClaims = validClaims.deepCopy(); + invalidClaims.getAsJsonObject("id_token").addProperty("invalidClaimName", "anything"); + assertThatThrownBy(() -> new ClaimsInfo(invalidClaims.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining("claim invalidClaimName is not supported"); + } + + @Test + void test_constructor_claimsIsNotAJsonObject_INVALID() { + + assertThatThrownBy(() -> new ClaimsInfo("invalidJsonStruct")) + .isInstanceOf(GsiException.class) + .hasMessageContaining("parameter claims is not a JSON object"); + } + + @Test + void test_constructor_paramJsonButNotClaims_INVALID() { + final JsonObject noClaimsObject = new JsonObject(); + noClaimsObject.addProperty("invalid", "any"); + + assertThatThrownBy(() -> new ClaimsInfo(noClaimsObject.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining("parameter claims has invalid structure"); + } + + @Test + void test_constructor_invalidAcrValue_INVALID() { + final JsonObject invalidClaims = validClaims.deepCopy(); + invalidClaims + .getAsJsonObject("id_token") + .getAsJsonObject("acr") + .getAsJsonArray("values") + .add("invalid-acr-value"); + + assertThatThrownBy(() -> new ClaimsInfo(invalidClaims.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid acr value: invalid-acr-value"); + } + + @Test + void test_constructor_invalidAmrValue_INVALID() { + final JsonObject invalidClaims = validClaims.deepCopy(); + invalidClaims + .getAsJsonObject("id_token") + .getAsJsonObject("amr") + .getAsJsonArray("values") + .add("invalid:amr:value"); + + assertThatThrownBy(() -> new ClaimsInfo(invalidClaims.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid amr value: invalid:amr:value"); + } + + @Test + void test_constructor_invalidAcrAmrCombination_acrSubstantial_INVALID() { + final JsonObject invalidClaims = validClaims.deepCopy(); + final JsonArray acr = + invalidClaims.getAsJsonObject("id_token").getAsJsonObject("acr").getAsJsonArray("values"); + acr.remove(0); + acr.add(ACR_SUBSTANTIAL); + final JsonArray amr = + invalidClaims.getAsJsonObject("id_token").getAsJsonObject("amr").getAsJsonArray("values"); + amr.add("urn:telematik:auth:mEW"); + + assertThatThrownBy(() -> new ClaimsInfo(invalidClaims.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid combination of essential values acr and amr"); + } + + @Test + void test_constructor_invalidAcrAmrCombination_acrHigh_INVALID() { + final JsonObject invalidClaims = validClaims.deepCopy(); + invalidClaims + .getAsJsonObject("id_token") + .getAsJsonObject("amr") + .getAsJsonArray("values") + .add("urn:telematik:auth:mEW"); + + assertThatThrownBy(() -> new ClaimsInfo(invalidClaims.toString())) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid combination of essential values acr and amr"); + } + + @Test + void test_constructor_invalidAcrAmrCombinationButNotEssential_VALID() { + final JsonObject validClaimsInvalidCombo = validClaims.deepCopy(); + final JsonObject amr = + validClaimsInvalidCombo.getAsJsonObject("id_token").getAsJsonObject("amr"); + amr.getAsJsonArray("values").add("urn:telematik:auth:mEW"); + amr.remove("essential"); + + assertDoesNotThrow(() -> new ClaimsInfo(validClaimsInvalidCombo.toString())); + } + + @Test + void test_addClaimsFromScopeToClaimsSet_VALID() { + + final ClaimsInfo validClaimsInfo = new ClaimsInfo(validClaims.toString()); + + assertThat(validClaimsInfo.getEssentialClaims()) + .containsExactlyInAnyOrder("urn:telematik:claims:email"); + assertThat(validClaimsInfo.getOptionalClaims()) + .containsExactlyInAnyOrder("urn:telematik:claims:given_name"); + assertThat(validClaimsInfo.getAcrValues()).containsExactlyInAnyOrder(ACR_HIGH); + assertThat(validClaimsInfo.getAmrValues()).containsExactlyInAnyOrder("urn:telematik:auth:eGK"); + + final Set claimsFromScope = + getClaimsForScopeSet( + new HashSet<>( + Arrays.asList( + "urn:telematik:family_name", + "urn:telematik:display_name", + "urn:telematik:given_name"))); + validClaimsInfo.addClaimsFromScopeToClaimsSet(claimsFromScope); + + assertThat(validClaimsInfo.getEssentialClaims()) + .containsExactlyInAnyOrder("urn:telematik:claims:email"); + assertThat(validClaimsInfo.getOptionalClaims()) + .containsExactlyInAnyOrder( + "urn:telematik:claims:given_name", + "urn:telematik:claims:family_name", + "urn:telematik:claims:display_name"); + assertThat(validClaimsInfo.getAcrValues()).containsExactlyInAnyOrder(ACR_HIGH); + assertThat(validClaimsInfo.getAmrValues()).containsExactlyInAnyOrder("urn:telematik:auth:eGK"); + } +} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/FedIdpAuthSessionTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/FedIdpAuthSessionTest.java index 96deed6..8185b17 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/FedIdpAuthSessionTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/FedIdpAuthSessionTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import de.gematik.idp.crypto.Nonce; +import de.gematik.idp.gsi.server.util.ClaimHelper; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.HashSet; @@ -32,14 +33,19 @@ class FedIdpAuthSessionTest { void testBuild() { final int REQUEST_URI_TTL_SECS = 42; final Set scopes = - new HashSet<>(Arrays.asList("profile", "telematik", "openid", "email")); + new HashSet<>( + Arrays.asList( + "urn:telematik:display_name", + "urn:telematik:alter", + "openid", + "urn:telematik:email")); final FedIdpAuthSession fedIdpAuthSession = FedIdpAuthSession.builder() .fachdienstCodeChallenge("fachdienstCodeChallenge") .fachdienstCodeChallengeMethod("fachdienstCodeChallengeMethod") .fachdienstNonce("fachdienstNonce") - .requestedScopes(scopes) + .requestedOptionalClaims(ClaimHelper.getClaimsForScopeSet(scopes)) .fachdienstRedirectUri("fachdienstRedirectUri") .authorizationCode(Nonce.getNonceAsHex(AUTH_CODE_LENGTH)) .expiresAt(ZonedDateTime.now().plusSeconds(REQUEST_URI_TTL_SECS).toString()) @@ -53,7 +59,12 @@ void testBuild() { assertThat(fedIdpAuthSession.getFachdienstRedirectUri()).isEqualTo("fachdienstRedirectUri"); assertThat(fedIdpAuthSession.getFachdienstCodeChallengeMethod()) .isEqualTo("fachdienstCodeChallengeMethod"); - assertThat(fedIdpAuthSession.getRequestedScopes()).isEqualTo(scopes); + assertThat(fedIdpAuthSession.getRequestedOptionalClaims()) + .isEqualTo( + Set.of( + "urn:telematik:claims:display_name", + "urn:telematik:claims:alter", + "urn:telematik:claims:email")); assertThat(FedIdpAuthSession.builder().toString()).hasSizeGreaterThan(0); } diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/InsuredPersonsServiceTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/InsuredPersonsServiceTest.java index 825cfaa..1d1a294 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/InsuredPersonsServiceTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/data/InsuredPersonsServiceTest.java @@ -39,30 +39,30 @@ class InsuredPersonsServiceTest { @Autowired InsuredPersonsService insuredPersonsService; @Test - void getPersonFromService() { + void test_getPersonFromService_VALID() { assertThat(insuredPersonsService.getPersons().get("X110411675")).isNotNull(); } @Test - void getPersons() { + void test_getPersons_VALID() { assertDoesNotThrow(() -> new InsuredPersonsService("versicherte.gesundheitsid.json")); } @Test - void getPersonX110411675() { + void test_getPersonX110411675_VALID() { final InsuredPersonsService iPr = new InsuredPersonsService("versicherte.gesundheitsid.json"); assertThat(iPr.getPersons().get("X110411675")).isNotNull(); } @Test - void getFamilyNameOfPersonX110411675() { + void test_getFamilyNameOfPersonX110411675_VALID() { final InsuredPersonsService iPr = new InsuredPersonsService("versicherte.gesundheitsid.json"); assertThat(iPr.getPersons().get("X110411675")) .containsEntry(ClaimName.TELEMATIK_FAMILY_NAME.getJoseName(), "Bödefeld"); } @Test - void checkInsuredPersonsList() { + void test_checkInsuredPersonsList_VALID() { final InsuredPersonsService iPr = new InsuredPersonsService("versicherte.gesundheitsid.json"); iPr.getPersons() .values() @@ -78,7 +78,7 @@ void checkInsuredPersonsList() { } @Test - void getPersonFileNotFound() { + void test_getPersonFileNotFound_INVALID() { final String invalidFilePath = "invalidFilePath"; final InsuredPersonsService iPr = new InsuredPersonsService(invalidFilePath); assertThatThrownBy(iPr::getPersons) @@ -87,7 +87,7 @@ void getPersonFileNotFound() { } @Test - void getPersonFileNotJson() { + void test_getPersonFileNotJson_INVALID() { final String invalidFilePath = "application.yml"; final InsuredPersonsService iPr = new InsuredPersonsService(invalidFilePath); assertThatThrownBy(iPr::getPersons) diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/handler/FedIdpExceptionHandlerTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/handler/FedIdpExceptionHandlerTest.java index da339fb..b4db7bc 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/handler/FedIdpExceptionHandlerTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/handler/FedIdpExceptionHandlerTest.java @@ -34,7 +34,7 @@ class FedIdpExceptionHandlerTest { private final GsiExceptionHandler fedIdpExceptionHandler = new GsiExceptionHandler(); @Test - void testGsiException() { + void test_GsiException() { final ResponseEntity resp = fedIdpExceptionHandler.handleGsiException( new GsiException( @@ -46,7 +46,7 @@ void testGsiException() { } @Test - void testValidationException() { + void test_ValidationException() { final ResponseEntity resp = fedIdpExceptionHandler.handleValidationException( new ValidationException("something strange happened again")); @@ -55,7 +55,7 @@ void testValidationException() { } @Test - void testMissingServletRequestParameterException() { + void test_MissingServletRequestParameterException() { final ResponseEntity resp = fedIdpExceptionHandler.handleMissingServletRequestParameter( new MissingServletRequestParameterException("anyName", "anyType")); @@ -64,7 +64,7 @@ void testMissingServletRequestParameterException() { } @Test - void testRuntimeException() { + void test_RuntimeException() { final ResponseEntity resp = fedIdpExceptionHandler.handleRuntimeException(new RuntimeException("anyMsg")); assertThat(Objects.requireNonNull(resp.getBody()).getError()) @@ -72,14 +72,14 @@ void testRuntimeException() { } @Test - void testGsiExceptionWithEx() { + void test_GsiExceptionWithEx() { final ResponseEntity resp = fedIdpExceptionHandler.handleGsiException(new GsiException(new NullPointerException())); assertThat(resp.getStatusCode().is5xxServerError()).isTrue(); } @Test - void testGsiExceptionWithExAndMsg() { + void test_GsiExceptionWithExAndMsg() { final ResponseEntity resp = fedIdpExceptionHandler.handleGsiException( new GsiException("Oooops", new NullPointerException())); diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/AuthenticationServiceTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/AuthenticationServiceTest.java index aa8a834..075122e 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/AuthenticationServiceTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/AuthenticationServiceTest.java @@ -38,7 +38,7 @@ class AuthenticationServiceTest { void init() {} @Test - void authenticationTest_LegacyUser12345678() { + void test_authentication_LegacyUser12345678() { final AuthenticationService authenticationService = new AuthenticationService(new InsuredPersonsService("versicherte.gesundheitsid.json")); final Map userData = new HashMap<>(); @@ -61,7 +61,7 @@ void authenticationTest_LegacyUser12345678() { } @Test - void authenticationTest_User_AcrAndAmrNotSet() { + void test_authentication_User_AcrAndAmrNotSet() { final AuthenticationService authenticationService = new AuthenticationService(new InsuredPersonsService("versicherte.gesundheitsid.json")); final Map userData = new HashMap<>(); diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementBuilderTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementBuilderTest.java index 4eeac3c..fb795f9 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementBuilderTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementBuilderTest.java @@ -40,7 +40,7 @@ class EntityStatementBuilderTest { @Autowired private GsiConfiguration gsiConfiguration; @Test - void generateEntityStatementValid20Years() { + void test_generateEntityStatementValid20Years() { final int entityStatementTtlYears = 20; final long nowPlus20Years = ZonedDateTime.now().plusYears(entityStatementTtlYears).toEpochSecond(); diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpReaderTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpReaderTest.java new file mode 100644 index 0000000..abf4d6d --- /dev/null +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpReaderTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.gsi.server.common.Constants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; + +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.token.JsonWebToken; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class EntityStatementRpReaderTest { + + private static final JsonWebToken VALID_ENTITY_STMNT = + new JsonWebToken(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043); + + private static final String ENTITY_STATEMENT_WITH_CERT = + "eyJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCIsImtpZCI6InB1a19pZHBfc2lnX3NlayIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJodHRwczovL2lkcC10ZXN0LmFwcC50aS1kaWVuc3RlLmRlIiwic3ViIjoiaHR0cHM6Ly9pZHAtdGVzdC5hcHAudGktZGllbnN0ZS5kZSIsImlhdCI6MTcyNDc4ODA3MiwiZXhwIjoxNzI0ODc0NDcyLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiV2JPWk9hS01uaWVIaFRzbk1TdlFNZUh6dDR4U1V3YlRMdWRjQTVseVV3WSIsInkiOiJXVmN5UFVvU3FWMFp1MFJQcC00NU5kZWJ1YURVLWVZbjRqTk9fN2k2QzJjIiwia2lkIjoicHVrX2lkcF9zaWdfc2VrIiwidXNlIjoic2lnIiwiYWxnIjoiRVMyNTYifV19LCJhdXRob3JpdHlfaGludHMiOlsiaHR0cHM6Ly9hcHAtdGVzdC5mZWRlcmF0aW9ubWFzdGVyLmRlIl0sIm1ldGFkYXRhIjp7Im9wZW5pZF9yZWx5aW5nX3BhcnR5Ijp7Imp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJXNTVqWTRZR3QtaDZ5M1pDbTRoVEJudWUzM21GaHA4S3Q2WW80SFJtdkFNIiwieSI6IlRhd01vQWVGck0wRUNQMGQxVkdJTFJwTVd1TTNpZVFidU1ZTmZ4enJWeEkiLCJraWQiOiJwdWtfaWRwX2VuY19zZWsiLCJ1c2UiOiJlbmMiLCJhbGciOiJFQ0RILUVTIn0seyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Il96TF85ZGptMEJFUFFRdXBfZjBrTHN5WHZNRVhhd0s4alB1VE1UMThJNHciLCJ5IjoidnVGUW16RkdKYWVVdzl2a2ZnWnVCUDdsTzJOSmpHcHkwZ2wwd0tDd2NhRSIsInVzZSI6InNpZyIsImFsZyI6IkVTMjU2IiwieDVjIjpbIk1JSUJvRENDQVVXZ0F3SUJBZ0lVRnN2a1hNQ1Azc2hUbUJtVWE2SC9wMmxXWTVZd0NnWUlLb1pJemowRUF3SXdKVEVqTUNFR0ExVUVBd3dhYVdSd0xYUmxjM1F1WVhCd0xuUnBMV1JwWlc1emRHVXVaR1V3SGhjTk1qTXdPREUyTVRFeU56QXdXaGNOTWpRd09URTNNVEV5TnpBd1dqQWxNU013SVFZRFZRUUREQnBwWkhBdGRHVnpkQzVoY0hBdWRHa3RaR2xsYm5OMFpTNWtaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCUDh5Ly9YWTV0QVJEMEVMcWYzOUpDN01sN3pCRjJzQ3ZJejdrekU5ZkNPTXZ1RlFtekZHSmFlVXc5dmtmZ1p1QlA3bE8yTkpqR3B5MGdsMHdLQ3djYUdqVXpCUk1CMEdBMVVkRGdRV0JCUUtSRm9mZUpPeTI1cUwrTG55VERSTzlEVlFBREFmQmdOVkhTTUVHREFXZ0JRS1JGb2ZlSk95MjVxTCtMbnlURFJPOURWUUFEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01Bb0dDQ3FHU000OUJBTUNBMGtBTUVZQ0lRRFFWVnFqT2RDN21zb28rMG9UQWNlaUZqVXlEb3FwYTBBZjRyQjdHNEhiaUFJaEFQcjlDaWJGOFZrNGNOaHRKWUZBSU9aQXJnRGFWbXhmZHVkckRZSkFBOVhJIl0sImtpZCI6IklFR3F1cUMzSkpIT2tPLTRHdU93aHZGNF9FQldpNEFHdXQ4cTYxZDM4REUifV19LCJjbGllbnRfbmFtZSI6IkUtUmV6ZXB0IEFwcCIsInJlZGlyZWN0X3VyaXMiOlsiaHR0cHM6Ly9hcHBsaW5rLXRlc3QudGsuZGUvZXJlemVwdC9yZWRpcmVjdEZyb21BdXRoZW50aWNhdG9yIiwiaHR0cHM6Ly9kYXMtZS1yZXplcHQtZnVlci1kZXV0c2NobGFuZC5kZS9leHRhdXRoIiwiaHR0cHM6Ly9pZGJyb2tlci5hb2tidy5ydS5ub25wcm9kLWVoZWFsdGgtaWQuZGUvZXJwL2xvZ2luIiwiaHR0cHM6Ly9pZGJyb2tlci5hb2twbC5ydS5ub25wcm9kLWVoZWFsdGgtaWQuZGUvZXJwL2xvZ2luIiwiaHR0cHM6Ly9pZGJyb2tlci5pYm0udHUubm9ucHJvZC1laGVhbHRoLWlkLmRlL2VycC9sb2dpbiIsImh0dHBzOi8vcmVkaXJlY3QuZ2VtYXRpay5kZS9lcmV6ZXB0IiwiaHR0cHM6Ly90dS5yaXNlLWVwYS5kZS9lcmV6ZXB0Il0sInJlc3BvbnNlX3R5cGVzIjpbImNvZGUiXSwiY2xpZW50X3JlZ2lzdHJhdGlvbl90eXBlcyI6WyJhdXRvbWF0aWMiXSwiZ3JhbnRfdHlwZXMiOlsiYXV0aG9yaXphdGlvbl9jb2RlIl0sInJlcXVpcmVfcHVzaGVkX2F1dGhvcml6YXRpb25fcmVxdWVzdHMiOnRydWUsInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kIjoic2VsZl9zaWduZWRfdGxzX2NsaWVudF9hdXRoIiwiZGVmYXVsdF9hY3JfdmFsdWVzIjpbImdlbWF0aWstZWhlYWx0aC1sb2EtaGlnaCJdLCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjoiRVMyNTYiLCJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfYWxnIjoiRUNESC1FUyIsImlkX3Rva2VuX2VuY3J5cHRlZF9yZXNwb25zZV9lbmMiOiJBMjU2R0NNIiwic2NvcGUiOiJvcGVuaWQgdXJuOnRlbGVtYXRpazpkaXNwbGF5X25hbWUgdXJuOnRlbGVtYXRpazp2ZXJzaWNoZXJ0ZXIifX19.zUhIP91srHWXfuuGMCGPlP7uKUQnhnL-orKAdifCPEnUmv9JLdPiRmRkLI7_pXqVuR8UCQMF2vi50QT6NkwA5w"; + + private static final String SIGNED_JWKS_WITHOUT_CERT = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InB1a19mZF9zaWcifQ.ewogICJpc3MiOiAiaHR0cHM6Ly9pZHBmYWRpLmRldi5nZW1hdGlrLnNvbHV0aW9ucyIsCiAgImlhdCI6IDE2OTcyMDI0ODgsCiAgImtleXMiOiBbCiAgICB7CiAgICAgICJ1c2UiOiAic2lnIiwKICAgICAgImtpZCI6ICJwdWtfZmRfc2lnIiwKICAgICAgImt0eSI6ICJFQyIsCiAgICAgICJjcnYiOiAiUC0yNTYiLAogICAgICAieCI6ICI5YkpzMjdZQWZsTVVXSzVueHVpRjZYQUcwSmF6dXZ3UmkxRXBGSzBYS2lrIiwKICAgICAgInkiOiAiUDhsek5WUk9nVHV3YkRxc2Q4clQxQUkzemV6OTRIQnNURHBPdmFqUDByWSIKICAgIH0sCiAgICB7CiAgICAgICJ1c2UiOiAiZW5jIiwKICAgICAgImtpZCI6ICJwdWtfZmRfZW5jIiwKICAgICAgImt0eSI6ICJFQyIsCiAgICAgICJjcnYiOiAiUC0yNTYiLAogICAgICAieCI6ICJOUUxhV2J1UURIZ1NIYWhxYjl6eGxEZGlNQ0hYU2dZMEw5cWwxazdCVlVFIiwKICAgICAgInkiOiAiX1VTZ21xaGxNM3B2YWJrWjJTU19ZRTJRNTd0VHM2cEs5Y0VfdVpCLXUzYyIKICAgIH0KICBdCn0=.BYCS-xn-jSpIFq501Jb7B0kewaO7UeOrg7LJmCLyQI3xz76rXJ7PiDtBTyAeTQ1S5jyQfS_-t2oDtWv5UJQh3w"; + private static final String SIGNED_JWKS_WITHOUT_ENCKEY = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InB1a19mZF9zaWcifQ.ewogICJpc3MiOiAiaHR0cHM6Ly9pZHBmYWRpLmRldi5nZW1hdGlrLnNvbHV0aW9ucyIsCiAgImlhdCI6IDE2OTcyMDI0ODgsCiAgImtleXMiOiBbCiAgICB7CiAgICAgICJ1c2UiOiAic2lnIiwKICAgICAgImtpZCI6ICJwdWtfZmRfc2lnIiwKICAgICAgImt0eSI6ICJFQyIsCiAgICAgICJjcnYiOiAiUC0yNTYiLAogICAgICAieCI6ICI5YkpzMjdZQWZsTVVXSzVueHVpRjZYQUcwSmF6dXZ3UmkxRXBGSzBYS2lrIiwKICAgICAgInkiOiAiUDhsek5WUk9nVHV3YkRxc2Q4clQxQUkzemV6OTRIQnNURHBPdmFqUDByWSIKICAgIH0sCiAgICB7CiAgICAgICJ4NWMiOiBbCiAgICAgICAgIk1JSUNHakNDQWNDZ0F3SUJBZ0lVVEd5TG0wZFhDU3dVdW5TK0M3WTRkclpnRzVrd0NnWUlLb1pJemowRUF3SXdmekVMTUFrR0ExVUVCaE1DUkVVeER6QU5CZ05WQkFnTUJrSmxjbXhwYmpFUE1BMEdBMVVFQnd3R1FtVnliR2x1TVJvd0dBWURWUVFLREJGblpXMWhkR2xySUU1UFZDMVdRVXhKUkRFUE1BMEdBMVVFQ3d3R1VGUWdTVVJOTVNFd0h3WURWUVFEREJobVlXTm9aR2xsYm5OMFZHeHpReUJVUlZOVUxVOU9URmt3SGhjTk1qTXdNakV3TVRJek5UTTFXaGNOTWpRd01qRXdNVEl6TlRNMVdqQi9NUXN3Q1FZRFZRUUdFd0pFUlRFUE1BMEdBMVVFQ0F3R1FtVnliR2x1TVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4R2pBWUJnTlZCQW9NRVdkbGJXRjBhV3NnVGs5VUxWWkJURWxFTVE4d0RRWURWUVFMREFaUVZDQkpSRTB4SVRBZkJnTlZCQU1NR0daaFkyaGthV1Z1YzNSVWJITkRJRlJGVTFRdFQwNU1XVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT1JxcVN1cisySFpUaEZxQTdFR3E4YmJGMi81d0w1bWpjL0J4b09kb3Q3cnQwUUwwRG5LMjBlcjRwS1R4cml5MCtOUHN4UUZrdm1LZUVLYlY0RWlKNlNqR2pBWU1Ba0dBMVVkRXdRQ01BQXdDd1lEVlIwUEJBUURBZ1hnTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUQwVGl2VitubFROMDZ2akJadDFQVVFkNkdoUWtheUVKK2FEcVMwUjJaL3hBaUVBMUt4RkhRN0dMRFNsLzZPb2dXRnN4S2FmWFEreVpLazl2dEsvUG9oZm0zbz0iCiAgICAgIF0sCiAgICAgICJ1c2UiOiAic2lnIiwKICAgICAgImtpZCI6ICJwdWtfdGxzX3NpZyIsCiAgICAgICJrdHkiOiAiRUMiLAogICAgICAiY3J2IjogIlAtMjU2IiwKICAgICAgIngiOiAiNUdxcEs2djdZZGxPRVdvRHNRYXJ4dHNYYl9uQXZtYU56OEhHZzUyaTN1cyIsCiAgICAgICJ5IjogInQwUUwwRG5LMjBlcjRwS1R4cml5MC1OUHN4UUZrdm1LZUVLYlY0RWlKNlEiCiAgICB9CiAgXQp9.BYCS-xn-jSpIFq501Jb7B0kewaO7UeOrg7LJmCLyQI3xz76rXJ7PiDtBTyAeTQ1S5jyQfS_-t2oDtWv5UJQh3w"; + + private static final String ENTITY_STMNT_MISSING_CLAIM_REDIRECT_URIS = + "eyJhbGciOiJFUzI1NiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0Iiwia2lkIjoicHVrX2ZkX3NpZyJ9.ewogICJpc3MiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0IiwKICAic3ViIjogImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NCIsCiAgImlhdCI6IDE3MDIwNTA0NTEsCiAgImV4cCI6IDIzMzMyMDI0NTEsCiAgImp3a3MiOiB7CiAgICAia2V5cyI6IFsKICAgICAgewogICAgICAgICJ1c2UiOiAic2lnIiwKICAgICAgICAia2lkIjogInB1a19mZF9zaWciLAogICAgICAgICJrdHkiOiAiRUMiLAogICAgICAgICJjcnYiOiAiUC0yNTYiLAogICAgICAgICJ4IjogIjliSnMyN1lBZmxNVVdLNW54dWlGNlhBRzBKYXp1dndSaTFFcEZLMFhLaWsiLAogICAgICAgICJ5IjogIlA4bHpOVlJPZ1R1d2JEcXNkOHJUMUFJM3plejk0SEJzVERwT3ZhalAwclkiLAogICAgICAgICJhbGciOiAiRVMyNTYiCiAgICAgIH0KICAgIF0KICB9LAogICJhdXRob3JpdHlfaGludHMiOiBbCiAgICAiaHR0cHM6Ly9hcHAtdGVzdC5mZWRlcmF0aW9ubWFzdGVyLmRlIgogIF0sCiAgIm1ldGFkYXRhIjogewogICAgIm9wZW5pZF9yZWx5aW5nX3BhcnR5IjogewogICAgICAic2lnbmVkX2p3a3NfdXJpIjogImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NC9qd3MuanNvbiIsCiAgICAgICJvcmdhbml6YXRpb25fbmFtZSI6ICJGYWNoZGllbnN0MDA3IGRlcyBGZWRJZHAgUE9DcyIsCiAgICAgICJjbGllbnRfbmFtZSI6ICJGYWNoZGllbnN0MDA3IiwKICAgICAgImxvZ29fdXJpIjogImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NC9ub0xvZ29ZZXQiLAogICAgICAicmVzcG9uc2VfdHlwZXMiOiBbCiAgICAgICAgImNvZGUiCiAgICAgIF0sCiAgICAgICJjbGllbnRfcmVnaXN0cmF0aW9uX3R5cGVzIjogWwogICAgICAgICJhdXRvbWF0aWMiCiAgICAgIF0sCiAgICAgICJncmFudF90eXBlcyI6IFsKICAgICAgICAiYXV0aG9yaXphdGlvbl9jb2RlIgogICAgICBdLAogICAgICAicmVxdWlyZV9wdXNoZWRfYXV0aG9yaXphdGlvbl9yZXF1ZXN0cyI6IHRydWUsCiAgICAgICJ0b2tlbl9lbmRwb2ludF9hdXRoX21ldGhvZCI6ICJzZWxmX3NpZ25lZF90bHNfY2xpZW50X2F1dGgiLAogICAgICAiZGVmYXVsdF9hY3JfdmFsdWVzIjogWwogICAgICAgICJnZW1hdGlrLWVoZWFsdGgtbG9hLWhpZ2giCiAgICAgIF0sCiAgICAgICJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjogIkVTMjU2IiwKICAgICAgImlkX3Rva2VuX2VuY3J5cHRlZF9yZXNwb25zZV9hbGciOiAiRUNESC1FUyIsCiAgICAgICJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjogIkEyNTZHQ00iLAogICAgICAic2NvcGUiOiAidXJuOnRlbGVtYXRpazpkaXNwbGF5X25hbWUgdXJuOnRlbGVtYXRpazp2ZXJzaWNoZXJ0ZXIgb3BlbmlkIgogICAgfSwKICAgICJmZWRlcmF0aW9uX2VudGl0eSI6IHsKICAgICAgIm5hbWUiOiAiRmFjaGRpZW5zdDAwNyIsCiAgICAgICJjb250YWN0cyI6IFsKICAgICAgICAiU3VwcG9ydEBGYWNoZGllbnN0MDA3LmRlIgogICAgICBdLAogICAgICAiaG9tZXBhZ2VfdXJpIjogImh0dHBzOi8vRmFjaGRpZW5zdDAwNy5kZSIKICAgIH0KICB9Cn0.XomqqjzmGfu3LFySjaKrfHcFStBK8lWW8uxH9HmNhdYoslBVd4z5t6I_DQQ2gbe5WWvKoGl0pVpGlGf5oIGR7Q"; + + private static final String ENTITY_STMNT_MISSING_CLAIM_SCOPE = + "eyJhbGciOiJFUzI1NiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0Iiwia2lkIjoicHVrX2ZkX3NpZyJ9.ewogICJpc3MiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0IiwKICAic3ViIjogImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NCIsCiAgImlhdCI6IDE3MDIwNTA0NTEsCiAgImV4cCI6IDIzMzMyMDI0NTEsCiAgImp3a3MiOiB7CiAgICAia2V5cyI6IFsKICAgICAgewogICAgICAgICJ1c2UiOiAic2lnIiwKICAgICAgICAia2lkIjogInB1a19mZF9zaWciLAogICAgICAgICJrdHkiOiAiRUMiLAogICAgICAgICJjcnYiOiAiUC0yNTYiLAogICAgICAgICJ4IjogIjliSnMyN1lBZmxNVVdLNW54dWlGNlhBRzBKYXp1dndSaTFFcEZLMFhLaWsiLAogICAgICAgICJ5IjogIlA4bHpOVlJPZ1R1d2JEcXNkOHJUMUFJM3plejk0SEJzVERwT3ZhalAwclkiLAogICAgICAgICJhbGciOiAiRVMyNTYiCiAgICAgIH0KICAgIF0KICB9LAogICJhdXRob3JpdHlfaGludHMiOiBbCiAgICAiaHR0cHM6Ly9hcHAtdGVzdC5mZWRlcmF0aW9ubWFzdGVyLmRlIgogIF0sCiAgIm1ldGFkYXRhIjogewogICAgIm9wZW5pZF9yZWx5aW5nX3BhcnR5IjogewogICAgICAic2lnbmVkX2p3a3NfdXJpIjogImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NC9qd3MuanNvbiIsCiAgICAgICJvcmdhbml6YXRpb25fbmFtZSI6ICJGYWNoZGllbnN0MDA3IGRlcyBGZWRJZHAgUE9DcyIsCiAgICAgICJjbGllbnRfbmFtZSI6ICJGYWNoZGllbnN0MDA3IiwKICAgICAgImxvZ29fdXJpIjogImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NC9ub0xvZ29ZZXQiLAogICAgICAicmVkaXJlY3RfdXJpcyI6IFsKICAgICAgICAiaHR0cHM6Ly9GYWNoZGllbnN0MDA3LmRlL2NsaWVudCIsCiAgICAgICAgImh0dHBzOi8vcmVkaXJlY3QudGVzdHN1aXRlLmdzaSIsCiAgICAgICAgImh0dHBzOi8vaWRwZmFkaS5kZXYuZ2VtYXRpay5zb2x1dGlvbnMvYXV0aCIKICAgICAgXSwKICAgICAgInJlc3BvbnNlX3R5cGVzIjogWwogICAgICAgICJjb2RlIgogICAgICBdLAogICAgICAiY2xpZW50X3JlZ2lzdHJhdGlvbl90eXBlcyI6IFsKICAgICAgICAiYXV0b21hdGljIgogICAgICBdLAogICAgICAiZ3JhbnRfdHlwZXMiOiBbCiAgICAgICAgImF1dGhvcml6YXRpb25fY29kZSIKICAgICAgXSwKICAgICAgInJlcXVpcmVfcHVzaGVkX2F1dGhvcml6YXRpb25fcmVxdWVzdHMiOiB0cnVlLAogICAgICAidG9rZW5fZW5kcG9pbnRfYXV0aF9tZXRob2QiOiAic2VsZl9zaWduZWRfdGxzX2NsaWVudF9hdXRoIiwKICAgICAgImRlZmF1bHRfYWNyX3ZhbHVlcyI6IFsKICAgICAgICAiZ2VtYXRpay1laGVhbHRoLWxvYS1oaWdoIgogICAgICBdLAogICAgICAiaWRfdG9rZW5fc2lnbmVkX3Jlc3BvbnNlX2FsZyI6ICJFUzI1NiIsCiAgICAgICJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfYWxnIjogIkVDREgtRVMiLAogICAgICAiaWRfdG9rZW5fZW5jcnlwdGVkX3Jlc3BvbnNlX2VuYyI6ICJBMjU2R0NNIgogICAgfSwKICAgICJmZWRlcmF0aW9uX2VudGl0eSI6IHsKICAgICAgIm5hbWUiOiAiRmFjaGRpZW5zdDAwNyIsCiAgICAgICJjb250YWN0cyI6IFsKICAgICAgICAiU3VwcG9ydEBGYWNoZGllbnN0MDA3LmRlIgogICAgICBdLAogICAgICAiaG9tZXBhZ2VfdXJpIjogImh0dHBzOi8vRmFjaGRpZW5zdDAwNy5kZSIKICAgIH0KICB9Cn0=.XomqqjzmGfu3LFySjaKrfHcFStBK8lWW8uxH9HmNhdYoslBVd4z5t6I_DQQ2gbe5WWvKoGl0pVpGlGf5oIGR7Q"; + + @Test + void test_getRedirectUrisEntityStatementRp_VALID() { + assertDoesNotThrow( + () -> EntityStatementRpReader.getRedirectUrisEntityStatementRp(VALID_ENTITY_STMNT)); + assertThat(EntityStatementRpReader.getRedirectUrisEntityStatementRp(VALID_ENTITY_STMNT)) + .isEqualTo( + List.of( + "https://Fachdienst007.de/client", + "https://redirect.testsuite.gsi", + "https://idpfadi.dev.gematik.solutions/auth")); + } + + @Test + void test_getRedirectUrisEntityStatementRp_throwsException_INVALID() { + assertThatThrownBy( + () -> + EntityStatementRpReader.getRedirectUrisEntityStatementRp( + new JsonWebToken(ENTITY_STMNT_MISSING_CLAIM_REDIRECT_URIS))) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("missing claim: redirect_uris"); + } + + @Test + void test_getScopesFromEntityStatementRp_VALID() { + assertDoesNotThrow( + () -> EntityStatementRpReader.getScopesFromEntityStatementRp(VALID_ENTITY_STMNT)); + assertThat(EntityStatementRpReader.getScopesFromEntityStatementRp(VALID_ENTITY_STMNT)) + .isEqualTo(List.of("urn:telematik:display_name", "urn:telematik:versicherter", "openid")); + } + + @Test + void test_getScopesFromEntityStatementRp_throwsException_INVALID() { + assertThatThrownBy( + () -> + EntityStatementRpReader.getScopesFromEntityStatementRp( + new JsonWebToken(ENTITY_STMNT_MISSING_CLAIM_SCOPE))) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("missing claim: scope"); + } + + @Test + void test_getRpTlsClientCerts_VALID() { + try (final MockedStatic mockedStatic = Mockito.mockStatic(HttpClient.class)) { + mockedStatic + .when(() -> HttpClient.fetchSignedJwks(any())) + .thenReturn(Optional.of(new JsonWebToken(SIGNED_JWKS))); + assertDoesNotThrow(() -> EntityStatementRpReader.getRpTlsClientCerts(VALID_ENTITY_STMNT)); + assertThat(EntityStatementRpReader.getRpTlsClientCerts(VALID_ENTITY_STMNT)).isNotNull(); + } + } + + @Test + void test_getRpTlsClientCerts_twoCerts_VALID() { + try (final MockedStatic mockedStatic = Mockito.mockStatic(HttpClient.class)) { + mockedStatic + .when(() -> HttpClient.fetchSignedJwks(any())) + .thenReturn(Optional.of(new JsonWebToken(SIGNED_JWKS_TWO_CERTS))); + assertDoesNotThrow(() -> EntityStatementRpReader.getRpTlsClientCerts(VALID_ENTITY_STMNT)); + List certs = EntityStatementRpReader.getRpTlsClientCerts(VALID_ENTITY_STMNT); + assertThat(certs).isNotNull(); + assertThat(certs).hasSize(2); + } + } + + @Test + void test_getRpTlsClientCerts_fromEntityStatementRp_VALID() { + assertDoesNotThrow( + () -> + EntityStatementRpReader.getRpTlsClientCerts( + new JsonWebToken(ENTITY_STATEMENT_WITH_CERT))); + assertThat( + EntityStatementRpReader.getRpTlsClientCerts( + new JsonWebToken(ENTITY_STATEMENT_WITH_CERT))) + .isNotNull(); + } + + @Test + void test_getRpTlsClientCerts_throwsException_INVALID() { + try (final MockedStatic mockedStatic = Mockito.mockStatic(HttpClient.class)) { + mockedStatic + .when(() -> HttpClient.fetchSignedJwks(any())) + .thenReturn(Optional.of(new JsonWebToken(SIGNED_JWKS_WITHOUT_CERT))); + assertThatThrownBy(() -> EntityStatementRpReader.getRpTlsClientCerts(VALID_ENTITY_STMNT)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("No TLS client certificate for relying party found"); + } + } + + @Test + void test_getRpEncKey_fromSignedJwks_VALID() { + try (final MockedStatic mockedStatic = Mockito.mockStatic(HttpClient.class)) { + mockedStatic + .when(() -> HttpClient.fetchSignedJwks(any())) + .thenReturn(Optional.of(new JsonWebToken(SIGNED_JWKS))); + assertDoesNotThrow(() -> EntityStatementRpReader.getRpEncKey(VALID_ENTITY_STMNT)); + assertThat(EntityStatementRpReader.getRpEncKey(VALID_ENTITY_STMNT)).isNotNull(); + } + } + + @Test + void test_getRpEncKey_fromEntityStatementRp_VALID() { + assertDoesNotThrow( + () -> + EntityStatementRpReader.getRpEncKey( + new JsonWebToken(ENTITY_STMNT_FACHDIENST_WITH_OPTIONAL_JWKS))); + assertThat( + EntityStatementRpReader.getRpEncKey( + new JsonWebToken(ENTITY_STMNT_FACHDIENST_WITH_OPTIONAL_JWKS))) + .isNotNull(); + } + + @Test + void test_getRpEncKey_throwsException_INVALID() { + try (final MockedStatic mockedStatic = Mockito.mockStatic(HttpClient.class)) { + mockedStatic + .when(() -> HttpClient.fetchSignedJwks(any())) + .thenReturn(Optional.of(new JsonWebToken(SIGNED_JWKS_WITHOUT_ENCKEY))); + assertThatThrownBy(() -> EntityStatementRpReader.getRpEncKey(VALID_ENTITY_STMNT)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("Encryption key for relying party not found"); + } + } +} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceJwksTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceJwksTest.java deleted file mode 100644 index 373b009..0000000 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceJwksTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 gematik GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.gematik.idp.gsi.server.services; - -import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; -import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_FACHDIENST_WITH_OPTIONAL_JWKS; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - -import de.gematik.idp.IdpConstants; -import de.gematik.idp.gsi.server.GsiServer; -import de.gematik.idp.gsi.server.configuration.GsiConfiguration; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.jose4j.jwk.PublicJsonWebKey; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.mockserver.client.MockServerClient; -import org.mockserver.model.MediaType; -import org.mockserver.springtest.MockServerTest; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.ActiveProfiles; - -@Slf4j -@ActiveProfiles("test-entityservice") -@MockServerTest("server.url=http://localhost:${mockServerPort}") -@SpringBootTest(classes = GsiServer.class, webEnvironment = WebEnvironment.RANDOM_PORT) -class EntityStatementRpServiceJwksTest { - - @Value("${server.url}") - private String mockServerUrl; - - private MockServerClient mockServerClient; - @Autowired EntityStatementRpService entityStatementRpService; - @Autowired GsiConfiguration gsiConfiguration; - @Autowired ServerUrlService serverUrlService; - - @SneakyThrows - @Test - void getEncKeyRpFromEntityStatement() { - prepareMocks(); - final PublicJsonWebKey rpEncKey = entityStatementRpService.getRpEncKey(mockServerUrl); - assertThat(rpEncKey).isNotNull(); - } - - private void prepareMocks() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_FACHDIENST_WITH_OPTIONAL_JWKS)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - } -} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceTest.java deleted file mode 100644 index a0b7da8..0000000 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceTest.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright 2023 gematik GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.gematik.idp.gsi.server.services; - -import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; -import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; -import static de.gematik.idp.gsi.server.common.Constants.SIGNED_JWKS; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - -import de.gematik.idp.IdpConstants; -import de.gematik.idp.gsi.server.GsiServer; -import de.gematik.idp.gsi.server.configuration.GsiConfiguration; -import de.gematik.idp.gsi.server.exceptions.GsiException; -import de.gematik.idp.token.JsonWebToken; -import java.security.cert.X509Certificate; -import java.util.Optional; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.jose4j.jwk.PublicJsonWebKey; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mockito; -import org.mockserver.client.MockServerClient; -import org.mockserver.model.MediaType; -import org.mockserver.springtest.MockServerTest; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.annotation.DirtiesContext.ClassMode; -import org.springframework.test.context.ActiveProfiles; - -@Slf4j -@ActiveProfiles("test-entityservice") -@MockServerTest("server.url=http://localhost:${mockServerPort}") -@DirtiesContext(classMode = ClassMode.AFTER_CLASS) -@SpringBootTest(classes = GsiServer.class, webEnvironment = WebEnvironment.RANDOM_PORT) -class EntityStatementRpServiceTest { - - @Value("${server.url}") - private String mockServerUrl; - - private MockServerClient mockServerClient; - @Autowired EntityStatementRpService entityStatementRpService; - @Autowired GsiConfiguration gsiConfiguration; - @Autowired ServerUrlService serverUrlService; - - @Test - void getEntityStatementRp() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - gsiConfiguration.setFedmasterUrl(mockServerUrl); - final JsonWebToken entStmntFd = entityStatementRpService.getEntityStatementRp(mockServerUrl); - assertThat(entStmntFd).isNotNull(); - } - - @Test - void getEntityStatementAboutRp_Idpfachdienst() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - // switch configuration to mockserver - gsiConfiguration.setFedmasterUrl(mockServerUrl); - final JsonWebToken entityStmntAboutFachdienst = - entityStatementRpService.getEntityStatementAboutRp("dummyUrl"); - assertThat(entityStmntAboutFachdienst).isNotNull(); - } - - @Test - void verifyRedirectUriExistsInEntityStmnt() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - gsiConfiguration.setFedmasterUrl(mockServerUrl); - final String nonExistingUri = "nonExistingUri"; - assertThatThrownBy( - () -> - entityStatementRpService.doAutoregistration( - mockServerUrl, nonExistingUri, "urn:telematik:versicherter openid")) - .isInstanceOf(GsiException.class) - .hasMessageContaining( - "Content of parameter redirect_uri [" - + nonExistingUri - + "] not found in entity statement"); - } - - @SneakyThrows - @Test - void getEncKeyRpFromSignedJwks() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - Mockito.doReturn(Optional.of(mockServerUrl + "/jws.json")) - .when(serverUrlService) - .determineSignedJwksUri(Mockito.any()); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/jws.json")) - .respond( - response() - .withStatusCode(200) - .withContentType(new MediaType("application", "entity-statement+jwt")) - .withBody(SIGNED_JWKS)); - gsiConfiguration.setFedmasterUrl(mockServerUrl); - final PublicJsonWebKey rpEncKey = entityStatementRpService.getRpEncKey(mockServerUrl); - assertThat(rpEncKey).isNotNull(); - } - - @SneakyThrows - @Test - void getClientCertRpFromSignedJwks() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - Mockito.doReturn(Optional.of(mockServerUrl + "/jws.json")) - .when(serverUrlService) - .determineSignedJwksUri(Mockito.any()); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/jws.json")) - .respond( - response() - .withStatusCode(200) - .withContentType(new MediaType("application", "entity-statement+jwt")) - .withBody(SIGNED_JWKS)); - gsiConfiguration.setFedmasterUrl(mockServerUrl); - final X509Certificate cert = entityStatementRpService.getRpTlsClientCert(mockServerUrl); - assertThat(cert).isNotNull(); - } - - @Test - void relyingPartyAutoregistration() { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - gsiConfiguration.setFedmasterUrl(mockServerUrl); - final String correctRedirectUri = "https://redirect.testsuite.gsi"; - assertDoesNotThrow( - () -> - entityStatementRpService.doAutoregistration( - mockServerUrl, correctRedirectUri, "urn:telematik:versicherter openid")); - } - - @ValueSource( - strings = { - "urn:telematik:geburtsdatumurn:telematik:alter openid", - "urn%3Atelematik%3Adisplay_name", - "urn:telematik:given_name+openid", - "urn:telematik:schlecht openid" - }) - @ParameterizedTest(name = "checkException_verifyInvalidScopes scope: {0}") - void checkException_verifyInvalidScopes(final String scope) { - Mockito.doReturn(mockServerUrl + "/federation/fetch") - .when(serverUrlService) - .determineFetchEntityStatementEndpoint(); - mockServerClient - .when(request().withMethod("GET").withPath(IdpConstants.ENTITY_STATEMENT_ENDPOINT)) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - mockServerClient - .when(request().withMethod("GET").withPath("/federation/fetch")) - .respond( - response() - .withStatusCode(200) - .withContentType(MediaType.APPLICATION_JSON) - .withBody(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); - gsiConfiguration.setFedmasterUrl(mockServerUrl); - assertThatThrownBy( - () -> - entityStatementRpService.doAutoregistration( - mockServerUrl, "https://redirect.testsuite.gsi", scope)) - .isInstanceOf(GsiException.class) - .hasMessageContaining( - "Content of parameter scope [" + scope + "] exceeds scopes found in entity statement."); - } -} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceTestConfiguration.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceTestConfiguration.java deleted file mode 100644 index 4c02e53..0000000 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpServiceTestConfiguration.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 gematik GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.gematik.idp.gsi.server.services; - -import org.mockito.Mockito; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; - -@Profile("test-entityservice") -@Configuration -public class EntityStatementRpServiceTestConfiguration { - @Bean - @Primary - public ServerUrlService serverUrlServiceMock() { - return Mockito.mock(ServerUrlService.class); - } -} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpVerifierTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpVerifierTest.java new file mode 100644 index 0000000..308aae6 --- /dev/null +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/EntityStatementRpVerifierTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.token.JsonWebToken; +import org.junit.jupiter.api.Test; + +class EntityStatementRpVerifierTest { + + private static final JsonWebToken VALID_ENTITY_STMNT = + new JsonWebToken(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043); + + @Test + void test_verifyRedirectUriExistsInEntityStmnt_VALID() { + assertDoesNotThrow( + () -> + EntityStatementRpVerifier.verifyRedirectUriExistsInEntityStmnt( + VALID_ENTITY_STMNT, "https://Fachdienst007.de/client")); + } + + @Test + void test_verifyRedirectUriExistsInEntityStmnt_throwsException_INVALID() { + final String invalidRedirectUri = "https://uri-does-not-exist-in-entity-stmnt"; + assertThatThrownBy( + () -> + EntityStatementRpVerifier.verifyRedirectUriExistsInEntityStmnt( + VALID_ENTITY_STMNT, invalidRedirectUri)) + .isInstanceOf(GsiException.class) + .hasMessageContaining( + "Content of parameter redirect_uri [" + + invalidRedirectUri + + "] not found in entity statement. "); + } + + @Test + void test_verifyRequestedScopesListedInEntityStmnt_VALID() { + assertDoesNotThrow( + () -> + EntityStatementRpVerifier.verifyRequestedScopesListedInEntityStmnt( + VALID_ENTITY_STMNT, + "urn:telematik:display_name urn:telematik:versicherter openid")); + } + + @Test + void test_verifyRequestedScopesListedInEntityStmnt_throwsException_INVALID() { + final String invalidScopes = "urn:telematik:display_name urn:telematik:alter openid"; + assertThatThrownBy( + () -> + EntityStatementRpVerifier.verifyRequestedScopesListedInEntityStmnt( + VALID_ENTITY_STMNT, "urn:telematik:display_name urn:telematik:alter openid")) + .isInstanceOf(GsiException.class) + .hasMessageContaining( + "Content of parameter scope [" + + invalidScopes + + "] exceeds scopes found in entity statement. "); + } +} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/RequestValidatorTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/RequestValidatorTest.java new file mode 100644 index 0000000..0e05306 --- /dev/null +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/RequestValidatorTest.java @@ -0,0 +1,327 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; +import static de.gematik.idp.gsi.server.controller.FedIdpController.AUTH_CODE_LENGTH; +import static de.gematik.idp.gsi.server.data.GsiConstants.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; + +import de.gematik.idp.crypto.CryptoLoader; +import de.gematik.idp.crypto.Nonce; +import de.gematik.idp.field.ClientUtilities; +import de.gematik.idp.gsi.server.configuration.GsiConfiguration; +import de.gematik.idp.gsi.server.data.FedIdpAuthSession; +import de.gematik.idp.gsi.server.data.RpToken; +import de.gematik.idp.gsi.server.exceptions.GsiException; +import de.gematik.idp.token.JsonWebToken; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Set; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; + +@SpringBootTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +class RequestValidatorTest { + + @Autowired GsiConfiguration gsiConfiguration; + + private static final String CERT1_FROM_REQUEST = + "-----BEGIN%20CERTIFICATE-----%0AMIIDszCCApugAwIBAgIUY%2FqefKABeWr36nT%2Brw9hJsbYFu8wDQYJKoZIhvcNAQEL%0ABQAwdjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy%0AbGluMRkwFwYDVQQKDBBnZW1hdGlrVEVTVC1PTkxZMQ8wDQYDVQQLDAZQVCBJRE0x%0AGTAXBgNVBAMMEGZhZGlUbHNDbGllbnRSc2EwHhcNMjQwNjEzMDcxNjUyWhcNMjUw%0ANjEzMDcxNjUyWjB2MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYD%0AVQQHDAZCZXJsaW4xGTAXBgNVBAoMEGdlbWF0aWtURVNULU9OTFkxDzANBgNVBAsM%0ABlBUIElETTEZMBcGA1UEAwwQZmFkaVRsc0NsaWVudFJzYTCCASIwDQYJKoZIhvcN%0AAQEBBQADggEPADCCAQoCggEBAKiQaMTyY%2FlTTO9V4YJq7xsfN8l0%2BSqe2rRRasVU%0A8wenG8eohk99d1i5%2Fh08%2B%2BK1A5FX9GxgWh0RXGotpvbVvM7kzdOWxBJIK7j68R9g%0A%2F6B%2BKKO89rywLiJkxRT%2BOA4dusqocGDKmqFYZC1ntt2nSsSLlX3OuDC%2F1Thlhz2i%0AEGtweuYRL3zPeDXiegdyjRCY%2F9Xe%2FwaC4amuuJ5JkE5EsM0mL09kfkZCzdx8j2KK%0AqYTH2TYmiOG16CIVyZi9pE%2BKEHw95MIIcrzrO6QLWXcl7Y82rwVeeoUSicLBEydd%0A4YmsZ6pp%2BKGH0b9ycQO%2Bxs2uv79%2B5Zza9Q4OazEka4N0LyMCAwEAAaM5MDcwCQYD%0AVR0TBAIwADALBgNVHQ8EBAMCBeAwHQYDVR0OBBYEFMmogwgia7kONxur5UWBDX5g%0ABP0HMA0GCSqGSIb3DQEBCwUAA4IBAQAFK6nct1YVLMR6Tznh6ZrsvYs0UzCElUGM%0AnJtYaeCTgQPVKigQC4SPf%2FJp9qychooSbS7gbponndXgGIz8VFmt9y4d4q0uZKOr%0ALp7qcK%2BgQdvBts5TDZH20IiwW5b6VyGp%2Fos8fqR8WIt7fHdNz6Mu1fh2HsB4YjV9%0AxbbXTcKSzS6TROzh9bt2ubFX4ex56j6Mniy3DNF6zsW4kdh7naB%2FLfXvtH276Gj%2B%0AInhaF1sBLI8IIyQ5K2q2MJaly%2F8wiOys7FuG7duD1Lmh2kRO0FZkXsaQJmbZncUs%0A%2B4tgmnpEVgZ0FlKQ1BDAl0o0e7QbVRMiI2gjz7itOWFiUXvnMNIA%0A-----END%20CERTIFICATE-----%0A"; + + private static final RpToken VALID_RPTOKEN = + new RpToken(new JsonWebToken(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); + + private X509Certificate cert1FromEntityStmtRpService; + private X509Certificate cert2FromEntityStmtRpService; + + @SneakyThrows + @BeforeAll + void setup() { + + cert1FromEntityStmtRpService = + CryptoLoader.getCertificateFromPem( + java.net.URLDecoder.decode(CERT1_FROM_REQUEST, StandardCharsets.UTF_8).getBytes()); + + cert2FromEntityStmtRpService = + CryptoLoader.getCertificateFromPem( + FileUtils.readFileToByteArray( + new File("src/test/resources/keys/ref-key-rotation.crt"))); + } + + @Test + void test_validateParParams_VALID() { + final String correctRedirectUri = "https://redirect.testsuite.gsi"; + assertDoesNotThrow( + () -> + RequestValidator.validateParParams( + VALID_RPTOKEN, correctRedirectUri, "urn:telematik:versicherter openid")); + } + + @ValueSource( + strings = { + "urn:telematik:geburtsdatumurn:telematik:alter openid", + "urn%3Atelematik%3Adisplay_name", + "urn:telematik:given_name+openid", + "urn:telematik:schlecht openid" + }) + @ParameterizedTest(name = "checkException_verifyInvalidScopes scope: {0}") + void test_validateParParams_checkException_verifyInvalidScopes_VALID(final String scope) { + final String correctRedirectUri = "https://redirect.testsuite.gsi"; + + assertThatThrownBy( + () -> RequestValidator.validateParParams(VALID_RPTOKEN, correctRedirectUri, scope)) + .isInstanceOf(GsiException.class) + .hasMessageContaining( + "Content of parameter scope [" + scope + "] exceeds scopes found in entity statement."); + } + + @Test + void test_validateCertificate_match_VALID() { + + try (final MockedStatic mockedStatic = + Mockito.mockStatic(EntityStatementRpReader.class)) { + mockedStatic + .when(() -> EntityStatementRpReader.getRpTlsClientCerts(any())) + .thenReturn(List.of(cert2FromEntityStmtRpService, cert1FromEntityStmtRpService)); + + assertDoesNotThrow( + () -> + RequestValidator.validateCertificate( + CERT1_FROM_REQUEST, VALID_RPTOKEN, gsiConfiguration.isClientCertRequired())); + } + } + + @Test + void test_validateCertificate_noMatch_INVALID() { + + try (final MockedStatic mockedStatic = + Mockito.mockStatic(EntityStatementRpReader.class)) { + mockedStatic + .when(() -> EntityStatementRpReader.getRpTlsClientCerts(any())) + .thenReturn(List.of(cert2FromEntityStmtRpService)); + + assertThatThrownBy( + () -> + RequestValidator.validateCertificate( + CERT1_FROM_REQUEST, VALID_RPTOKEN, gsiConfiguration.isClientCertRequired())) + .isInstanceOf(GsiException.class) + .hasMessageContaining( + "client certificate in tls handshake does not match any certificate in entity" + + " statement/signed_jwks"); + } + } + + @Test + void test_validateCertificate_noTlsCert_INVALID() { + + assertThatThrownBy( + () -> + RequestValidator.validateCertificate( + "noTlsCert", VALID_RPTOKEN, gsiConfiguration.isClientCertRequired())) + .isInstanceOf(GsiException.class) + .hasMessageContaining( + "client certificate in tls handshake is not a valid x509 certificate"); + } + + @Test + void test_validateCertificate_certIsRequired_INVALID() { + assertThatThrownBy(() -> RequestValidator.validateCertificate(null, VALID_RPTOKEN, true)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("client certificate is missing"); + } + + @Test + void test_validateAuthRequestParams_VALID() { + + final FedIdpAuthSession session = + FedIdpAuthSession.builder() + .fachdienstClientId("http://localhost:8080") + .fachdienstState("") + .fachdienstCodeChallenge("") + .fachdienstCodeChallengeMethod("") + .fachdienstNonce("") + .requestedOptionalClaims(Set.of()) + .fachdienstRedirectUri("") + .authorizationCode(Nonce.getNonceAsHex(AUTH_CODE_LENGTH)) + .expiresAt( + ZonedDateTime.now().plusSeconds(gsiConfiguration.getRequestUriTTL()).toString()) + .build(); + assertDoesNotThrow( + () -> RequestValidator.validateAuthRequestParams(session, "http://localhost:8080")); + } + + @Test + void test_validateAuthRequestParams_throwsException_INVALID() { + + final FedIdpAuthSession session = + FedIdpAuthSession.builder() + .fachdienstClientId("http://localhost:8080") + .fachdienstState("") + .fachdienstCodeChallenge("") + .fachdienstCodeChallengeMethod("") + .fachdienstNonce("") + .requestedOptionalClaims(Set.of()) + .fachdienstRedirectUri("") + .authorizationCode(Nonce.getNonceAsHex(AUTH_CODE_LENGTH)) + .expiresAt( + ZonedDateTime.now().plusSeconds(gsiConfiguration.getRequestUriTTL()).toString()) + .build(); + assertThatThrownBy( + () -> RequestValidator.validateAuthRequestParams(session, "http://localhost:8083")) + .isInstanceOf(GsiException.class) + .hasMessageContaining("unknown client_id"); + } + + @Test + void test_verifyRedirectUri_VALID() { + assertDoesNotThrow( + () -> + RequestValidator.verifyRedirectUri( + "http://localhost:8080/AS", "http://localhost:8080/AS")); + } + + @Test + void test_verifyRedirectUri_throwsException_INVALID() { + assertThatThrownBy( + () -> + RequestValidator.verifyRedirectUri( + "http://localhost:8080/AS", "http://localhost:8080/AUTH")) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid redirect_uri"); + } + + @Test + void test_verifyCodeVerifier_VALID() { + final String codeVerifier = ClientUtilities.generateCodeVerifier(); + final String codeChallenge = ClientUtilities.generateCodeChallenge(codeVerifier); + assertDoesNotThrow(() -> RequestValidator.verifyCodeVerifier(codeVerifier, codeChallenge)); + } + + @Test + void test_verifyCodeVerifier_throwsException_INVALID() { + final String codeVerifier = ClientUtilities.generateCodeVerifier(); + final String invalidCodeChallenge = ClientUtilities.generateCodeChallenge("anyCodeVerifier"); + assertThatThrownBy( + () -> RequestValidator.verifyCodeVerifier(codeVerifier, invalidCodeChallenge)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid code_verifier"); + } + + @Test + void test_verifyClientId_VALID() { + assertDoesNotThrow( + () -> RequestValidator.verifyClientId("http://localhost:8080", "http://localhost:8080")); + } + + @Test + void test_verifyClientId_throwsException_INVALID() { + assertThatThrownBy( + () -> RequestValidator.verifyClientId("http://localhost:8080", "http://localhost:8083")) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid client_id"); + } + + @Test + void test_verifyIdpDoesSupportRequestedScopes_VALID() { + assertDoesNotThrow( + () -> + RequestValidator.verifyIdpDoesSupportRequestedScopes( + "urn:telematik:display_name urn:telematik:versicherter openid")); + } + + @Test + void test_verifyIdpDoesSupportRequestedScopes_throwsException_INVALID() { + assertThatThrownBy( + () -> + RequestValidator.verifyIdpDoesSupportRequestedScopes( + "urn:telematik:kvnr urn:telematik:versicherter openid")) + .isInstanceOf(GsiException.class) + .hasMessageContaining("More scopes requested in PAR than supported."); + } + + @Test + void test_validateAcrAmrCombination_validAcr_High_VALID() { + final Set acrHigh = Set.of(ACR_HIGH); + + assertDoesNotThrow(() -> RequestValidator.validateAmrAcrCombination(acrHigh, AMR_VALUES_HIGH)); + } + + @Test + void test_validateAcrAmrCombination_invalidAcr_High_INVALID() { + final Set acrHigh = Set.of(ACR_HIGH); + assertThatThrownBy(() -> RequestValidator.validateAmrAcrCombination(acrHigh, AMR_VALUES)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid combination of essential values acr and amr"); + } + + @Test + void test_validateAcrAmrCombination_validAcr_Substantial_VALID() { + final Set acrSubstantial = Set.of(ACR_SUBSTANTIAL); + assertDoesNotThrow( + () -> RequestValidator.validateAmrAcrCombination(acrSubstantial, AMR_VALUES_SUBSTANTIAL)); + } + + @Test + void test_validateAcrAmrCombination_invalidAcr_Substantial_INVALID() { + final Set acrSubstantial = Set.of(ACR_SUBSTANTIAL); + assertThatThrownBy(() -> RequestValidator.validateAmrAcrCombination(acrSubstantial, AMR_VALUES)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid combination of essential values acr and amr"); + } + + @Test + void test_validateAcrAmrCombination_validAcr_SubstantialAndHigh_VALID() { + assertDoesNotThrow(() -> RequestValidator.validateAmrAcrCombination(ACR_VALUES, AMR_VALUES)); + } + + @Test + void test_validateAcrAmrCombination_invalidAcr_INVALID() { + final Set acrInvalid = Set.of(ACR_SUBSTANTIAL, "invalidAcr"); + assertThatThrownBy( + () -> RequestValidator.validateAmrAcrCombination(acrInvalid, AMR_VALUES_SUBSTANTIAL)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid acr value"); + } + + @Test + void test_validateAcrAmrCombination_invalidAmr_INVALID() { + final Set acrHigh = Set.of(ACR_HIGH); + final Set amrInvalid = Set.of("urn:telematik:auth:eGK", "urn:telematik:auth:invalid"); + assertThatThrownBy(() -> RequestValidator.validateAmrAcrCombination(acrHigh, amrInvalid)) + .isInstanceOf(GsiException.class) + .hasMessageContaining("invalid amr value"); + } +} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/SektoralIdpAuthenticatorTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/SektoralIdpAuthenticatorTest.java index ea3b234..2019ea5 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/SektoralIdpAuthenticatorTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/SektoralIdpAuthenticatorTest.java @@ -33,7 +33,7 @@ class SektoralIdpAuthenticatorTest { @Autowired SektoralIdpAuthenticator sektoralIdpAuthenticator; @Test - void testCreateLocationForAuthorizationResponse() { + void test_createLocationForAuthorizationResponse_VALID() { final AtomicReference location = new AtomicReference<>(); Assertions.assertDoesNotThrow( @@ -45,7 +45,7 @@ void testCreateLocationForAuthorizationResponse() { } @Test - void testCreateLocationForAuthorizationResponseUriSyntaxException() { + void test_createLocationForAuthorizationResponse_UriSyntaxException_INVALID() { final AtomicReference location = new AtomicReference<>(); assertThatThrownBy( diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/TokenRepositoryRpTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/TokenRepositoryRpTest.java new file mode 100644 index 0000000..366304e --- /dev/null +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/services/TokenRepositoryRpTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023 gematik GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.gematik.idp.gsi.server.services; + +import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; +import static de.gematik.idp.gsi.server.common.Constants.ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; + +import de.gematik.idp.gsi.server.configuration.GsiConfiguration; +import de.gematik.idp.gsi.server.data.RpToken; +import de.gematik.idp.token.JsonWebToken; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockserver.client.MockServerClient; +import org.mockserver.springtest.MockServerTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@MockServerTest("server.url=http://localhost:${mockServerPort}") +@SpringBootTest +class TokenRepositoryRpTest { + + private MockServerClient mockServerClient; + @Autowired private TokenRepositoryRp tokenRepositoryRp; + @Autowired GsiConfiguration gsiConfiguration; + @MockBean private ServerUrlService serverUrlService; + private static MockedStatic httpClientMockedStatic; + + private static final RpToken VALID_RPTOKEN = + new RpToken(new JsonWebToken(ENTITY_STMNT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); + + @Value("${server.url}") + private String mockServerUrl; + + @BeforeEach + void init(final TestInfo testInfo) { + Mockito.doReturn(mockServerUrl + "/federation/fetch") + .when(serverUrlService) + .determineFetchEntityStatementEndpoint(); + httpClientMockedStatic = Mockito.mockStatic(HttpClient.class); + + httpClientMockedStatic + .when(() -> HttpClient.fetchEntityStatementRp(any())) + .thenReturn(VALID_RPTOKEN); + httpClientMockedStatic + .when(() -> HttpClient.fetchEntityStatementAboutRp(any(), any(), any())) + .thenReturn(new JsonWebToken(ENTITY_STMNT_ABOUT_IDP_FACHDIENST_EXPIRES_IN_YEAR_2043)); + } + + @AfterEach + void tearDown() { + httpClientMockedStatic.close(); + } + + @Test + void test_getEntityStatementRp_VALID() { + + final RpToken entStmntFd = tokenRepositoryRp.getEntityStatementRp("http://any-client-id:8080"); + assertThat(entStmntFd).isNotNull(); + } + + @Test + void test_getEntityStatementAboutRp_Idpfachdienst_VALID() { + final JsonWebToken entityStmntAboutFachdienst = + tokenRepositoryRp.getEntityStatementAboutRp("http://any-client-id:8080"); + assertThat(entityStmntAboutFachdienst).isNotNull(); + } +} diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/token/IdTokenBuilderTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/token/IdTokenBuilderTest.java index db82d8a..88c6b78 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/token/IdTokenBuilderTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/token/IdTokenBuilderTest.java @@ -81,7 +81,7 @@ public void init() { } @Test - void checkIdTokenClaims() { + void test_checkIdTokenClaims_VALID() { final JsonWebToken idToken = idTokenBuilder.buildIdToken(); assertThat(idToken.getBodyClaims()) diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/ClaimHelperTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/ClaimHelperTest.java index 9f967ba..58e312f 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/ClaimHelperTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/ClaimHelperTest.java @@ -28,7 +28,7 @@ class ClaimHelperTest { @Test - void getClaimsForValidScopeSet() { + void test_getClaimsForValidScopeSet_VALID() { assertThat( getClaimsForScopeSet( new HashSet<>( @@ -46,7 +46,7 @@ void getClaimsForValidScopeSet() { } @Test - void getClaimsForInvalidScopeSet() { + void test_getClaimsForInvalidScopeSet_INVALID() { assertDoesNotThrow( () -> getClaimsForScopeSet(new HashSet<>(Arrays.asList("openid", "invalid:scope")))); } diff --git a/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/EntityStatementRpTest.java b/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/EntityStatementRpTest.java index e3e9b21..231f9cd 100644 --- a/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/EntityStatementRpTest.java +++ b/gsi-server/src/test/java/de/gematik/idp/gsi/server/util/EntityStatementRpTest.java @@ -43,7 +43,7 @@ class EntityStatementRpTest { } @Test - void verifySignature_Token1Valid() { + void test_verifySignature_Token1_VALID() { final PublicKey publicKey = KeyUtility.readX509PublicKey(new File("src/test/resources/keys/fachdienst-sig-pub.pem")); assertDoesNotThrow( @@ -51,7 +51,7 @@ void verifySignature_Token1Valid() { } @Test - void verifySignature_Token1Invalid_SigAlgNone() { + void test_verifySignature_Token1_SigAlgNone_INVALID() { final PublicKey publicKey = KeyUtility.readX509PublicKey(new File("src/test/resources/keys/fachdienst-sig-pub.pem")); final JsonWebToken jwt = @@ -60,7 +60,7 @@ void verifySignature_Token1Invalid_SigAlgNone() { } @Test - void verifySignature_Token2Valid() { + void test_verifySignature_Token2_VALID() { final PublicKey publicKey = KeyUtility.readX509PublicKey( new File("src/test/resources/keys/fedmaster-sigkey-TU-pub.pem")); @@ -71,7 +71,7 @@ void verifySignature_Token2Valid() { } @Test - void verifySignature_TokenExpired() { + void test_verifySignature_TokenExpired_INVALID() { final PublicKey publicKey = KeyUtility.readX509PublicKey(new File("src/test/resources/keys/fachdienst-sig-pub.pem")); final JsonWebToken jsonWebTokenExpired = new JsonWebToken(ENTITY_STMNT_IDP_FACHDIENST_EXPIRED); diff --git a/gsi-server/src/test/resources/keys/ref-key-rotation.crt b/gsi-server/src/test/resources/keys/ref-key-rotation.crt new file mode 100644 index 0000000..247161a --- /dev/null +++ b/gsi-server/src/test/resources/keys/ref-key-rotation.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICMDCCAdegAwIBAgIUJ+m5b4duPFfStUjJdkRplJ8nFG8wCgYIKoZIzj0EAwIw +ezELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGlu +MRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEPMA0GA1UECwwGUFQgSURNMR0w +GwYDVQQDDBRyZWYgY2VydCAyIFRFU1QtT05MWTAeFw0yNDA4MTQwNzE2MDlaFw0y +ODA5MjIwNzE2MDlaMHsxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzAN +BgNVBAcMBkJlcmxpbjEaMBgGA1UECgwRZ2VtYXRpayBOT1QtVkFMSUQxDzANBgNV +BAsMBlBUIElETTEdMBsGA1UEAwwUcmVmIGNlcnQgMiBURVNULU9OTFkwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAATipWs36KbDasCRa51DFfU6YRHQ9jsERuZDKksM +0qsFhoXb1Xsv4rvQ6HOQjEYZ1AFKZBqPMeMAVYbgSA5aaKmiozkwNzAJBgNVHRME +AjAAMAsGA1UdDwQEAwIF4DAdBgNVHQ4EFgQU5EIp7BjRq8gfi/hIQMoH0PdRgdow +CgYIKoZIzj0EAwIDRwAwRAIgX6Kjr/pqqQDUvmYinlYMlocoZdZZn61yIbA5Mr8Y +IPkCIAWKhzwnVyU8Qkn2IWzpHDvtKEyMztqKqvwDLTSIZKO8 +-----END CERTIFICATE----- diff --git a/gsi-testsuite/pom.xml b/gsi-testsuite/pom.xml index eea9b05..282d672 100644 --- a/gsi-testsuite/pom.xml +++ b/gsi-testsuite/pom.xml @@ -6,22 +6,22 @@ de.gematik.idp gsi-testsuite - 7.0.3 + 7.0.4 jar Testsuite fuer sektorale IDPs - 29.0.1 + 29.0.2 21 1.18.34 3.13.0 3.5.0 - 3.3.1 + 3.5.1 3.3.1 - 3.3.1 + 3.5.1 2.43.0 1.17.0 - 3.1.3 + 3.4.2 0.8.12 @@ -30,7 +30,7 @@ ch.qos.logback logback-classic - 1.5.6 + 1.5.11 @@ -51,11 +51,16 @@
- + + io.cucumber + cucumber-junit-platform-engine + 7.19.0 + test + org.junit.vintage junit-vintage-engine - 5.10.3 + 5.11.3 test @@ -181,13 +186,10 @@ maven-failsafe-plugin ${version.maven-failsafe-plugin} - 18000 **/Driver*IT.java - classes - true ${skip.inttests} @@ -252,7 +254,7 @@ org.codehaus.mojo exec-maven-plugin - 3.3.0 + 3.4.1 check-file-existence diff --git a/gsi-testsuite/src/test/resources/features/gsiSpecificAuthentication.feature b/gsi-testsuite/src/test/resources/features/gsiSpecificAuthentication.feature index 95d380f..b78d9b3 100644 --- a/gsi-testsuite/src/test/resources/features/gsiSpecificAuthentication.feature +++ b/gsi-testsuite/src/test/resources/features/gsiSpecificAuthentication.feature @@ -188,7 +188,7 @@ Feature: Test GSI specific authentication Then TGR current response at "$.body" matches as JSON: """ { - "requested_claims": "${json-unit.ignore}" + "requested_claims": "${json-unit.ignore}" } """ And TGR current response with attribute "$.body.requested_claims.0" matches "urn:telematik:claims:profession" diff --git a/gsi-testsuite/src/test/resources/features/idpsektoralAuthorizationEndpoint.feature b/gsi-testsuite/src/test/resources/features/idpsektoralAuthorizationEndpoint.feature index 4b04356..895e5d7 100644 --- a/gsi-testsuite/src/test/resources/features/idpsektoralAuthorizationEndpoint.feature +++ b/gsi-testsuite/src/test/resources/features/idpsektoralAuthorizationEndpoint.feature @@ -23,6 +23,7 @@ Feature: Test IdpSektoral's Auth Endpoint And TGR find request to path ".*/.well-known/openid-federation" Then TGR set local variable "pushed_authorization_request_endpoint" to "!{rbel:currentResponseAsString('$..pushed_authorization_request_endpoint')}" Then TGR set local variable "authorization_endpoint" to "!{rbel:currentResponseAsString('$..authorization_endpoint')}" + And TGR HttpClient followRedirects Konfiguration deaktiviert @TCID:IDPSEKTORAL_AUTH_ENDPOINT_001 @Approval diff --git a/gsi-testsuite/src/test/resources/features/idpsektoralPushedAuthorizationEndpoint.feature b/gsi-testsuite/src/test/resources/features/idpsektoralPushedAuthorizationEndpoint.feature index f998d02..78ca358 100644 --- a/gsi-testsuite/src/test/resources/features/idpsektoralPushedAuthorizationEndpoint.feature +++ b/gsi-testsuite/src/test/resources/features/idpsektoralPushedAuthorizationEndpoint.feature @@ -19,15 +19,19 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint Background: Initialisiere Testkontext durch Abfrage des Entity Statements + Given TGR clear recorded messages When TGR sende eine leere GET Anfrage an "${gsi.fachdienstEntityStatementEndpoint}" And TGR find request to path ".*/.well-known/openid-federation" Then TGR set local variable "pushed_authorization_request_endpoint" to "!{rbel:currentResponseAsString('$..pushed_authorization_request_endpoint')}" + And TGR HttpClient followRedirects Konfiguration deaktiviert + And Wait for 1 Seconds @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_001 - @Approval - @PRIO:1 - @TESTSTUFE:4 - Scenario: IdpSektoral Pushed Auth Endpoint - Gutfall - Validiere Response + @Approval + @PRIO:1 + @TESTSTUFE:4 + @OpenBug + Scenario Outline: IdpSektoral Pushed Auth Endpoint - Gutfall - Validiere Response ``` Wir senden einen PAR an den sektoralen IDP @@ -38,13 +42,19 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint - einen json-Body enthalten Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "201" And TGR current response with attribute "$.header.Content-Type" matches "application/json.*" + Examples: + | acr_values | + | gematik-ehealth-loa-high | + | gematik-ehealth-loa-substantial | +# | gematik-ehealth-loa-substantial gematik-ehealth-loa-high | + @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_002 @Approval @@ -59,9 +69,9 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint Die Response muss als Body eine korrekte json Struktur enthalten: Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | And TGR find request to path ".*" Then TGR current response at "$.body" matches as JSON: """ @@ -74,7 +84,6 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_003 @Approval @PRIO:1 - @TESTSTUFE:4 Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - fehlerhaft befüllte Parameter ``` @@ -83,7 +92,7 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint Die Response muss als Body eine passende Fehlermeldung enthalten: Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | | | yyystateyyy | | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | | | vy7rM801AQw1or22GhrZ | | | And TGR find request to path ".*" @@ -99,13 +108,13 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint And TGR current response with attribute "$.body.error" matches "" Examples: - | client_id | redirect_uri | code_challenge_method | response_type | scope | acr_values | error | responseCode | - | notUrl | gsi.redirectUri | S256 | code | gsi.scope | gematik-ehealth-loa-high | invalid_.* | 40.* | - | gsi.clientid.valid | gsi.redirectUri | plain | code | gsi.scope | gematik-ehealth-loa-high | invalid_request | 400 | - | gsi.clientid.valid | gsi.redirectUri | S256 | token | gsi.scope | gematik-ehealth-loa-high | .* | 400 | - | gsi.clientid.valid | gsi.redirectUri | S256 | code | invalidScope | gematik-ehealth-loa-high | invalid_scope | 400 | - | gsi.clientid.valid | gsi.redirectUri | S256 | code | gsi.scope | invalidAcr | invalid_request | 400 | - | gsi.clientid.valid | https://invalidRedirect | S256 | code | gsi.scope | gematik-ehealth-loa-high | invalid_request | 400 | + | client_id | redirect_uri | code_challenge_method | response_type | scope | acr_values | error | responseCode | + | notUrl | ${gsi.redirectUri} | S256 | code | ${gsi.scope} | gematik-ehealth-loa-high | invalid_.* | 40.* | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | plain | code | ${gsi.scope} | gematik-ehealth-loa-high | invalid_request | 400 | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | S256 | token | ${gsi.scope} | gematik-ehealth-loa-high | .* | 400 | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | S256 | code | invalidScope | gematik-ehealth-loa-high | invalid_scope | 400 | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | S256 | code | ${gsi.scope} | invalidAcr | invalid_request | 400 | + | ${gsi.clientid.valid} | https://invalidRedirect | S256 | code | ${gsi.scope} | gematik-ehealth-loa-high | invalid_request | 400 | @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_004 @@ -145,7 +154,7 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint Die Response muss als Body eine passende Fehlermeldung enthalten: Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | | | yyystateyyy | | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | | | vy7rM801AQw1or22GhrZ | | | And TGR find request to path ".*" @@ -161,12 +170,12 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint And TGR current response with attribute "$.body.error" matches "(invalid_request|invalid_scope|unsupported_response_type)" Examples: - | client_id | redirect_uri | code_challenge_method | response_type | scope | acr_values | responseCode | - | $REMOVE | gsi.redirectUri | S256 | code | gsi.scope | gematik-ehealth-loa-high | 400 | - | gsi.clientid.valid | $REMOVE | S256 | code | gsi.scope | gematik-ehealth-loa-high | 400 | - | gsi.clientid.valid | gsi.redirectUri | $REMOVE | code | gsi.scope | gematik-ehealth-loa-high | 400 | - | gsi.clientid.valid | gsi.redirectUri | S256 | $REMOVE | gsi.scope | gematik-ehealth-loa-high | 400 | - | gsi.clientid.valid | gsi.redirectUri | S256 | code | $REMOVE | gematik-ehealth-loa-high | 400 | + | client_id | redirect_uri | code_challenge_method | response_type | scope | acr_values | responseCode | + | $REMOVE | ${gsi.redirectUri} | S256 | code | ${gsi.scope} | gematik-ehealth-loa-high | 400 | + | ${gsi.clientid.valid} | $REMOVE | S256 | code | ${gsi.scope} | gematik-ehealth-loa-high | 400 | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | $REMOVE | code | ${gsi.scope} | gematik-ehealth-loa-high | 400 | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | S256 | $REMOVE | ${gsi.scope} | gematik-ehealth-loa-high | 400 | + | ${gsi.clientid.valid} | ${gsi.redirectUri} | S256 | code | $REMOVE | gematik-ehealth-loa-high | 400 | @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_006 @@ -181,9 +190,9 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint Die Response auf den zweiten PAR muss als Body eine passende Fehlermeldung enthalten: Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "201" And TGR clear recorded messages @@ -204,6 +213,7 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_007 @PRIO:1 @TESTSTUFE:4 + @Approval Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - invalide Entity Statements ``` @@ -211,7 +221,7 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint so dass die Autoregistrierung scheitern muss. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | | | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | And TGR find request to path ".*" @@ -219,15 +229,15 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint And TGR current response at "$.body" matches as JSON: """ { - "error": '(invalid_request|invalid_client)', + "error": '(invalid_request|invalid_client|missing_trust_anchor)', "____error_description": '.*' } """ Examples: - | client_id | responseCode | - | gsi.clientid.expired | 40.* | - | gsi.clientid.invalidSignature | 40.* | + | client_id | responseCode | + | ${gsi.clientid.expired} | 40.* | + | ${gsi.clientid.invalidSignature} | 40.* | @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_008 @@ -243,15 +253,15 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint Dieser Request muss abgelehnt werden. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | | gematik-ehealth-loa-high | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | | gematik-ehealth-loa-high | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "" Examples: | scope | responseCode | - | gsi.scope | 201 | + | ${gsi.scope} | 201 | | urn:telematik:geburtsdatum openid | 40.* | @@ -259,16 +269,16 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @Approval @PRIO:1 @TESTSTUFE:4 - Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - Zusätzliche Parameter + Scenario Outline: IdpSektoral Pushed Auth Endpoint - Positivfall - Zusätzliche Parameter ``` Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dieser enthält zusätzliche Parameter. Die erste Variante entspricht dem PAR des eRezept-Authservers, Die zweite enthält einen unbekannten Parameter. Der IDP muss den PAR akzeptieren. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "" @@ -281,66 +291,51 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_010 @Akzeptanzfeature @Approval + @PRIO:1 + @TESTSTUFE:4 Scenario Outline: IdpSektoral Pushed Auth Endpoint - Positivfall - Amr/Acr Kombinationen ``` Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionale Parameter amr mit verschiedenen Werten mitgeschickt. - Alle gelisteten Kombinationen sind gültig und Spec-konform. + Alle gelisteten Kombinationen sind teilweise gültig/Spec-konform und teilweise genau nicht. + Der IDP kann auch die invaliden Request akzeptieren und diese invaliden "Vorschläge" dann ignorieren. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | amr | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | amr | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | | | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "201" - Examples: - | acr_values | amr | - | gematik-ehealth-loa-high | urn:telematik:auth:eGK | - | gematik-ehealth-loa-high | urn:telematik:auth:eID | - | gematik-ehealth-loa-high | urn:telematik:auth:sso | - | gematik-ehealth-loa-substantial | urn:telematik:auth:mEW | - | gematik-ehealth-loa-high | urn:telematik:auth:guest:eGK | - | gematik-ehealth-loa-substantial | urn:telematik:auth:other | - | gematik-ehealth-loa-high | urn:telematik:auth:other | - - - @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_011 - @Akzeptanzfeature - @OpenBug - Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - Amr/Acr Kombinationen - - ``` - Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionale Parameter amr mit verschiedenen Werten mitgeschickt. - Keine der gelisteten Kombinationen ist gültig oder Spec-konform. - - Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | amr | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | | | - And TGR find request to path ".*" - Then TGR current response with attribute "$.responseCode" matches "400" - Examples: | acr_values | amr | + | gematik-ehealth-loa-high | urn:telematik:auth:eGK | + | gematik-ehealth-loa-high | urn:telematik:auth:eID | + | gematik-ehealth-loa-high | urn:telematik:auth:sso | + | gematik-ehealth-loa-substantial | urn:telematik:auth:mEW | + | gematik-ehealth-loa-high | urn:telematik:auth:guest:eGK | + | gematik-ehealth-loa-substantial | urn:telematik:auth:other | + | gematik-ehealth-loa-high | urn:telematik:auth:other | | gematik-ehealth-loa-substantial | urn:telematik:auth:eGK | | gematik-ehealth-loa-substantial | urn:telematik:auth:eID | - | gematik-ehealth-loa-substantial | urn:telematik:auth:sso | + | gematik-ehealth-loa-substantial | urn:telematik:auth:sso | | gematik-ehealth-loa-high | urn:telematik:auth:mEW | | gematik-ehealth-loa-substantial | urn:telematik:auth:guest:eGK | + @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_012 @Akzeptanzfeature @Approval + @GematikSekIdpOnly Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - Amr Parameter ``` Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionale Parameter amr mit verschiedenen invaliden Werten mitgeschickt, die nicht spec-konform sind. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | amr | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | amr | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "400" And TGR current response at "$.body" matches as JSON: @@ -361,16 +356,17 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_013 @Akzeptanzfeature @Approval + @PRIO:1 + @TESTSTUFE:4 Scenario Outline: IdpSektoral Pushed Auth Endpoint - Gutfall - Prompt Parameter ``` - Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei werden die optionalen Parameter prompt mit verschiedenen validen Werten mitgeschickt. - Keine der gelisteten Kombinationen ist gültig oder Spec-konform. + Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionalen Parameter prompt mit verschiedenen validen Werten mitgeschickt. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | prompt | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | prompt | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "201" @@ -381,21 +377,24 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint | consent | | select_account | + @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_014 @Akzeptanzfeature - @OpenBug + @PRIO:1 + @TESTSTUFE:4 + @Approval Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - Prompt Parameter ``` - Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei werden die optionalen Parameter prompt mit verschiedenen invaliden Werten mitgeschickt. - Keine der gelisteten Kombinationen ist gültig oder Spec-konform. + Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionale Parameter prompt mit invaliden Werten mitgeschickt. + Keine der gelisteten Kombinationen ist gültig. Der IDP darf diesen Wert aber ignorieren und eine 201 schicken oder mit einer Fehlermeldung ablehnen. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | prompt | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | prompt | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | And TGR find request to path ".*" - Then TGR current response with attribute "$.responseCode" matches "400" + Then TGR current response with attribute "$.responseCode" matches "(201|400)" Examples: | prompt | @@ -404,16 +403,18 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_015 @Akzeptanzfeature @Approval + @PRIO:1 + @TESTSTUFE:4 Scenario Outline: IdpSektoral Pushed Auth Endpoint - Positivfall - max_age Parameter ``` Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei werden die optionalen Parameter max_age mit verschiedenen validen Werten mitgeschickt. - Keine der gelisteten Kombinationen ist gültig oder Spec-konform. + Die gelisteten Kombinationen sind gültig und müssen akzeptiert werden. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | max_age | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | max_age | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | And TGR find request to path ".*" Then TGR current response with attribute "$.responseCode" matches "201" @@ -424,23 +425,73 @@ Feature: Test IdpSektoral's Pushed Auth Endpoint @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_016 @Akzeptanzfeature - @OpenBug + @PRIO:1 + @TESTSTUFE:4 + @Approval Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - max_age Parameter ``` Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei werden die optionalen Parameter max_age mit verschiedenen invaliden Werten mitgeschickt. - Keine der gelisteten Kombinationen ist gültig oder Spec-konform. + Keine der gelisteten Kombinationen ist gültig oder Spec-konform. Der IDP darf diesen Wert aber ignorieren und eine 201 schicken oder mit einer Fehlermeldung ablehnen. Given TGR clear recorded messages - When Send Post Request to "${pushed_authorization_request_endpoint}" with - | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | max_age | - | gsi.clientid.valid | yyystateyyy | gsi.redirectUri | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | gsi.scope | gematik-ehealth-loa-high | | + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | max_age | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | And TGR find request to path ".*" - Then TGR current response with attribute "$.responseCode" matches "400" + Then TGR current response with attribute "$.responseCode" matches "(201|400)" Examples: | max_age | | -123 | + @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_017 + @Akzeptanzfeature + @OpenBug + @PRIO:1 + @TESTSTUFE:4 + @Approval + Scenario Outline: IdpSektoral Pushed Auth Endpoint - Negativfall - claims Parameter + + ``` + Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionale Parameter claims mit verschiedenen invaliden Werten mitgeschickt. + Keine der gelisteten Kombinationen ist gültig. Da die invaliden Claims essential sind, muss der IDP den Request ablehnen. + Given TGR clear recorded messages + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | claims | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | + And TGR find request to path ".*" + Then TGR current response with attribute "$.responseCode" matches "400" + + Examples: + | claims | + | {"id_token":{"acr":{"essential":true,"values":["gematik-ehealth-loa-invalid"]}}} | + | {"id_token":{"amr":{"essential":true,"value":"invalidAmr"}}} | + | {"id_token":{"invalid_claim":{"essential":true}}} | + | {"id_token":{"invalid_claim":null}} | + + + @TCID:IDPSEKTORAL_PUSHED_AUTH_ENDPOINT_018 + @Akzeptanzfeature + @PRIO:1 + @TESTSTUFE:4 + @Approval + Scenario Outline: IdpSektoral Pushed Auth Endpoint - Positivfall - claims Parameter + + ``` + Wir senden einen PAR, um die Autoregistrierung anzustoßen. Dabei wird der optionale Parameter claims mit validen Werten geschickt. + Die erste Variante ist ein Gastlogin, bei der zweiten können nicht-essential-Claims ignoriert werden. + + Given TGR clear recorded messages + When TGR send POST request to "${pushed_authorization_request_endpoint}" with: + | client_id | state | redirect_uri | code_challenge | code_challenge_method | response_type | nonce | scope | acr_values | claims | + | ${gsi.clientid.valid} | yyystateyyy | ${gsi.redirectUri} | 9tI-0CQIkUYaGQOVR1emznlDFjlX0kVY1yd3oiMtGUI | S256 | code | vy7rM801AQw1or22GhrZ | ${gsi.scope} | gematik-ehealth-loa-high | | + And TGR find request to path ".*" + Then TGR current response with attribute "$.responseCode" matches "201" + + Examples: + | claims | + | {"id_token":{"acr":{"essential":true,"value":"gematik-ehealth-loa-high"},"amr":{"essential":true,"value":"urn:telematik:auth:guest:eGK"}}} | + | {"id_token":{"acr":{"value":"gematik-ehealth-loa-invalid"}}} | diff --git a/gsi-testsuite/src/test/resources/features/idpsektoralTokenEndpoint.feature b/gsi-testsuite/src/test/resources/features/idpsektoralTokenEndpoint.feature index 535b275..8a46a10 100644 --- a/gsi-testsuite/src/test/resources/features/idpsektoralTokenEndpoint.feature +++ b/gsi-testsuite/src/test/resources/features/idpsektoralTokenEndpoint.feature @@ -24,6 +24,7 @@ Feature: Test IdpSektoral's Token Endpoint Then TGR set local variable "pushed_authorization_request_endpoint" to "!{rbel:currentResponseAsString('$..pushed_authorization_request_endpoint')}" Then TGR set local variable "authorization_endpoint" to "!{rbel:currentResponseAsString('$..authorization_endpoint')}" Then TGR set local variable "token_endpoint" to "!{rbel:currentResponseAsString('$..token_endpoint')}" + And TGR HttpClient followRedirects Konfiguration deaktiviert @TCID:IDPSEKTORAL_TOKEN_ENDPOINT_001 diff --git a/gsi-testsuite/src/test/resources/features/registration.feature b/gsi-testsuite/src/test/resources/features/registration.feature index fb8036b..a6545d4 100644 --- a/gsi-testsuite/src/test/resources/features/registration.feature +++ b/gsi-testsuite/src/test/resources/features/registration.feature @@ -24,6 +24,7 @@ Feature: Test Fed Master's Entity Statement about IdpSektoral And TGR find request to path "/.well-known/openid-federation" Then TGR set local variable "fedmasterFederationFetchEndpoint" to "!{rbel:currentResponseAsString('$..federation_fetch_endpoint')}" Then TGR set local variable "fedmasterIdpListEndpoint" to "!{rbel:currentResponseAsString('$..idp_list_endpoint')}" + And TGR HttpClient followRedirects Konfiguration deaktiviert @TCID:IDPSEKTORAL_FEDM_ENTITY_STATEMENT_001 @PRIO:1 diff --git a/gsi-testsuite/tiger-external-Idp.yaml b/gsi-testsuite/tiger-external-Idp.yaml index a0f2e98..379f500 100644 --- a/gsi-testsuite/tiger-external-Idp.yaml +++ b/gsi-testsuite/tiger-external-Idp.yaml @@ -16,7 +16,7 @@ tigerProxy: lib: activateWorkflowUI: false -additionalYamls: +additionalConfigurationFiles: - filename: tc_properties-external-Idp.yaml baseKey: gsi diff --git a/pom.xml b/pom.xml index cab4e63..28f31b7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,12 +7,12 @@ org.springframework.boot spring-boot-starter-parent - 3.3.2 + 3.3.4 de.gematik.idp gemSekIdp-global - 7.0.3 + 7.0.4 pom gsi - gematik sektoraler IDP @@ -87,17 +87,17 @@ 1.78.1 - 2.16.1 + 2.17.0 1.0.1 20240303 - 2.23.1 + 2.24.1 4.4.4 4.2.9 - 5.3.1 - 0.45.0 - 29.0.1 - 29.0.1 - 29.0.1 + 5.3 + 0.45.1 + 29.0.2 + 29.0.2 + 29.0.2 0.8.12 4.0.0 @@ -107,13 +107,13 @@ 3.13.0 3.5.0 3.2.5 - 3.2.4 - 3.6.2 + 3.2.7 + 3.8.0 3.3.1 - 3.12.1 + 3.20.0 - 3.3.1 + 3.5.1 1.7.0 4.0.0.4121 2.43.0 diff --git a/runTestsuite-external-Idp.sh b/runTestsuite-external-Idp.sh old mode 100644 new mode 100755