dev.fitko.fitconnect.core.routing.RouteVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of client Show documentation
Show all versions of client Show documentation
Library that provides client access to the FIT-Connect api-endpoints for sending, subscribing and
routing
The newest version!
package dev.fitko.fitconnect.core.routing;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import dev.fitko.fitconnect.api.config.Environment;
import dev.fitko.fitconnect.api.domain.model.route.Route;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.internal.InvalidKeyException;
import dev.fitko.fitconnect.api.exceptions.internal.RestApiException;
import dev.fitko.fitconnect.api.exceptions.internal.ValidationException;
import dev.fitko.fitconnect.api.services.keys.KeyService;
import dev.fitko.fitconnect.api.services.routing.RoutingVerificationService;
import dev.fitko.fitconnect.api.services.validation.ValidationService;
import java.net.URI;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static dev.fitko.fitconnect.api.domain.validation.ValidationResult.error;
import static dev.fitko.fitconnect.api.domain.validation.ValidationResult.ok;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
public class RouteVerifier implements RoutingVerificationService {
private final Environment environment;
private final KeyService keyService;
private final ValidationService validationService;
public RouteVerifier(Environment environment, final KeyService keyService, final ValidationService validationService) {
this.environment = environment;
this.keyService = keyService;
this.validationService = validationService;
}
@Override
public ValidationResult validateRouteDestinations(final List routes, final String requestedServiceIdentifier, final String requestedRegion) {
return routes.stream()
.map(route -> validateRoute(route, requestedServiceIdentifier, requestedRegion))
.filter(ValidationResult::hasError)
.findFirst().orElse(ValidationResult.ok());
}
private ValidationResult validateRoute(final Route route, final String requestedServiceIdentifier, final String requestedRegion) {
try {
final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature());
final JWSHeader header = signature.getHeader();
final JWTClaimsSet claims = signature.getJWTClaimsSet();
checkHeaderAlgorithm(header);
validatePayloadSchema(claims);
checkExpectedServices(claims, requestedServiceIdentifier, requestedRegion);
validateAgainstPublicKey(signature, header.getKeyID());
return ok();
} catch (final ValidationException e) {
return error(e);
} catch (final InvalidKeyException e) {
return error(new ValidationException("Public signature key is invalid: " + e.getMessage()));
} catch (final RestApiException e) {
return error(new ValidationException("Could not retrieve public signature key: " + e.getMessage()));
} catch (final ParseException | JOSEException e) {
return error(new ValidationException("Signature processing failed: " + e.getMessage()));
}
}
private void validatePayloadSchema(final JWTClaimsSet claims) {
final ValidationResult validationResult = validationService.validateDestinationSchema(claims.toJSONObject());
if (validationResult.hasError()) {
throw new ValidationException(validationResult.getError().getMessage(), validationResult.getError());
}
}
private void validateAgainstPublicKey(final SignedJWT signature, final String keyId) throws JOSEException {
final RSAKey portalPublicKey = keyService.getPortalPublicKey(keyId);
if (!signature.verify(new RSASSAVerifier(portalPublicKey))) {
throw new ValidationException("Invalid destination signature for public key id " + keyId);
}
}
private void checkExpectedServices(final JWTClaimsSet claims, final String requestedServiceIdentifier, final String requestedRegion) throws ParseException {
final List services = claims.getListClaim("services").stream()
.map(service -> new RouteService((Map>) service))
.collect(toList());
final var serviceId = getIdFromIdentifier(requestedServiceIdentifier);
if (services.stream().noneMatch(service -> service.hasMatchingService(serviceId))) {
throw new ValidationException("Requested service identifier '" + requestedServiceIdentifier + "' is not supported by any of the destinations services");
}
// check combination of service and region - ars can be null if the requested region is an areaId or ags
if (requestedRegion != null) {
final var regionId = getIdFromIdentifier(requestedRegion);
if (services.stream().noneMatch(service -> service.hasMatchingRegionAndService(regionId, serviceId))) {
throw new ValidationException("Requested region '" + requestedRegion + "' does not match any service provided by the destination");
}
}
}
private static String getIdFromIdentifier(final String identifier) {
if (isNumericId(identifier)) {
return identifier;
}
return Arrays.stream(identifier.split(":")).reduce((first, second) -> second).orElse(null);
}
private static boolean isNumericId(final String identifier) {
return Pattern.compile("\\d+").matcher(identifier).matches();
}
private void checkHeaderAlgorithm(final JWSHeader header) {
if (!header.getAlgorithm().equals(JWSAlgorithm.PS512)) {
throw new ValidationException("Algorithm in signature header is not " + JWSAlgorithm.PS512);
}
}
static class RouteService {
private final List regionIds;
private final List serviceIds;
protected RouteService(final Map> service) {
regionIds = service.getOrDefault("gebietIDs", emptyList()).stream().map(RouteVerifier::getIdFromIdentifier).collect(toList());
serviceIds = service.getOrDefault("leistungIDs", emptyList()).stream().map(RouteVerifier::getIdFromIdentifier).collect(toList());
}
public boolean hasMatchingRegionAndService(final String regionId, final String serviceId) {
return hasMatchingRegion(regionId) && hasMatchingService(serviceId);
}
public boolean hasMatchingRegion(final String regionId) {
return regionIds.stream().anyMatch(regionId::contains);
}
public boolean hasMatchingService(final String serviceId) {
return serviceIds.contains(serviceId);
}
}
}