Skip to content

Commit

Permalink
feat(identity-trust)!: update IATP protocol
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `/api/presentations/iatp` endpoint now accepts PresentationQueryMessage and returns PresentationResponseMessage objects.

Signed-off-by: Dominik Pinsel <[email protected]>
  • Loading branch information
DominikPinsel committed Jul 18, 2024
1 parent 9b3381d commit e3c5450
Show file tree
Hide file tree
Showing 14 changed files with 798 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@

public class PresentationIatpFilter extends GenericFilterBean {

RequestMatcher customFilterUrl = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP);
RequestMatcher customFilterUrl1 = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP);
RequestMatcher customFilterUrl2 = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP_WORKAROUND);

STSTokenValidationService validationService;

Expand All @@ -57,7 +58,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;

if (customFilterUrl.matches(httpServletRequest)) {
if (customFilterUrl1.matches(httpServletRequest) || customFilterUrl2.matches(httpServletRequest)) {
String authHeader = httpServletRequest.getHeader("Authorization");
if (StringUtils.isEmpty(authHeader)) {
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.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(RestURI.API_PRESENTATIONS_IATP_WORKAROUND, POST.name())).permitAll()
.requestMatchers(new AntPathRequestMatcher("/actuator/loggers/**")).hasRole(ApplicationRole.ROLE_MANAGE_APP)

//did document resolve APIs
Expand Down Expand Up @@ -137,7 +138,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 @@ -80,4 +80,10 @@ private RestURI() {
*/
public static final String API_PRESENTATIONS_IATP = "/api/presentations/iatp";

/**
* The constant API_PRESENTATIONS_IATP_WORKAROUND. THe EDC assumes (hard coded) that the presentation query endpoint is at /presentations/query.
* To mitigate this issue the MIW has to provide the same endpoint (without documentation), besides the correct one.
*/
public static final String API_PRESENTATIONS_IATP_WORKAROUND = "/presentations/query";

}
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,30 @@ 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, RestURI.API_PRESENTATIONS_IATP_WORKAROUND }, 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)map.get("vp"));
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
@@ -0,0 +1,58 @@
/*
* *******************************************************************************
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* ******************************************************************************
*/

package org.eclipse.tractusx.managedidentitywallets.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation;

import java.util.List;

