Skip to content

Commit

Permalink
update callback endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
epicsoft-llc committed Sep 30, 2021
1 parent 8a95eb6 commit a68cf22
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@
package eu.europa.ec.dgc.validation.decorator.controller;

import eu.europa.ec.dgc.validation.decorator.dto.CallbackRequest;
import eu.europa.ec.dgc.validation.decorator.service.AccessTokenService;
import eu.europa.ec.dgc.validation.decorator.service.BackendService;
import io.jsonwebtoken.JwtException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.util.Map;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -47,44 +46,36 @@ public class CallbackController {

private static final String PATH = "/callback/{subject}";

private final AccessTokenService accessTokenService;

private final BackendService backendService;

/**
* Callback endpoint receives the validation result to a subject.
*
* @param subject Subject
*/
@Operation(summary = "The optional callback endpoint receives the validation result to a subject",
description = "The optional callback endpoint receives the validation result to a subject")
@Operation(summary = "The optional callback endpoint receives the validation result to a subject", description = "The optional callback endpoint receives the validation result to a subject")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "401", description = "Unauthorized, if Result Token was not correctly signed"),
@ApiResponse(responseCode = "404", description = "Not Found"),
@ApiResponse(responseCode = "410", description = "Gone. Subject does not exist anymore"),
@ApiResponse(responseCode = "500", description = "Internal Server Error")
})
@PutMapping(value = PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
@PutMapping(value = PATH, consumes = { "application/jwt", MediaType.TEXT_PLAIN_VALUE })
public ResponseEntity callback(
@PathVariable(value = "subject", required = true) final String subject,
@RequestHeader("Authorization") final String token,
@RequestHeader("X-Version") final String version,
@Valid @RequestBody final CallbackRequest request) {
log.debug("Incoming PUT request to '{}' with subject '{}'", PATH, subject);

if (this.accessTokenService.isValid(token)) {
final Map<String, Object> tokenContent = this.accessTokenService.parseAccessToken(token);
if (tokenContent.containsKey("sub") && tokenContent.get("sub") instanceof String) {

this.backendService.saveResult(subject, request);
return ResponseEntity.ok()
.cacheControl(CacheControl.noCache())
.build();
}
@RequestHeader("X-Version") final String version,
@Valid @RequestBody final String body) {
log.debug("Incoming PUT request to '{}' with subject '{}' and bldy '{}'", PATH, subject, body);
try {
final CallbackRequest request = this.backendService.parseRequest(subject, body);
this.backendService.saveResult(subject, request);
return ResponseEntity.ok()
.cacheControl(CacheControl.noCache())
.build();
} catch (JwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.cacheControl(CacheControl.noCache())
.build();
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.cacheControl(CacheControl.noCache())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@

package eu.europa.ec.dgc.validation.decorator.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import lombok.Data;

@Data
public class CallbackRequest {

@JsonProperty("iss")
private String issuer;

private Long iat;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ public static final class ResultRequest {
private String type;

private String details;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@

package eu.europa.ec.dgc.validation.decorator.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import eu.europa.ec.dgc.validation.decorator.config.DgcProperties.ServiceProperties;
import eu.europa.ec.dgc.validation.decorator.dto.CallbackRequest;
import eu.europa.ec.dgc.validation.decorator.entity.KeyUse;
import eu.europa.ec.dgc.validation.decorator.entity.ServiceResultRequest;
import eu.europa.ec.dgc.validation.decorator.entity.ValidationServiceIdentityResponse;
import eu.europa.ec.dgc.validation.decorator.entity.ValidationServiceIdentityResponse.PublicKeyJwk;
import eu.europa.ec.dgc.validation.decorator.exception.NotFoundException;
import eu.europa.ec.dgc.validation.decorator.exception.UncheckedCertificateException;
import eu.europa.ec.dgc.validation.decorator.repository.BackendRepository;
import eu.europa.ec.dgc.validation.decorator.repository.ValidationServiceRepository;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Base64;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Service;
Expand All @@ -35,6 +53,27 @@ public class BackendService {

private final ConversionService converter;

private final AccessTokenService accessTokenService;

private final ValidationServiceRepository validationServiceRepository;

private final ObjectMapper mapper;

private final SubjectService subjectService;

/**
* Reads the content from the JWT and converts it into {@link CallbackRequest}.
*
* @param subject Subject ID
* @param body JWT
* @return {@link CallbackRequest}
*/
public CallbackRequest parseRequest(String subject, String body) {
final ServiceProperties service = this.subjectService.getServiceBySubject(subject);
final Map<String, Object> jwtContent = this.getJwtContent(service, body);
return this.mapper.convertValue(jwtContent, CallbackRequest.class);
}

/**
* Save result in booking service.
*
Expand All @@ -45,4 +84,40 @@ public void saveResult(final String subject, final CallbackRequest request) {
final ServiceResultRequest resultRequest = this.converter.convert(request, ServiceResultRequest.class);
this.backendRepository.result(subject, resultRequest);
}

private Map<String, Object> getJwtContent(final ServiceProperties service, final String token) {
final Jwt jwt = this.accessTokenService.parseUnsecure(token);
final Header jwtHeaders = jwt.getHeader();
if (jwtHeaders.containsKey("kid") && jwtHeaders.get("kid") instanceof String) {
final String keyId = (String) jwtHeaders.get("kid");

final PublicKey vsPublicKey = this.getSignPublicKey(service, keyId);
return this.accessTokenService.parseAccessToken(token, vsPublicKey);
} else {
throw new NotFoundException("Status JWT has no key ID");
}
}

private PublicKey getSignPublicKey(final ServiceProperties service, final String keyId) {
final ValidationServiceIdentityResponse identity = this.validationServiceRepository.identity(service);
return identity.getVerificationMethod().stream()
.filter(vm -> vm.getPublicKeyJwk() != null)
.filter(vm -> KeyUse.SIG.name().equalsIgnoreCase(vm.getPublicKeyJwk().getUse()))
.filter(vm -> keyId.equalsIgnoreCase(vm.getPublicKeyJwk().getKid()))
.map(vm -> this.toPublicKey(vm.getPublicKeyJwk()))
.findFirst()
.orElseThrow(() -> new NotFoundException(
String.format("Validation service method with ID '%s' not found", keyId)));
}

private PublicKey toPublicKey(final PublicKeyJwk publicKeyJwk) {
final byte[] encoded = Base64.getDecoder().decode(publicKeyJwk.getX5c());
try (ByteArrayInputStream encStream = new ByteArrayInputStream(encoded)) {
return CertificateFactory.getInstance("X.509").generateCertificate(encStream).getPublicKey();
} catch (CertificateException e) {
throw new UncheckedCertificateException(e);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package eu.europa.ec.dgc.validation.decorator.service;

import eu.europa.ec.dgc.validation.decorator.config.DgcProperties.ServiceProperties;
import eu.europa.ec.dgc.validation.decorator.entity.ServiceTokenContentResponse;
import eu.europa.ec.dgc.validation.decorator.entity.ServiceTokenContentResponse.SubjectResponse;
import eu.europa.ec.dgc.validation.decorator.exception.DccException;
import eu.europa.ec.dgc.validation.decorator.exception.RepositoryException;
import eu.europa.ec.dgc.validation.decorator.repository.BackendRepository;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;

@Slf4j
@Service
@RequiredArgsConstructor
public class SubjectService {

private final IdentityService identityService;

private final BackendRepository backendRepository;

public ServiceProperties getServiceBySubject(String subject) {
final ServiceTokenContentResponse tokenContent = this.getBackendTokenContent(subject);
if (tokenContent != null && tokenContent.getSubjects() == null || tokenContent.getSubjects().isEmpty()) {
throw new DccException("Subject not found in token", HttpStatus.NO_CONTENT.value());
}

final SubjectResponse subjectResponse = tokenContent.getSubjects().get(0);
final String serviceId = subjectResponse.getServiceIdUsed();
log.debug("Receive service ID (encoded) from booking service '{}'", serviceId);
if (serviceId == null || serviceId.isBlank()) {
throw new DccException(String.format("Subject without service ID '%s'", serviceId),
HttpStatus.NO_CONTENT.value());
}

final String decodedServiceId = new String(Base64.getUrlDecoder().decode(serviceId), StandardCharsets.UTF_8);
log.debug("Receive service ID (decoded) from booking service '{}'", decodedServiceId);

final ServiceProperties service = this.identityService.getServicePropertiesById(decodedServiceId);
log.debug("Receive service: {}", service);
return service;
}

private ServiceTokenContentResponse getBackendTokenContent(final String subject) {
try {
return this.backendRepository.tokenContent(subject);
} catch (HttpClientErrorException e) {
log.error(e.getMessage(), e);
throw new RepositoryException("Backend service http client error", e);
}
}
}

0 comments on commit a68cf22

Please sign in to comment.