Skip to content

Commit

Permalink
v7.0.4
Browse files Browse the repository at this point in the history
  • Loading branch information
Gematik-Entwicklung authored and TabeaHarper committed Oct 23, 2024
1 parent ea29bcf commit 502a285
Show file tree
Hide file tree
Showing 53 changed files with 3,141 additions and 1,995 deletions.
10 changes: 10 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion gsi-coverage-report/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>de.gematik.idp</groupId>
<artifactId>gemSekIdp-global</artifactId>
<version>7.0.3</version>
<version>7.0.4</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
6 changes: 3 additions & 3 deletions gsi-fedmaster/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
<parent>
<groupId>de.gematik.idp</groupId>
<artifactId>gemSekIdp-global</artifactId>
<version>7.0.3</version>
<version>7.0.4</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>gsi-fedmaster</artifactId>
<version>7.0.3</version>
<version>7.0.4</version>
<packaging>jar</packaging>

<name>gsi-fedmaster</name>
Expand Down Expand Up @@ -95,7 +95,7 @@
<dependency>
<groupId>org.jmdns</groupId>
<artifactId>jmdns</artifactId>
<version>3.5.9</version>
<version>3.5.12</version>
</dependency>
</dependencies>

Expand Down
21 changes: 13 additions & 8 deletions gsi-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
<parent>
<groupId>de.gematik.idp</groupId>
<artifactId>gemSekIdp-global</artifactId>
<version>7.0.3</version>
<version>7.0.4</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>gsi-server</artifactId>
<version>7.0.3</version>
<version>7.0.4</version>
<packaging>jar</packaging>

<name>gsi-server</name>
Expand Down Expand Up @@ -60,7 +60,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -92,11 +92,10 @@
<groupId>com.konghq</groupId>
<artifactId>unirest-java-core</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${version.httpclient}</version>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>${version.httpcore5}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
Expand Down Expand Up @@ -138,7 +137,13 @@
<dependency>
<groupId>org.jmdns</groupId>
<artifactId>jmdns</artifactId>
<version>3.5.9</version>
<version>3.5.12</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ public class GsiConfiguration {
private KeyConfig tokenSigPubKeyConfig;
private String loglevel;
private Integer requestUriTTL;
private boolean isRequiredClientCert;
private boolean clientCertRequired;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,39 @@
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;
import jakarta.validation.constraints.Pattern;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());

Expand All @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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<String> requestedScopes = session.getRequestedScopes();
final Set<String> 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
Expand All @@ -289,8 +293,11 @@ public void getAuthorizationCode(
serverUrlService.determineServerUrl());
final FedIdpAuthSession session = getSessionByRequestUri(requestUri);

final Set<String> requestedScopes = session.getRequestedScopes();
final Set<String> requestedClaims = getClaimsForScopeSet(requestedScopes);
final Set<String> requestedClaims =
Stream.concat(
session.getRequestedOptionalClaims().stream(),
session.getRequestedEssentialClaims().stream())
.collect(Collectors.toSet());

final Set<String> selectedClaimsSet;
selectedClaimsSet = getSelectedClaimsSet(selectedClaims, requestedClaims);
Expand Down Expand Up @@ -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());
Expand All @@ -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);
Expand All @@ -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))
Expand Down Expand Up @@ -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<String> getSelectedClaimsSet(
final String selectedClaims, final Set<String> requestedClaims) {
final Set<String> selectedClaimsSet;
Expand All @@ -442,35 +426,4 @@ private static Set<String> 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();
}
}
}
}
Loading

0 comments on commit 502a285

Please sign in to comment.