Added code for BAEL-4965: Securing SOAP services using Keycloa
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package com.baeldung.keycloaksoap;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* The class contains Live/Integration tests.
|
||||
* These tests expect that the Keycloak server is up and running on port 8080.
|
||||
* The tests may fail without a Keycloak server.
|
||||
*/
|
||||
@DisplayName("Keycloak SOAP Webservice Unit Tests")
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@AutoConfigureMockMvc
|
||||
class KeycloakSoapIntegrationTest {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeycloakSoapIntegrationTest.class);
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Value("${grant.type}")
|
||||
private String grantType;
|
||||
@Value("${client.id}")
|
||||
private String clientId;
|
||||
@Value("${client.secret}")
|
||||
private String clientSecret;
|
||||
@Value("${url}")
|
||||
private String keycloakUrl;
|
||||
|
||||
/**
|
||||
* Test a happy flow. Test the <i>janedoe</i> user.
|
||||
* This user should be configured in Keycloak server with a role <i>user</i>
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("Get Products With Access Token")
|
||||
void givenAccessToken_whenGetProducts_thenReturnProduct() {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("content-type", "text/xml");
|
||||
headers.set("Authorization", "Bearer " + generateToken("janedoe", "password"));
|
||||
HttpEntity<String> request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers);
|
||||
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
|
||||
|
||||
assertThat(responseEntity).isNotNull();
|
||||
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value());
|
||||
assertThat(responseEntity.getBody()).isNotBlank();
|
||||
assertThat(responseEntity.getBody()).containsIgnoringCase(":id>1</");
|
||||
}
|
||||
|
||||
/**
|
||||
* A negative test. Deliberately pass wrong credentials to Keycloak. Test the invalid <i>janeadoe</i> user.
|
||||
* Keycloak returns Unauthorized. Assert 401 status and empty body.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("Get Products With Wrong Access Token")
|
||||
void givenWrongAccessToken_whenGetProducts_thenReturnError() {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("content-type", "text/xml");
|
||||
headers.set("Authorization", "Bearer " + generateToken("janeadoe", "password"));
|
||||
HttpEntity<String> request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers);
|
||||
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
|
||||
System.out.println("This is the URL --> " + "http://localhost:" + port + "/ws/api/v1/");
|
||||
System.out.println("Body --> " + responseEntity.getBody());
|
||||
System.out.println("Location Header --> " + responseEntity.getHeaders().get("Location"));
|
||||
assertThat(responseEntity).isNotNull();
|
||||
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||
assertThat(responseEntity.getBody()).isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* Happy flow to test <i>deleteProduct</i> operation. Test the <i>jhondoe</i> user.
|
||||
* This user should be configured in Keycloak server with a role <i>user</i>
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("Delete Product With Access Token")
|
||||
void givenAccessToken_whenDeleteProduct_thenReturnSuccess() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("content-type", "text/xml");
|
||||
headers.set("Authorization", "Bearer " + generateToken("jhondoe", "password"));
|
||||
HttpEntity<String> request = new HttpEntity<>(Utility.getDeleteProductsRequest(), headers);
|
||||
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
|
||||
|
||||
assertThat(responseEntity).isNotNull();
|
||||
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value());
|
||||
assertThat(responseEntity.getBody()).isNotBlank();
|
||||
assertThat(responseEntity.getBody()).containsIgnoringCase("Deleted the product with the id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Negative flow to test <i></i>. Test the <i>janedoe</i> user.
|
||||
* Obtain the access token of <i>janedoe</i> and access the admin operation <i>deleteProduct</i>
|
||||
* Assume <i>janedoe</i> has restricted access to <i>deleteProduct</i> operation
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("Delete Products With Unauthorized Access Token")
|
||||
void givenUnauthorizedAccessToken_whenDeleteProduct_thenReturnUnauthorized() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("content-type", "text/xml");
|
||||
headers.set("Authorization", "Bearer " + generateToken("janedoe", "password"));
|
||||
HttpEntity<String> request = new HttpEntity<>(Utility.getDeleteProductsRequest(), headers);
|
||||
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
|
||||
|
||||
assertThat(responseEntity).isNotNull();
|
||||
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
assertThat(responseEntity.getBody()).isNotBlank();
|
||||
assertThat(responseEntity.getBody()).containsIgnoringCase("Access is denied");
|
||||
}
|
||||
|
||||
private String generateToken(String username, String password) {
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
|
||||
map.add("grant_type", grantType);
|
||||
map.add("client_id", clientId);
|
||||
map.add("client_secret", clientSecret);
|
||||
map.add("username", username);
|
||||
map.add("password", password);
|
||||
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
|
||||
ResponseEntity<String> response = restTemplate.exchange(keycloakUrl, HttpMethod.POST, entity, String.class);
|
||||
return Objects.requireNonNull(response.getBody()).contains("access_token") ? objectMapper.readTree(response.getBody()).get("access_token").asText() : "";
|
||||
} catch (Exception ex) {
|
||||
logger.error("There is an internal server error. Returning an empty access token", ex);
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baeldung.keycloaksoap;
|
||||
|
||||
public class Utility {
|
||||
public static String getGetProductDetailsRequest() {
|
||||
return "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:key=\"http://www.baeldung.com/springbootsoap/keycloak\">\n" + " <soapenv:Header/>\n" + " <soapenv:Body>\n" + " <key:getProductDetailsRequest>\n"
|
||||
+ " <key:id>1</key:id>\n" + " </key:getProductDetailsRequest>\n" + " </soapenv:Body>\n" + "</soapenv:Envelope>";
|
||||
}
|
||||
public static String getDeleteProductsRequest() {
|
||||
return "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:key=\"http://www.baeldung.com/springbootsoap/keycloak\">\n" + " <soapenv:Header/>\n" + " <soapenv:Body>\n" + " <key:deleteProductRequest>\n"
|
||||
+ " <key:id>1</key:id>\n" + " </key:deleteProductRequest>\n" + " </soapenv:Body>\n" + "</soapenv:Envelope>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
grant.type=password
|
||||
client.id=baeldung-soap-services
|
||||
client.secret=d2ba7af8-f7d2-4c97-b4a5-3c88b59920ae
|
||||
url=http://localhost:8080/auth/realms/baeldung-soap-services/protocol/openid-connect/token
|
||||
Reference in New Issue
Block a user