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