From 6a67c9266170d77d5161ea38f6e9a8fc76a213ba Mon Sep 17 00:00:00 2001 From: andreibogus Date: Thu, 18 Apr 2024 09:23:20 +0200 Subject: [PATCH] feat: added body type to /token endpoint --- .../controller/SecureTokenController.java | 18 ++++++++ .../utils/CommonUtils.java | 12 +++++- .../SecureTokenRequestValidator.java | 40 +++++++++++------ .../controller/SecureTokenControllerTest.java | 43 ++++++++++++++++--- 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java index 94caec746..82e74e1e9 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java @@ -48,6 +48,7 @@ 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; @@ -55,9 +56,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.text.ParseException; import java.util.Set; import java.util.regex.Pattern; +import static org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils.getSecureTokenRequest; + @RestController @Slf4j @@ -82,6 +86,20 @@ void initBinder(WebDataBinder webDataBinder) { public ResponseEntity token( @Valid @RequestBody SecureTokenRequest secureTokenRequest ) { + return processTokenRequest(secureTokenRequest); + } + + @SneakyThrows + @PostMapping(path = "/api/token", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE }) + @SecureTokenControllerApiDoc.PostSecureTokenDoc + public ResponseEntity token( + @Valid @RequestBody MultiValueMap requestParameters + ) { + final SecureTokenRequest secureTokenRequest = getSecureTokenRequest(requestParameters); + return processTokenRequest(secureTokenRequest); + } + + private ResponseEntity processTokenRequest(SecureTokenRequest secureTokenRequest) throws ParseException { // handle idp authorization IdpTokenResponse idpResponse = idpAuthorization.fromSecureTokenRequest(secureTokenRequest); BusinessPartnerNumber bpn = idpResponse.bpn(); diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java index 36cfcaf5a..338311669 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java @@ -21,12 +21,14 @@ package org.eclipse.tractusx.managedidentitywallets.utils; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; +import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest; import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.ssi.lib.crypt.x21559.x21559PrivateKey; import org.eclipse.tractusx.ssi.lib.exception.InvalidePrivateKeyFormat; @@ -39,6 +41,7 @@ import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialType; import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofGenerator; import org.eclipse.tractusx.ssi.lib.proof.SignatureType; +import org.springframework.util.MultiValueMap; import java.io.StringWriter; import java.net.URI; @@ -46,6 +49,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.regex.Pattern; @@ -105,7 +109,7 @@ public static HoldersCredential getHoldersCredential(VerifiableCredentialSubject .build(); } - @SneakyThrows({UnsupportedSignatureTypeException.class, InvalidePrivateKeyFormat.class}) + @SneakyThrows({ UnsupportedSignatureTypeException.class, InvalidePrivateKeyFormat.class }) private static VerifiableCredential createVerifiableCredential(DidDocument issuerDoc, List verifiableCredentialType, VerifiableCredentialSubject verifiableCredentialSubject, byte[] privateKey, List contexts, Date expiryDate) { @@ -158,4 +162,10 @@ public static String getKeyString(byte[] privateKeyBytes, String keyType) { pemWriter.close(); return stringWriter.toString(); } + + public static SecureTokenRequest getSecureTokenRequest(MultiValueMap map) { + final ObjectMapper objectMapper = new ObjectMapper(); + Map singleValueMap = map.toSingleValueMap(); + return objectMapper.convertValue(singleValueMap, SecureTokenRequest.class); + } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java index 273b898a3..3e2fd2e11 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java @@ -22,36 +22,50 @@ package org.eclipse.tractusx.managedidentitywallets.validator; import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; -public class SecureTokenRequestValidator implements Validator { +import static org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils.getSecureTokenRequest; + +public class SecureTokenRequestValidator implements Validator { + + public static final String LINKED_MULTI_VALUE_MAP_CLASS_NAME = "org.springframework.util.LinkedMultiValueMap"; + public static final String OBJECT_NAME = "multiValueMap"; + @Override public boolean supports(Class clazz) { - return SecureTokenRequest.class.equals(clazz); + return SecureTokenRequest.class.equals(clazz) || clazz.getCanonicalName().equals(LINKED_MULTI_VALUE_MAP_CLASS_NAME); } @Override public void validate(Object target, Errors errors) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "audience", "audience.empty", "The 'audience' cannot be empty or missing."); - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "clientId", "client_id.empty", "The 'client_id' cannot be empty or missing."); - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "clientSecret", "client_secret.empty", "The 'client_secret' cannot be empty or missing."); - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "grantType", "grant_type.empty", "The 'grant_type' cannot be empty or missing."); - SecureTokenRequest secureTokenRequest = (SecureTokenRequest) target; + LinkedMultiValueMap requestParams = null; + if (target instanceof LinkedMultiValueMap) { + requestParams = (LinkedMultiValueMap) target; + } + SecureTokenRequest secureTokenRequest = requestParams != null ? getSecureTokenRequest(requestParams) : (SecureTokenRequest) target; + Errors errorsHandled = new BeanPropertyBindingResult(secureTokenRequest, OBJECT_NAME); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "audience", "audience.empty", "The 'audience' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "clientId", "client_id.empty", "The 'client_id' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "clientSecret", "client_secret.empty", "The 'client_secret' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "grantType", "grant_type.empty", "The 'grant_type' cannot be empty or missing."); + if (secureTokenRequest.getAccessToken() != null && secureTokenRequest.getBearerAccessScope() != null) { - errors.rejectValue("accessToken", "access_token.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); - errors.rejectValue("bearerAccessScope", "bearer_access_scope.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); + errorsHandled.rejectValue("accessToken", "access_token.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); + errorsHandled.rejectValue("bearerAccessScope", "bearer_access_scope.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); } if (secureTokenRequest.getAccessToken() == null && secureTokenRequest.getBearerAccessScope() == null) { - errors.rejectValue("accessToken", "access_token.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); - errors.rejectValue("bearerAccessScope", "bearer_access_scope.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); + errorsHandled.rejectValue("accessToken", "access_token.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); + errorsHandled.rejectValue("bearerAccessScope", "bearer_access_scope.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); } if (secureTokenRequest.getAccessToken() != null) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "accessToken", "access_token.empty", "The 'access_token' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "accessToken", "access_token.empty", "The 'access_token' cannot be empty or missing."); } if (secureTokenRequest.getBearerAccessScope() != null) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "bearerAccessScope", "bearer_access_scope.empty", "The 'bearer_access_scope' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "bearerAccessScope", "bearer_access_scope.empty", "The 'bearer_access_scope' cannot be empty or missing."); } } } diff --git a/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java b/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java index 0de251afa..768b4db2a 100644 --- a/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java +++ b/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java @@ -28,6 +28,7 @@ import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; import org.eclipse.tractusx.ssi.lib.did.web.DidWebFactory; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -56,13 +57,19 @@ class SecureTokenControllerTest { @Autowired private TestRestTemplate testTemplate; - @Test - void token() { + private String bpn; + + private String clientId; + + private String clientSecret; + + @BeforeEach + public void initWallets() { // given - String bpn = TestUtils.getRandomBpmNumber(); + bpn = TestUtils.getRandomBpmNumber(); String partnerBpn = TestUtils.getRandomBpmNumber(); - String clientId = "main"; - String clientSecret = "main"; + clientId = "main"; + clientSecret = "main"; AuthenticationUtils.setupKeycloakClient(clientId, clientSecret, bpn); AuthenticationUtils.setupKeycloakClient("partner", "partner", partnerBpn); String did = DidWebFactory.fromHostnameAndPath(miwSettings.host(), bpn).toString(); @@ -71,7 +78,10 @@ void token() { TestUtils.createWallet(bpn, did, testTemplate, miwSettings.authorityWalletBpn(), defaultLocation); String defaultLocationPartner = miwSettings.host() + COLON_SEPARATOR + partnerBpn; TestUtils.createWallet(partnerBpn, didPartner, testTemplate, miwSettings.authorityWalletBpn(), defaultLocationPartner); + } + @Test + void tokenJSON() { // when String body = """ { @@ -100,4 +110,27 @@ void token() { Assertions.assertNotNull(response.getBody().getOrDefault("token", null)); Assertions.assertNotNull(response.getBody().getOrDefault("expiresAt", null)); } + + @Test + void tokenFormUrlencoded() { + // when + String body = "audience=%s&client_id=%s&client_secret=%s&grant_type=client_credentials&bearer_access_scope=org.eclipse.tractusx.vc.type:BpnCredential:read"; + String requestBody = String.format(body, bpn, clientId, clientSecret); + // then + HttpHeaders headers = new HttpHeaders(); + headers.put(HttpHeaders.CONTENT_TYPE, List.of(MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + HttpEntity entity = new HttpEntity<>(requestBody, headers); + ResponseEntity> response = testTemplate.exchange( + "/api/token", + HttpMethod.POST, + entity, + new ParameterizedTypeReference<>() { + } + ); + Assertions.assertEquals(response.getStatusCode(), HttpStatus.CREATED); + Assertions.assertEquals(response.getHeaders().getContentType(), MediaType.APPLICATION_JSON); + Assertions.assertNotNull(response.getBody()); + Assertions.assertNotNull(response.getBody().getOrDefault("token", null)); + Assertions.assertNotNull(response.getBody().getOrDefault("expiresAt", null)); + } }