Skip to content

Commit

Permalink
feat(identity-trust): update protocol
Browse files Browse the repository at this point in the history
Signed-off-by: Dominik Pinsel <dominik.pinsel@mercedes-benz.com>
  • Loading branch information
DominikPinsel committed Jul 10, 2024
1 parent 31cbb74 commit c19c1b6
Show file tree
Hide file tree
Showing 30 changed files with 1,048 additions and 310 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import lombok.extern.slf4j.Slf4j;
import org.eclipse.tractusx.managedidentitywallets.constant.ApplicationRole;
import org.eclipse.tractusx.managedidentitywallets.constant.RestURI;
import org.eclipse.tractusx.managedidentitywallets.controller.SecureTokenController;
import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationEventPublisher;
Expand Down Expand Up @@ -80,10 +81,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers(new AntPathRequestMatcher("/docs/api-docs/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/ui/swagger-ui/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/actuator/health/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/token", POST.name())).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/presentations/iatp", GET.name())).permitAll()
.requestMatchers(new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP, POST.name())).permitAll()
.requestMatchers(new AntPathRequestMatcher("/actuator/loggers/**")).hasRole(ApplicationRole.ROLE_MANAGE_APP)

// secure token request
.requestMatchers(new AntPathRequestMatcher(SecureTokenController.BASE_PATH, POST.name())).hasAnyRole(ApplicationRole.ROLE_VIEW_WALLET)

