Skip to content

Commit

Permalink
feat: added body type to /token endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
andreibogus committed Apr 18, 2024
1 parent 1cbca0d commit 6a67c92
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,20 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.text.ParseException;
import java.util.Set;
import java.util.regex.Pattern;

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


@RestController
@Slf4j
Expand All @@ -82,6 +86,20 @@ void initBinder(WebDataBinder webDataBinder) {
public ResponseEntity<StsTokenResponse> 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<StsTokenResponse> token(
@Valid @RequestBody MultiValueMap<String, String> requestParameters
) {
final SecureTokenRequest secureTokenRequest = getSecureTokenRequest(requestParameters);
return processTokenRequest(secureTokenRequest);
}

private ResponseEntity<StsTokenResponse> processTokenRequest(SecureTokenRequest secureTokenRequest) throws ParseException {
// handle idp authorization
IdpTokenResponse idpResponse = idpAuthorization.fromSecureTokenRequest(secureTokenRequest);
BusinessPartnerNumber bpn = idpResponse.bpn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,13 +41,15 @@
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;
import java.time.Instant;
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;

Expand Down Expand Up @@ -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<String> verifiableCredentialType,
VerifiableCredentialSubject verifiableCredentialSubject,
byte[] privateKey, List<URI> contexts, Date expiryDate) {
Expand Down Expand Up @@ -158,4 +162,10 @@ public static String getKeyString(byte[] privateKeyBytes, String keyType) {
pemWriter.close();
return stringWriter.toString();
}

public static SecureTokenRequest getSecureTokenRequest(MultiValueMap<String, String> map) {
final ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> singleValueMap = map.toSingleValueMap();
return objectMapper.convertValue(singleValueMap, SecureTokenRequest.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> 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.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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 = """
{
Expand Down Expand Up @@ -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<String> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map<String, Object>> 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));
}
}

0 comments on commit 6a67c92

Please sign in to comment.