/**
* Class to represent the response message of a presentation request.
* Defined in JsonLD Tractus-X <a href="https://github.com/eclipse-tractusx/identity-trust/blob/main/specifications/context.json">context.json</a>.
* <p>
* As `presentationSubmission` a not well-defined, we will just skip the property for HTTP responses. Defining all types as 'Json' make the whole idea of using Json-Linked-Data a waste of time, but ok.
* <p>
* The `presentation` property is only specified as 'Json'. For this implementation we will assume these are Presentations from ether the <a href="https://www.w3.org/2018/credentials/v1">Verifiable Credential Data Model v1.1</a> or <a href="https://www.w3.org/ns/credentials/v2">Verifiable Credential Data Model v2.0</a>.
*/
@Getter
public class PresentationResponseMessage {


public PresentationResponseMessage(VerifiablePresentation verifiablePresentation) {
this(List.of(verifiablePresentation));
}

public PresentationResponseMessage(List<VerifiablePresentation> verifiablePresentations) {
this.verifiablePresentations = verifiablePresentations;
}

@JsonProperty("@context")
private List<String> contexts = List.of("https://w3id.org/tractusx-trust/v0.8");

@JsonProperty("@type")
private List<String> types = List.of("PresentationResponseMessage");

@JsonProperty("presentation")
private List<VerifiablePresentation> verifiablePresentations;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* *******************************************************************************
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* ******************************************************************************
*/

package org.eclipse.tractusx.managedidentitywallets.reader;

import com.apicatalog.jsonld.JsonLdError;
import com.apicatalog.jsonld.JsonLdOptions;
import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.processor.ExpansionProcessor;
import jakarta.json.JsonArray;
import lombok.NonNull;
import org.eclipse.tractusx.managedidentitywallets.utils.ResourceUtil;
import org.eclipse.tractusx.ssi.lib.model.RemoteDocumentLoader;

import java.io.InputStream;
import java.net.URI;
import java.util.Map;

public class TractusXJsonLdReader {

private static final String TRACTUS_X_CONTEXT_RESOURCE = "jsonld/IdentityMinusTrust.json";
private static final URI TRACTUS_X_CONTEXT = URI.create("https://w3id.org/tractusx-trust/v0.8");
private static final URI IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_CONTEXT = URI.create("https://identity.foundation/presentation-exchange/submission/v1");
private static final String IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_RESOURCE = "jsonld/identity.foundation.presentation-exchange.submission.v1.json";


private final RemoteDocumentLoader documentLoader = RemoteDocumentLoader.DOCUMENT_LOADER;

public TractusXJsonLdReader() {

documentLoader.setEnableLocalCache(true);

if (!documentLoader.getLocalCache().containsKey(TRACTUS_X_CONTEXT)) {
cacheOfflineResource(TRACTUS_X_CONTEXT_RESOURCE, TRACTUS_X_CONTEXT);
}
if (!documentLoader.getLocalCache().containsKey(IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_CONTEXT)) {
cacheOfflineResource(IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_RESOURCE, IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_CONTEXT);
}
}

public JsonArray expand(@NonNull final InputStream documentStream) throws JsonLdError {

final JsonLdOptions jsonLdOptions = new JsonLdOptions();
jsonLdOptions.setDocumentLoader(documentLoader);

final JsonDocument document = JsonDocument.of(com.apicatalog.jsonld.http.media.MediaType.JSON_LD, documentStream);
return ExpansionProcessor.expand(document, jsonLdOptions, false);
}

private void cacheOfflineResource(final String resource, final URI context) {
try {
final InputStream resourceStream = ResourceUtil.getResourceStream(resource);
final JsonDocument identityMinusTrustDocument;
identityMinusTrustDocument = JsonDocument.of(com.apicatalog.jsonld.http.media.MediaType.JSON_LD, resourceStream);
documentLoader.getLocalCache().put(context, identityMinusTrustDocument);
} catch (JsonLdError e) {
// If this ever fails, it is a programming error. Loading of the embedded context resource is checked by Unit Tests.
throw new RuntimeException("Could not parse Tractus-X JsonL-d context from resource. This should never happen. Resource: '%s'".formatted(resource), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* *******************************************************************************
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* ******************************************************************************
*/

package org.eclipse.tractusx.managedidentitywallets.reader;

import com.apicatalog.jsonld.JsonLdError;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.InputStream;
import java.util.List;

@Slf4j
@Component
public class TractusXPresentationRequestReader extends TractusXJsonLdReader {

private static final String JSON_LD_TYPE = "@type";
private static final String JSON_LD_VALUE = "@value";
private static final String TRACTUS_X_PRESENTATION_QUERY_MESSAGE_TYPE = "https://w3id.org/tractusx-trust/v0.8/PresentationQueryMessage";
private static final String TRACTUS_X_SCOPE_TYPE = "https://w3id.org/tractusx-trust/v0.8/scope";

public List<String> readVerifiableCredentialScopes(InputStream is) throws InvalidPresentationQueryMessageResource {
try {

final JsonArray jsonArray = expand(is);

if (jsonArray.size() != 1) {
log.atDebug().addArgument(jsonArray::toString).log("Expanded JSON-LD: {}");
throw new InvalidPresentationQueryMessageResource("Expected a single JSON object. Found %d".formatted(jsonArray.size()));
}

var jsonObject = jsonArray.getJsonObject(0);

final JsonArray typeArray = jsonObject.getJsonArray(JSON_LD_TYPE);
final List<String> types = typeArray.getValuesAs(JsonString.class).stream().map(JsonString::getString).toList();
if (!types.contains(TRACTUS_X_PRESENTATION_QUERY_MESSAGE_TYPE)) {
log.atDebug().addArgument(jsonArray::toString).log("Expanded JSON-LD: {}");
throw new InvalidPresentationQueryMessageResource("Unexpected type. Expected %s".formatted(TRACTUS_X_PRESENTATION_QUERY_MESSAGE_TYPE));
}

final JsonArray scopes = jsonObject.getJsonArray(TRACTUS_X_SCOPE_TYPE);
return scopes.getValuesAs(JsonObject.class)
.stream()
.map(o -> o.getJsonString(JSON_LD_VALUE))
.map(JsonString::getString)
.toList();

} catch (JsonLdError e) {
throw new InvalidPresentationQueryMessageResource(e);
}
}

public static class InvalidPresentationQueryMessageResource extends Exception {
public InvalidPresentationQueryMessageResource(String message) {
super(message);
}

public InvalidPresentationQueryMessageResource(Throwable cause) {
super(cause);
}
}

}

Loading

0 comments on commit e3c5450

Please sign in to comment.