//did document resolve APIs
.requestMatchers(new AntPathRequestMatcher(RestURI.DID_RESOLVE, GET.name())).permitAll() //Get did document
.requestMatchers(new AntPathRequestMatcher(RestURI.DID_DOCUMENTS, GET.name())).permitAll() //Get did document
Expand Down Expand Up @@ -137,7 +140,7 @@ public WebSecurityCustomizer securityCustomizer() {
*/
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
(ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ private RestURI() {
* The constant API_PRESENTATIONS.
*/
public static final String API_PRESENTATIONS = "/api/presentations";

/**
* The constant API_PRESENTATIONS_VALIDATION.
*/
public static final String API_PRESENTATIONS_VALIDATION = "/api/presentations/validation";

/**
* The constant API_PRESENTATIONS_IATP.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,28 @@
import com.nimbusds.jwt.SignedJWT;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.GetVerifiablePresentationIATPApiDocs;
import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationApiDocs;
import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationValidationApiDocs;
import org.eclipse.tractusx.managedidentitywallets.constant.RestURI;
import org.eclipse.tractusx.managedidentitywallets.dto.PresentationResponseMessage;
import org.eclipse.tractusx.managedidentitywallets.reader.TractusXPresentationRequestReader;
import org.eclipse.tractusx.managedidentitywallets.service.PresentationService;
import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.InputStream;
import java.security.Principal;
import java.util.List;
import java.util.Map;

import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getAccessToken;
Expand All @@ -55,6 +60,8 @@ public class PresentationController extends BaseController {

private final PresentationService presentationService;

private final TractusXPresentationRequestReader presentationRequestReader;

/**
* Create presentation response entity.
*
Expand Down Expand Up @@ -97,17 +104,29 @@ public ResponseEntity<Map<String, Object>> validatePresentation(@RequestBody Map
/**
* Create presentation response entity for VC types provided in STS token.
*
* @param stsToken the STS token with required scopes
* @param asJwt as JWT VP response
* @param stsToken the STS token with required scopes
* @param asJwt as JWT VP response
* @return the VP response entity
*/

@GetMapping(path = RestURI.API_PRESENTATIONS_IATP, produces = { MediaType.APPLICATION_JSON_VALUE })
@PostMapping(path = RestURI.API_PRESENTATIONS_IATP, produces = { MediaType.APPLICATION_JSON_VALUE })
@GetVerifiablePresentationIATPApiDocs
public ResponseEntity<Map<String, Object>> createPresentation(@Parameter(hidden = true) @RequestHeader(name = "Authorization") String stsToken,
@RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt) {
SignedJWT accessToken = getAccessToken(stsToken);
Map<String, Object> vp = presentationService.createVpWithRequiredScopes(accessToken, asJwt);
return ResponseEntity.ok(vp);
@SneakyThrows
public ResponseEntity<PresentationResponseMessage> createPresentation(@Parameter(hidden = true) @RequestHeader(name = "Authorization") String stsToken,
@RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt,
InputStream is) {
try {

final List<String> requestedScopes = presentationRequestReader.readVerifiableCredentialScopes(is);
// requested scopes are ignored until the documentation is better refined

SignedJWT accessToken = getAccessToken(stsToken);
Map<String, Object> map = presentationService.createVpWithRequiredScopes(accessToken, asJwt);
VerifiablePresentation verifiablePresentation = new VerifiablePresentation(map);
PresentationResponseMessage message = new PresentationResponseMessage(verifiablePresentation);
return ResponseEntity.ok(message);
} catch (TractusXPresentationRequestReader.InvalidPresentationQueryMessageResource e) {
return ResponseEntity.badRequest().build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,94 +21,90 @@

package org.eclipse.tractusx.managedidentitywallets.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.coyote.BadRequestException;
import org.eclipse.tractusx.managedidentitywallets.apidocs.SecureTokenControllerApiDoc;
import org.eclipse.tractusx.managedidentitywallets.constant.StringPool;
import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet;
import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository;
import org.eclipse.tractusx.managedidentitywallets.domain.BusinessPartnerNumber;
import org.eclipse.tractusx.managedidentitywallets.domain.DID;
import org.eclipse.tractusx.managedidentitywallets.domain.IdpTokenResponse;
import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType;
import org.eclipse.tractusx.managedidentitywallets.domain.StsTokenErrorResponse;
import org.eclipse.tractusx.managedidentitywallets.domain.StsTokenResponse;
import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest;
import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequestScope;
import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequestToken;
import org.eclipse.tractusx.managedidentitywallets.exception.InvalidIdpTokenResponseException;
import org.eclipse.tractusx.managedidentitywallets.exception.InvalidSecureTokenRequestException;
import org.eclipse.tractusx.managedidentitywallets.exception.UnknownBusinessPartnerNumberException;
import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedGrantTypeException;
import org.eclipse.tractusx.managedidentitywallets.service.IdpAuthorization;
import org.eclipse.tractusx.managedidentitywallets.signing.SigningService;
import org.eclipse.tractusx.managedidentitywallets.validator.SecureTokenRequestValidator;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
import java.text.ParseException;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import static org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils.getSecureTokenRequest;

import java.util.stream.Collectors;

@RestController
@Slf4j
@RequiredArgsConstructor
@Tag(name = "STS")
public class SecureTokenController {
public class SecureTokenController extends BaseController {

public static final String BASE_PATH = "/api/token";

private final IdpAuthorization idpAuthorization;

private final WalletRepository walletRepo;

private final Map<SigningServiceType, SigningService> availableSigningServices;

@InitBinder
void initBinder(WebDataBinder webDataBinder) {
webDataBinder.addValidators(new SecureTokenRequestValidator());
}


@SneakyThrows
@PostMapping(path = "/api/token", consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
@PostMapping(path = BASE_PATH, consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
@SecureTokenControllerApiDoc.PostSecureTokenDocJson
public ResponseEntity<StsTokenResponse> tokenJson(
@Valid @RequestBody SecureTokenRequest secureTokenRequest
@RequestBody String data,
Principal principal
) {
return processTokenRequest(secureTokenRequest);
var request = new ObjectMapper().readValue(data, SecureTokenRequest.class);
return processRequest(request, principal);
}

@SneakyThrows
@PostMapping(path = "/api/token", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
@SecureTokenControllerApiDoc.PostSecureTokenDocFormUrlencoded
public ResponseEntity<StsTokenResponse> tokenFormUrlencoded(
@Valid @RequestBody MultiValueMap<String, String> requestParameters
) {
final SecureTokenRequest secureTokenRequest = getSecureTokenRequest(requestParameters);
return processTokenRequest(secureTokenRequest);
private ResponseEntity<StsTokenResponse> processRequest(SecureTokenRequest secureTokenRequest,
Principal principal) throws ParseException, BadRequestException {

var bpn = getBPNFromToken(principal);

/* If Token is Present */
if (secureTokenRequest.getSecureTokenRequestToken().isPresent()) {
return processWithToken(secureTokenRequest.getSecureTokenRequestToken().get(), bpn);
/* Else If Scope is Present */
} else if (secureTokenRequest.getSecureTokenRequestScope().isPresent()) {
return processWithScope(secureTokenRequest.getSecureTokenRequestScope().get(), bpn);
/* Else Throw */
} else {
throw new BadRequestException("The provided data could not be used to create and sign a token.");
}
}

private ResponseEntity<StsTokenResponse> processTokenRequest(SecureTokenRequest secureTokenRequest) throws ParseException {
// handle idp authorization
IdpTokenResponse idpResponse = idpAuthorization.fromSecureTokenRequest(secureTokenRequest);
BusinessPartnerNumber bpn = idpResponse.bpn();
Wallet selfWallet = walletRepo.getByBpn(bpn.toString());

private ResponseEntity<StsTokenResponse> processWithToken(SecureTokenRequestToken secureTokenRequest, String bpn) throws ParseException {
Wallet selfWallet = walletRepo.getByBpn(bpn);
DID selfDid = new DID(selfWallet.getDid());
DID partnerDid;
if (Pattern.compile(StringPool.BPN_NUMBER_REGEX).matcher(secureTokenRequest.getAudience()).matches()) {
Expand All @@ -124,29 +120,63 @@ private ResponseEntity<StsTokenResponse> processTokenRequest(SecureTokenRequest

// create the SI token and put/create the access_token inside
JWT responseJwt;
if (secureTokenRequest.assertValidWithAccessToken()) {
log.debug("Signing si token.");
responseJwt = signingService.issueToken(
selfDid,
partnerDid,
JWTParser.parse(secureTokenRequest.getAccessToken())
);
} else if (secureTokenRequest.assertValidWithScopes()) {
log.debug("Creating access token and signing si token.");
responseJwt = signingService.issueToken(
selfDid,
partnerDid,
Set.of(secureTokenRequest.getBearerAccessScope())
);
log.debug("Signing si token.");
responseJwt = signingService.issueToken(
selfDid,
partnerDid,
JWTParser.parse(secureTokenRequest.getToken())
);

// create the response
log.debug("Preparing StsTokenResponse.");
StsTokenResponse response = StsTokenResponse.builder()
.token(responseJwt.serialize())
.build();

return ResponseEntity.status(HttpStatus.OK).body(response);
}

private ResponseEntity<StsTokenResponse> processWithScope(SecureTokenRequestScope secureTokenRequest, String bpn) {
Wallet selfWallet = walletRepo.getByBpn(bpn);
DID selfDid = new DID(selfWallet.getDid());
DID partnerDid;
if (Pattern.compile(StringPool.BPN_NUMBER_REGEX).matcher(secureTokenRequest.getConsumerDid()).matches()) {
partnerDid = new DID(walletRepo.getByBpn(secureTokenRequest.getConsumerDid()).getDid());
} else if (StringUtils.startsWith(secureTokenRequest.getConsumerDid(), "did:")) {
partnerDid = new DID(secureTokenRequest.getConsumerDid());
} else {
throw new InvalidSecureTokenRequestException("The provided data could not be used to create and sign a token.");
throw new InvalidSecureTokenRequestException("You must provide an audience either as a BPN or DID.");
}

SigningServiceType signingServiceType = selfWallet.getSigningServiceType();
SigningService signingService = availableSigningServices.get(signingServiceType);

// create the SI token and put/create the access_token inside
JWT responseJwt;
log.debug("Creating access token and signing si token.");

var scope = secureTokenRequest.getScope();
if (!scope.equalsIgnoreCase("read")) {
throw new UnsupportedOperationException("Only read scope is supported.");
}

var scopes = secureTokenRequest.getCredentialTypes()
.stream()
// Why this strange scopes? Doesn't make sense, but done as defined here
// https://github.com/eclipse-tractusx/identity-trust/blob/main/specifications/verifiable.presentation.protocol.md#3-security
.map("hereCouldBeYourText:%s:read"::formatted)
.collect(Collectors.toSet());

responseJwt = signingService.issueToken(
selfDid,
partnerDid,
scopes
);

// create the response
log.debug("Preparing StsTokenResponse.");
StsTokenResponse response = StsTokenResponse.builder()
.token(responseJwt.serialize())
.expiresAt(responseJwt.getJWTClaimsSet().getExpirationTime().getTime())
.build();
return ResponseEntity.status(HttpStatus.OK).body(response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@
@AllArgsConstructor
public class StsTokenResponse {

@JsonProperty("access_token")
@JsonProperty("jwt")
private String token;

private long expiresAt;
}
Loading

0 comments on commit c19c1b6

Please sign in to comment.