From 4feebd40dabe45e94f73cafcec410c2e51016758 Mon Sep 17 00:00:00 2001 From: aleksandra-bel Date: Wed, 21 Feb 2024 17:07:47 +0100 Subject: [PATCH] feat: add service method, controller, config for scope matching --- .../security/PresentationIatpFilter.java | 78 ++++++++++++++++++ .../config/security/SecurityConfig.java | 10 +++ .../constant/RestURI.java | 1 + .../controller/PresentationController.java | 22 ++++- .../exception/MissingVcTypesException.java | 29 +++++++ .../PermissionViolationException.java | 29 +++++++ .../service/PresentationService.java | 69 ++++++++++++++-- .../utils/TokenParsingUtils.java | 81 +++++++++++++++++++ 8 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java create mode 100644 src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/MissingVcTypesException.java create mode 100644 src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/PermissionViolationException.java create mode 100644 src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java new file mode 100644 index 000000000..dac29fcb3 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java @@ -0,0 +1,78 @@ +/* + * ******************************************************************************* + * 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.config.security; + +import io.micrometer.common.util.StringUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors; +import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; +import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class PresentationIatpFilter extends GenericFilterBean { + + RequestMatcher customFilterUrl = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP); + + STSTokenValidationService validationService; + + public PresentationIatpFilter(STSTokenValidationService validationService) { + this.validationService = validationService; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + + if (customFilterUrl.matches(httpServletRequest)) { + String authHeader = httpServletRequest.getHeader("Authorization"); + if (StringUtils.isEmpty(authHeader)) { + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } else { + ValidationResult result = validationService.validateToken(authHeader); + if (!result.isValid()) { + List errors = result.getErrors(); + String content = Arrays.toString(errors.toArray()); + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.setContentLength(content.length()); + httpServletResponse.getWriter().write(content); + } else { + chain.doFilter(request, response); + } + } + } + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index b74541da4..6d81af27a 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -25,6 +25,8 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.constant.ApplicationRole; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,6 +38,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import static org.springframework.http.HttpMethod.GET; @@ -51,6 +54,9 @@ @AllArgsConstructor public class SecurityConfig { + @Autowired + private final STSTokenValidationService validationService; + private final SecurityConfigProperties securityConfigProperties; /** @@ -74,6 +80,7 @@ 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("/actuator/loggers/**")).hasRole(ApplicationRole.ROLE_MANAGE_APP) //did document resolve APIs @@ -110,6 +117,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() ).oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(securityConfigProperties.clientId())))); + + http.addFilterAfter(new PresentationIatpFilter(validationService), BasicAuthenticationFilter.class); + return http.build(); } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java index a52149feb..db5415337 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java @@ -77,5 +77,6 @@ private RestURI() { public static final String API_PRESENTATIONS = "/api/presentations"; public static final String API_PRESENTATIONS_VALIDATION = "/api/presentations/validation"; + public static final String API_PRESENTATIONS_IATP = "/api/presentations/iatp"; } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index fe052f5cf..d41eae0fd 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -21,10 +21,11 @@ package org.eclipse.tractusx.managedidentitywallets.controller; +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.PostVerifiablePresentationApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationValidationApiDocs; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; @@ -32,14 +33,18 @@ 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.security.Principal; import java.util.Map; +import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getAccessToken; + /** * The type Presentation controller. */ @@ -90,4 +95,19 @@ public ResponseEntity> validatePresentation(@RequestBody Map log.debug("Received request to validate presentation"); return ResponseEntity.status(HttpStatus.OK).body(presentationService.validatePresentation(data, asJwt, withCredentialExpiryDate, audience)); } + + /** + * Create presentation response entity for VC types provided in STS token. + * + * @param stsToken the STS token with required scopes + * @return the VP response entity + */ + @SneakyThrows + @GetMapping(path = RestURI.API_PRESENTATIONS_IATP, produces = { MediaType.APPLICATION_JSON_VALUE }) + // @SecureTokenControllerApiDoc.PostSecureTokenDoc TODO create API docs + public ResponseEntity> createPresentation(@RequestHeader(name = "Authorization") String stsToken) { + SignedJWT accessToken = getAccessToken(stsToken); + Map vp = presentationService.createVpWithRequiredScopes(accessToken); + return ResponseEntity.ok(vp); + } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/MissingVcTypesException.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/MissingVcTypesException.java new file mode 100644 index 000000000..c7994ffb2 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/MissingVcTypesException.java @@ -0,0 +1,29 @@ +/* + * ******************************************************************************* + * 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.exception; + +public class MissingVcTypesException extends RuntimeException { + + public MissingVcTypesException(String message) { + super(message); + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/PermissionViolationException.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/PermissionViolationException.java new file mode 100644 index 000000000..ae868e007 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/PermissionViolationException.java @@ -0,0 +1,29 @@ +/* + * ******************************************************************************* + * 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.exception; + +public class PermissionViolationException extends RuntimeException { + + public PermissionViolationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java index 75739bdaf..fa99929ae 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java @@ -21,13 +21,12 @@ package org.eclipse.tractusx.managedidentitywallets.service; -import com.ctc.wstx.shaded.msv_core.util.Uri; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import com.smartsensesolutions.java.commons.base.repository.BaseRepository; import com.smartsensesolutions.java.commons.base.service.BaseService; import com.smartsensesolutions.java.commons.specification.SpecificationUtil; -import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -37,27 +36,25 @@ import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.HoldersCredentialRepository; import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.exception.MissingVcTypesException; +import org.eclipse.tractusx.managedidentitywallets.exception.PermissionViolationException; import org.eclipse.tractusx.managedidentitywallets.utils.Validate; import org.eclipse.tractusx.ssi.lib.crypt.octet.OctetKeyPairFactory; import org.eclipse.tractusx.ssi.lib.crypt.x21559.x21559PrivateKey; -import org.eclipse.tractusx.ssi.lib.did.resolver.DidDocumentResolver; import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; import org.eclipse.tractusx.ssi.lib.exception.InvalidJsonLdException; import org.eclipse.tractusx.ssi.lib.exception.InvalidePrivateKeyFormat; import org.eclipse.tractusx.ssi.lib.exception.JwtExpiredException; -import org.eclipse.tractusx.ssi.lib.exception.UnsupportedSignatureTypeException; import org.eclipse.tractusx.ssi.lib.jwt.SignedJwtFactory; import org.eclipse.tractusx.ssi.lib.jwt.SignedJwtValidator; import org.eclipse.tractusx.ssi.lib.jwt.SignedJwtVerifier; import org.eclipse.tractusx.ssi.lib.model.did.Did; -import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.did.DidParser; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentationBuilder; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentationType; import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofValidation; -import org.eclipse.tractusx.ssi.lib.proof.SignatureType; import org.eclipse.tractusx.ssi.lib.serialization.jsonLd.JsonLdSerializerImpl; import org.eclipse.tractusx.ssi.lib.serialization.jwt.SerializedJwtPresentationFactory; import org.eclipse.tractusx.ssi.lib.serialization.jwt.SerializedJwtPresentationFactoryImpl; @@ -66,7 +63,14 @@ import org.springframework.util.StringUtils; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getClaimsSet; +import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getScope; /** * The type Presentation service. @@ -108,7 +112,6 @@ protected SpecificationUtil getSpecificationUtil() { * @param callerBpn the caller bpn * @return the map */ - @SneakyThrows({InvalidePrivateKeyFormat.class}) public Map createPresentation(Map data, boolean asJwt, String audience, String callerBpn) { List> verifiableCredentialList = (List>) data.get(StringPool.VERIFIABLE_CREDENTIALS); @@ -121,6 +124,11 @@ public Map createPresentation(Map data, boolean verifiableCredentials.add(verifiableCredential); }); + return buildVP(asJwt, audience, callerBpn, callerWallet, verifiableCredentials); + } + + @SneakyThrows({ InvalidePrivateKeyFormat.class }) + private Map buildVP(boolean asJwt, String audience, String callerBpn, Wallet callerWallet, List verifiableCredentials) { Map response = new HashMap<>(); if (asJwt) { log.debug("Creating VP as JWT for bpn ->{}", callerBpn); @@ -276,4 +284,49 @@ private boolean validateCredential(VerifiableCredential credential) { } return isValid; } + + public Map createVpWithRequiredScopes(SignedJWT innerJWT) { + List holdersCredentials = new ArrayList<>(); + List missingVCTypes = new ArrayList<>(); + List verifiableCredentials = new ArrayList<>(); + + JWTClaimsSet jwtClaimsSet = getClaimsSet(innerJWT); + String scopeValue = getScope(jwtClaimsSet); + String[] scopes = scopeValue.split(" "); + + for (String scope : scopes) { + String[] scopeParts = scope.split(":"); + String vcType = scopeParts[1]; + checkReadPermission(scopeParts[2]); + + List credentials = + holdersCredentialRepository.getByHolderDidAndType(jwtClaimsSet.getIssuer(), vcType); + if ((null == credentials) || credentials.isEmpty()) { + missingVCTypes.add(String.format("%s MISSING", vcType)); + } else { + holdersCredentials.addAll(credentials); + } + } + + checkMissingVcs(missingVCTypes); + + Wallet callerWallet = commonService.getWalletByIdentifier(jwtClaimsSet.getIssuer()); + + holdersCredentials.forEach(c -> verifiableCredentials.add(c.getData())); + + return buildVP(false, jwtClaimsSet.getAudience().get(0), callerWallet.getBpn(), + callerWallet, verifiableCredentials); + } + + private void checkReadPermission(String permission) { + if (!"read".equals(permission)) { + throw new PermissionViolationException("Scopes must have only READ permission"); + } + } + + private void checkMissingVcs(List missingVCTypes) { + if (!missingVCTypes.isEmpty()) { + throw new MissingVcTypesException(String.format("MissingVCs of types: %s", missingVCTypes)); + } + } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java new file mode 100644 index 000000000..33c0067d1 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java @@ -0,0 +1,81 @@ +/* + * ******************************************************************************* + * 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.utils; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import lombok.experimental.UtilityClass; +import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; + +import java.text.ParseException; +import java.util.Optional; + +@UtilityClass +public class TokenParsingUtils { + + public static final String ACCESS_TOKEN = "access_token"; + public static final String SCOPE = "scope"; + public static final String BEARER_ACCESS_SCOPE = "bearer_access_scope"; + + public static JWTClaimsSet getClaimsSet(SignedJWT tokenParsed) { + try { + return tokenParsed.getJWTClaimsSet(); + } catch (ParseException e) { + throw new BadDataException("Could not parse jwt token", e); + } + } + + public static SignedJWT parseToken(String token) { + try { + return SignedJWT.parse(token); + } catch (ParseException e) { + throw new BadDataException("Could not parse jwt token", e); + } + } + + public static Optional getAccessToken(JWTClaimsSet claims) { + try { + String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN); + return accessTokenValue == null ? Optional.empty() : Optional.of(accessTokenValue); + } catch (ParseException e) { + throw new BadDataException("Could not parse jwt token", e); + } + } + public static SignedJWT getAccessToken(String outerToken) { + SignedJWT jwtOuter = parseToken(outerToken); + JWTClaimsSet claimsSet = getClaimsSet(jwtOuter); + Optional accessToken = getAccessToken(claimsSet); + return accessToken.map(TokenParsingUtils::parseToken).orElse(null); + } + + public static String getScope(JWTClaimsSet jwtClaimsSet) { + try { + String scopes = jwtClaimsSet.getStringClaim(SCOPE); + if (scopes == null) { + scopes = jwtClaimsSet.getStringClaim(BEARER_ACCESS_SCOPE); + } + return scopes; + } catch (ParseException e) { + throw new BadDataException("Token does not contain scope claim"); + } + } +}