From 15425beccd8bbb3560328d7d845766f422e6e4d8 Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Tue, 23 Jul 2024 12:03:34 +0530 Subject: [PATCH] feat: remove BaseController, change Principal to Authenticationand unit test cases added --- .../CustomAuthenticationEntryPoint.java | 103 ++++++++++++++++ .../config/security/SecurityConfig.java | 31 ++++- .../constant/StringPool.java | 1 + .../controller/DidDocumentController.java | 2 +- .../HoldersCredentialController.java | 23 ++-- .../IssuersCredentialController.java | 21 ++-- .../controller/PresentationController.java | 19 +-- .../controller/WalletController.java | 23 ++-- .../BpnValidator.java} | 35 +++--- .../utils/TokenParsingUtils.java | 15 +++ .../CustomAuthenticationEntryPointTest.java | 114 ++++++++++++++++++ .../utils/BpnValidatorTest.java | 89 ++++++++++++++ 12 files changed, 412 insertions(+), 64 deletions(-) create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java rename miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/{controller/BaseController.java => utils/BpnValidator.java} (58%) create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 000000000..6764d63f2 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,103 @@ +/* + * ******************************************************************************* + * 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 jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Setter; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The type Custom authentication entry point. + */ +@Setter +public final class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private String realmName; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + HttpStatus status = HttpStatus.UNAUTHORIZED; + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + parameters.put("error", error.getErrorCode()); + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError bearerTokenError) { + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + + status = ((BearerTokenError) error).getHttpStatus(); + } + } + + if (authException.getMessage().contains(StringPool.BPN_NOT_FOUND)) { + status = HttpStatus.FORBIDDEN; + } + + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(status.value()); + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + StringBuilder wwwAuthenticate = new StringBuilder(); + wwwAuthenticate.append("Bearer"); + if (!parameters.isEmpty()) { + wwwAuthenticate.append(" "); + int i = 0; + for (Map.Entry entry : parameters.entrySet()) { + wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\""); + if (i != parameters.size() - 1) { + wwwAuthenticate.append(", "); + } + i++; + } + } + return wwwAuthenticate.toString(); + } + +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index 148f18759..937060e10 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -21,11 +21,13 @@ package org.eclipse.tractusx.managedidentitywallets.config.security; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; 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.eclipse.tractusx.managedidentitywallets.utils.BpnValidator; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -39,6 +41,12 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoders; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -53,13 +61,16 @@ @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) @Configuration -@AllArgsConstructor +@RequiredArgsConstructor public class SecurityConfig { private final STSTokenValidationService validationService; private final SecurityConfigProperties securityConfigProperties; + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + /** * Filter chain security filter chain. * @@ -115,7 +126,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //error .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() ).oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> - jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(securityConfigProperties.clientId())))) + jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(securityConfigProperties.clientId()))) + .authenticationEntryPoint(new CustomAuthenticationEntryPoint())) .addFilterAfter(new PresentationIatpFilter(validationService), BasicAuthenticationFilter.class); return http.build(); @@ -141,4 +153,17 @@ public WebSecurityCustomizer securityCustomizer() { (ApplicationEventPublisher applicationEventPublisher) { return new DefaultAuthenticationEventPublisher(applicationEventPublisher); } + + @Bean + JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri); + OAuth2TokenValidator bpnValidator = bpnValidator(); + OAuth2TokenValidator withBpn = new DelegatingOAuth2TokenValidator<>(bpnValidator); + jwtDecoder.setJwtValidator(withBpn); + return jwtDecoder; + } + + OAuth2TokenValidator bpnValidator() { + return new BpnValidator(); + } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java index c14c75303..f63ae0816 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java @@ -101,4 +101,5 @@ private StringPool() { public static final String SECURITY_TOKEN_SERVICE = "SecurityTokenService"; public static final String CREDENTIAL_SERVICE = "CredentialService"; public static final String HTTPS_SCHEME = "https://"; + public static final String BPN_NOT_FOUND = "BPN not found"; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java index 6869d4532..298fdc102 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java @@ -45,7 +45,7 @@ @RequiredArgsConstructor @Tag(name = "DIDDocument") @Slf4j -public class DidDocumentController extends BaseController { +public class DidDocumentController { private final DidDocumentService service; /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java index 9d55124df..e037506f8 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java @@ -37,17 +37,18 @@ import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.HoldersCredentialService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -58,7 +59,7 @@ @RequiredArgsConstructor @Slf4j @Tag(name = "Verifiable Credential - Holder") -public class HoldersCredentialController extends BaseController { +public class HoldersCredentialController { private final HoldersCredentialService holdersCredentialService; @@ -71,7 +72,7 @@ public class HoldersCredentialController extends BaseController { * @param type the type * @param sortColumn the sort column * @param sortTpe the sort tpe - * @param principal the principal + * @param authentication the authentication * @return the credentials */ @GetCredentialsApiDocs @@ -94,8 +95,8 @@ public ResponseEntity> getCredentials(@Parameter(n @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt, - Principal principal) { - log.debug("Received request to get credentials. BPN: {}", getBPNFromToken(principal)); + Authentication authentication) { + log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); final GetCredentialsCommand command; command = GetCredentialsCommand.builder() .credentialId(credentialId) @@ -106,7 +107,7 @@ public ResponseEntity> getCredentials(@Parameter(n .pageNumber(pageNumber) .size(size) .asJwt(asJwt) - .callerBPN(getBPNFromToken(principal)) + .callerBPN(TokenParsingUtils.getBPNFromToken(authentication)) .build(); return ResponseEntity.status(HttpStatus.OK).body(holdersCredentialService.getCredentials(command)); } @@ -115,17 +116,17 @@ public ResponseEntity> getCredentials(@Parameter(n /** * Issue credential response entity. * - * @param data the data - * @param principal the principal + * @param data the data + * @param authentication the authentication * @return the response entity */ @IssueCredentialApiDoc @PostMapping(path = RestURI.CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity issueCredential(@RequestBody Map data, Principal principal, + public ResponseEntity issueCredential(@RequestBody Map data, Authentication authentication, @AsJwtParam @RequestParam(name = "asJwt", defaultValue = "false") boolean asJwt ) { - log.debug("Received request to issue credential. BPN: {}", getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, getBPNFromToken(principal), asJwt)); + log.debug("Received request to issue credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, TokenParsingUtils.getBPNFromToken(authentication), asJwt)); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java index 5d3ca437f..9ade312f6 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java @@ -37,17 +37,18 @@ import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -57,7 +58,7 @@ @RestController @RequiredArgsConstructor @Slf4j -public class IssuersCredentialController extends BaseController { +public class IssuersCredentialController { /** * The constant API_TAG_VERIFIABLE_CREDENTIAL_ISSUER. @@ -81,7 +82,7 @@ public class IssuersCredentialController extends BaseController { * @param size the size * @param sortColumn the sort column * @param sortTpe the sort tpe - * @param principal the principal + * @param authentication the authentication * @return the credentials */ @GetCredentialsApiDocs @@ -101,8 +102,8 @@ public ResponseEntity> getCredentials(@Parameter(n ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, @Parameter(name = "sortTpe", description = "Sort order", examples = { @ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order") }) @RequestParam(required = false, defaultValue = "desc") String sortTpe, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt, - Principal principal) { - log.debug("Received request to get credentials. BPN: {}", getBPNFromToken(principal)); + Authentication authentication) { + log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); final GetCredentialsCommand command; command = GetCredentialsCommand.builder() .credentialId(credentialId) @@ -113,7 +114,7 @@ public ResponseEntity> getCredentials(@Parameter(n .pageNumber(pageNumber) .size(size) .asJwt(asJwt) - .callerBPN(getBPNFromToken(principal)) + .callerBPN(TokenParsingUtils.getBPNFromToken(authentication)) .build(); return ResponseEntity.status(HttpStatus.OK).body(issuersCredentialService.getCredentials(command)); } @@ -139,14 +140,14 @@ public ResponseEntity> credentialsValidation(@RequestBody Cr * * @param holderDid the holder did * @param data the data - * @param principal the principal + * @param authentication the authentication * @return the response entity */ @PostMapping(path = RestURI.ISSUERS_CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @IssueVerifiableCredentialUsingBaseWalletApiDocs - public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = {@ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Principal principal, + public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = {@ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Authentication authentication, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt) { - log.debug("Received request to issue verifiable credential. BPN: {}", getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, getBPNFromToken(principal))); + log.debug("Received request to issue verifiable credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, TokenParsingUtils.getBPNFromToken(authentication))); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index 3f5b5a0c0..315678228 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -33,10 +33,12 @@ 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.managedidentitywallets.utils.TokenParsingUtils; 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.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -44,7 +46,6 @@ import org.springframework.web.bind.annotation.RestController; import java.io.InputStream; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -56,7 +57,7 @@ @RestController @RequiredArgsConstructor @Slf4j -public class PresentationController extends BaseController { +public class PresentationController { private final PresentationService presentationService; @@ -65,20 +66,20 @@ public class PresentationController extends BaseController { /** * Create presentation response entity. * - * @param data the data - * @param audience the audience - * @param asJwt the as jwt - * @param principal the principal + * @param data the data + * @param audience the audience + * @param asJwt the as jwt + * @param authentication the authentication * @return the response entity */ @PostVerifiablePresentationApiDocs @PostMapping(path = RestURI.API_PRESENTATIONS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> createPresentation(@RequestBody Map data, @RequestParam(name = "audience", required = false) String audience, - @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, Principal principal + @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, Authentication authentication ) { - log.debug("Received request to create presentation. BPN: {}", getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(presentationService.createPresentation(data, asJwt, audience, getBPNFromToken(principal))); + log.debug("Received request to create presentation. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(presentationService.createPresentation(data, asJwt, audience, TokenParsingUtils.getBPNFromToken(authentication))); } /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java index fb41bdf03..d38e353e2 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java @@ -39,13 +39,14 @@ import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; import org.eclipse.tractusx.managedidentitywallets.service.WalletService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import java.security.Principal; import java.util.Map; /** @@ -55,7 +56,7 @@ @RequiredArgsConstructor @Slf4j @Tag(name = "Wallets") -public class WalletController extends BaseController { +public class WalletController { private final WalletService service; @@ -67,9 +68,9 @@ public class WalletController extends BaseController { */ @CreateWalletApiDoc @PostMapping(path = RestURI.WALLETS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWallet(@Valid @RequestBody CreateWalletRequest request, Principal principal) { - log.debug("Received request to create wallet with BPN {}. authorized by BPN: {}", request.getBusinessPartnerNumber(), getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(service.createWallet(request, getBPNFromToken(principal))); + public ResponseEntity createWallet(@Valid @RequestBody CreateWalletRequest request, Authentication authentication) { + log.debug("Received request to create wallet with BPN {}. authorized by BPN: {}", request.getBusinessPartnerNumber(), TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(service.createWallet(request, TokenParsingUtils.getBPNFromToken(authentication))); } /** @@ -82,9 +83,9 @@ public ResponseEntity createWallet(@Valid @RequestBody CreateWalletReque @StoreVerifiableCredentialApiDoc @PostMapping(path = RestURI.API_WALLETS_IDENTIFIER_CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> storeCredential(@RequestBody Map data, - @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, Principal principal) { - log.debug("Received request to store credential in wallet with identifier {}. authorized by BPN: {}", identifier, getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(service.storeCredential(data, identifier, getBPNFromToken(principal))); + @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, Authentication authentication) { + log.debug("Received request to store credential in wallet with identifier {}. authorized by BPN: {}", identifier, TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(service.storeCredential(data, identifier, TokenParsingUtils.getBPNFromToken(authentication))); } /** @@ -98,9 +99,9 @@ public ResponseEntity> storeCredential(@RequestBody Map getWalletByIdentifier( @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, @RequestParam(name = "withCredentials", defaultValue = "false") boolean withCredentials, - Principal principal) { - log.debug("Received request to retrieve wallet with identifier {}. authorized by BPN: {}", identifier, getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.OK).body(service.getWalletByIdentifier(identifier, withCredentials, getBPNFromToken(principal))); + Authentication authentication) { + log.debug("Received request to retrieve wallet with identifier {}. authorized by BPN: {}", identifier, TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.OK).body(service.getWalletByIdentifier(identifier, withCredentials, TokenParsingUtils.getBPNFromToken(authentication))); } /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/BaseController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java similarity index 58% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/BaseController.java rename to miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java index dd3f958ea..b701cf3b4 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/BaseController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -19,38 +19,35 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.controller; +package org.eclipse.tractusx.managedidentitywallets.utils; import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import java.security.Principal; import java.util.Map; import java.util.TreeMap; /** - * The type Base controller. + * The type Bpn validator. */ -public class BaseController { +public class BpnValidator implements OAuth2TokenValidator { - /** - * Gets bpn from token. - * - * @param principal the principal - * @return the bpn from token - */ - public String getBPNFromToken(Principal principal) { - Object principal1 = ((JwtAuthenticationToken) principal).getPrincipal(); - Jwt jwt = (Jwt) principal1; + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, StringPool.BPN_NOT_FOUND, null); + @Override + public OAuth2TokenValidatorResult validate(Jwt jwt) { //this will misbehave if we have more then one claims with different case // ie. BPN=123456 and bpn=789456 Map claims = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); claims.putAll(jwt.getClaims()); - Validate.isFalse(claims.containsKey(StringPool.BPN)).launch(new ForbiddenException("Invalid token, BPN not found")); - return claims.get(StringPool.BPN).toString(); + if (claims.containsKey(StringPool.BPN)) { + return OAuth2TokenValidatorResult.success(); + } else { + return OAuth2TokenValidatorResult.failure(error); + } } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java index e5101f118..88b6bddbf 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java @@ -25,10 +25,16 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import lombok.experimental.UtilityClass; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import java.text.ParseException; +import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; @@ -109,4 +115,13 @@ public static String getNonceAccessToken(JWT accessToken) { throw new BadDataException(PARSING_TOKEN_ERROR, e); } } + + public static String getBPNFromToken(Authentication authentication) { + Jwt jwt = ((JwtAuthenticationToken) authentication).getToken(); + // this will misbehave if we have more then one claims with different case + // ie. BPN=123456 and bpn=789456 + Map claims = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + claims.putAll(jwt.getClaims()); + return claims.get(StringPool.BPN).toString(); + } } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java new file mode 100644 index 000000000..0f1a18a43 --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java @@ -0,0 +1,114 @@ +/* + * ******************************************************************************* + * 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; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.tractusx.managedidentitywallets.config.security.CustomAuthenticationEntryPoint; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +class CustomAuthenticationEntryPointTest { + + private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private HttpServletRequest request; + private HttpServletResponse response; + + @BeforeEach + void setUp() { + customAuthenticationEntryPoint = new CustomAuthenticationEntryPoint(); + request = Mockito.mock(HttpServletRequest.class); + response = Mockito.mock(HttpServletResponse.class); + } + + @Test + @DisplayName("Commence should set unauthorized status and headers when OAuth2 authentication exception") + void commenceShouldSetUnauthorizedStatusAndHeadersWhenOAuth2AuthenticationException() { + OAuth2Error error = new OAuth2Error("invalid_token", "The token is invalid", "https://example.com"); + OAuth2AuthenticationException authException = new OAuth2AuthenticationException(error); + + customAuthenticationEntryPoint.commence(request, response, authException); + + ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(String.class); + verify(response).addHeader(eq(HttpHeaders.WWW_AUTHENTICATE), headerCaptor.capture()); + verify(response).setStatus(HttpStatus.UNAUTHORIZED.value()); + + String wwwAuthenticate = headerCaptor.getValue(); + assertEquals("Bearer error=\"invalid_token\", error_description=\"The token is invalid\", error_uri=\"https://example.com\"", wwwAuthenticate); + } + + @Test + @DisplayName("Commence should set forbidden status when bpn not found exception") + void commence_ShouldSetForbiddenStatus_WhenBpnNotFoundException() { + AuthenticationException authException = new AuthenticationException(StringPool.BPN_NOT_FOUND) { + }; + + customAuthenticationEntryPoint.commence(request, response, authException); + + verify(response).setStatus(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("Commence should set custom realm when realm name is set") + void commence_ShouldSetCustomRealm_WhenRealmNameIsSet() { + customAuthenticationEntryPoint.setRealmName("custom-realm"); + + OAuth2Error error = new OAuth2Error("invalid_token", "The token is invalid", "https://example.com"); + OAuth2AuthenticationException authException = new OAuth2AuthenticationException(error); + + customAuthenticationEntryPoint.commence(request, response, authException); + + ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(String.class); + verify(response).addHeader(eq(HttpHeaders.WWW_AUTHENTICATE), headerCaptor.capture()); + + String wwwAuthenticate = headerCaptor.getValue(); + assertEquals("Bearer realm=\"custom-realm\", error=\"invalid_token\", error_description=\"The token is invalid\", error_uri=\"https://example.com\"", wwwAuthenticate); + } + + @Test + @DisplayName("Commence should set scope when bearer token error has scope") + void commence_ShouldSetScope_WhenBearerTokenErrorHasScope() { + BearerTokenError error = new BearerTokenError("insufficient_scope", HttpStatus.UNAUTHORIZED, "Insufficient scope", "https://example.com", "scope1 scope2"); + OAuth2AuthenticationException authException = new OAuth2AuthenticationException(error); + + customAuthenticationEntryPoint.commence(request, response, authException); + + ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(String.class); + verify(response).addHeader(eq(HttpHeaders.WWW_AUTHENTICATE), headerCaptor.capture()); + + String wwwAuthenticate = headerCaptor.getValue(); + assertEquals("Bearer error=\"insufficient_scope\", error_description=\"Insufficient scope\", error_uri=\"https://example.com\", scope=\"scope1 scope2\"", wwwAuthenticate); + } +} diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java new file mode 100644 index 000000000..794a6aea1 --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java @@ -0,0 +1,89 @@ +/* + * ******************************************************************************* + * 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 org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +class BpnValidatorTest { + + private BpnValidator bpnValidator; + private Jwt jwt; + + @BeforeEach + void setUp() { + bpnValidator = new BpnValidator(); + jwt = Mockito.mock(Jwt.class); + } + + @Test + @DisplayName("Validate when bpn claim is present") + void validateWhenBpnClaimIsPresent() { + Map claims = new HashMap<>(); + claims.put(StringPool.BPN, "123456"); + + when(jwt.getClaims()).thenReturn(claims); + + OAuth2TokenValidatorResult result = bpnValidator.validate(jwt); + + assertFalse(result.hasErrors()); + } + + @Test + @DisplayName("Validate when bpn claim is not present") + void validateWhenBpnClaimIsNotPresent() { + Map claims = new HashMap<>(); + + when(jwt.getClaims()).thenReturn(claims); + + OAuth2TokenValidatorResult result = bpnValidator.validate(jwt); + + assertTrue(result.hasErrors()); + assertEquals(bpnValidator.error.getErrorCode(), result.getErrors().iterator().next().getErrorCode()); + assertEquals(bpnValidator.error.getDescription(), result.getErrors().iterator().next().getDescription()); + } + + @Test + @DisplayName("Validate when bpn claim is present with different case") + void validateWhenBpnClaimIsPresentWithDifferentCase() { + Map claims = new HashMap<>(); + claims.put("BPN", "123456"); + + when(jwt.getClaims()).thenReturn(claims); + + OAuth2TokenValidatorResult result = bpnValidator.validate(jwt); + + assertFalse(result.hasErrors()); + } +}