• notice
  • Congratulations on the launch of the Sought Tech site

Spring Boot - Keycloak Integration Testing with Testcontainers

1. Introduction

Integration tests are critical when verifying that an application is working correctly. Also, we should test authentication properly as it is a sensitive part . Testcontainers allow us to launch Docker containers during the testing phase to run our tests against the actual technology stack.

In this article, we'll see how to use Testcontainers to set up integration tests against an actual Keycloak instance .

2. Setting up Spring Security with Keycloak

We need to setup Spring Security, Keycloak configuration, and finally Testcontainers.

2.1. Setting up Spring Boot and Spring Security

Thanks to Spring Security, let's start by setting up security. We need the spring-boot-starter-security dependency. Let's add this to our pom:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

We will use the spring-boot parent pom. Therefore, we don't need to specify the version of the library specified in its dependency management.

Next, let's create a simple controller to return a user:

@RestController
 @RequestMapping("/users")
 public class UserController {
 @GetMapping("me")
 public UserDto getMe() {
 return new UserDto(1L, "janedoe", "Doe", "Jane", "[email protected]");
 }
 }

At this point, we have a security controller that responds to /users/me”requests on " ." When starting the application, Spring Security generates a password for the user "user", which is visible in the application log.

2.2. Configuring Keycloak

The easiest way to start a local Keycloak is to use Docker . Let's run a Keycloak container with an administrator account configured:

docker run -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:17.0.1 start-dev

Let's open a browser to the URL http://localhost:8081 to access the Keycloak console:

spring-boot-keycloak-integration-testing-with-testcontainers.png

Next, let's create our realm. We call it baeldung:

spring-boot-keycloak-integration-testing-with-testcontainers-1.png

We need to add a client, we will name it baeldung-api:

spring-boot-keycloak-integration-testing-with-testcontainers-2.png

Finally, let's add the Jane Doe user using the Users menu:

spring-boot-keycloak-integration-testing-with-testcontainers-3.png

Now that we have created the user, we must assign it a password. Let's select s3cr3t and uncheck the temporary button:

spring-boot-keycloak-integration-testing-with-testcontainers-4.png

We have now set up our Keycloak realm with the baeldung-api client and the Jane Doe user .

We will next configure Spring to use Keycloak as the identity provider.

2.3. Putting the two together

First, we delegate identification control to the Keycloak server. For this we will use a handy starter keycloak-spring-boot-starter . So let's add this to our pom:

<dependency>
 <groupId>org.keycloak</groupId>
 <artifactId>keycloak-spring-boot-starter</artifactId>
 </dependency>

We also need the keycloak-adapter-bom dependency. Specifically, it adds the main adapter to take full advantage of Spring auto-configuration, as well as the libraries needed to connect Keycloak with different web containers (including Tomcat):

<dependencyManagement>
 <dependencies>
 <dependency>
 <groupId>org.keycloak.bom</groupId>
 <artifactId>keycloak-adapter-bom</artifactId>
 <version>${keycloak-adapter.version}</version>
 <type>pom</type>
 <scope>import</scope>
 </dependency>
 </dependencies>
 </dependencyManagement>

Then, let's create a configuration class that uses Spring properties to configure the Keycloak adapter.

@Configuration
 public class KeycloakConfiguration {
 @Bean
 public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
 return new KeycloakSpringBootConfigResolver();
 }
 }

Let's go ahead and configure Spring Security to use the Keycloak configuration:

@KeycloakConfiguration
 @ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
 public class KeycloakSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
 @Autowired
 public void configureGlobal(AuthenticationManagerBuilder auth) {
 auth.authenticationProvider(keycloakAuthenticationProvider());
 }
 @Bean
 @Override
 protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
 return new NullAuthenticatedSessionStrategy();
 }
 @Override
 protected void configure(HttpSecurity http) throws Exception {
 super.configure(http);
 http.csrf()
 .disable()
 .cors()
 .and()
 .authorizeRequests()
 .anyRequest()
 .authenticated();
 }
 }

We are building a stateless application with bearer-only authentication. For this reason, we will use NullAuthenticatedSessionStrategyas the session strategy . Also, @ConditionalOnPropertyallows us to disable the Keycloak configuration by setting the keycloak.enableproperty to .false

Finally, let's application.propertiesadd the configuration needed to connect Keycloak to the file:

keycloak.enabled=true
 keycloak.realm=baeldung
 keycloak.resource=baeldung-api
 keycloak.auth-server-url=http://localhost:8081

Our application is now secure and queries Keycloak on every request to verify authentication .

3. Set up a test container for Keycloak

3.1. Export realm configuration

The Keycloak container starts without any configuration. Therefore, we have to import it when the container is started as a JSON file . Let's export this file from the currently running instance:

spring-boot-keycloak-integration-testing-with-testcontainers-5.png

Unfortunately, Keycloak doesn't export users. In this case, we have to manually edit the generated realm-export.jsonfile and add our Jane Doe to it. Let's add this configuration before the last curly brace:

"users": [
 {
 "username": "janedoe",
 "email": "[email protected]",
 "firstName": "Jane",
 "lastName": "Doe",
 "enabled": true,
 "credentials": [
 {
 "type": "password",
 "value": "s3cr3t"
 }
 ],
 "clientRoles": {
 "account": [
 "view-profile",
 "manage-account"
 ]
 }
 }
 ]

Let's include the file into a folder in realm-export.jsonthe project . src/test/resources/keycloakWe'll use it when starting the Keycloak container.

3.2. Setting up the test container

Let's add the testcontainers dependency along with testcontainers-keycloak , which allows us to start the Keycloak container:

<dependency>
 <groupId>com.github.dasniko</groupId>
 <artifactId>testcontainers-keycloak</artifactId>
 <version>2.1.2</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>org.testcontainers</groupId>
 <artifactId>testcontainers</artifactId>
 <version>1.16.3</version>
 </dependency>

Next, let's create a class from which all our tests will derive. We use this to configure Keycloak containers started by Testcontainers:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
 public abstract class IntegrationTest {
 static final KeycloakContainer keycloak = new KeycloakContainer().withRealmImportFile("keycloak/realm-export.json");
 }

Declaring our container statically will ensure that it is instantiated once for all our tests. We use methods in the KeycloakContainer object to withRealmImportFilespecify the realm configuration to import at startupKeycloakContainer

3.3. Spring Boot test configuration

Now, let's start the Keycloak container at the beginning of the test. It uses random ports. So, once started, we need to override application.propertiesthe configuration defined in keycloak.auth-server-urlTo do this, we'll implement a Spring-triggered callback interface before refreshing the context:

static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
 public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
 keycloak.start();
 TestPropertyValues.of("keycloak.auth-server-url=" + keycloak.getAuthServerUrl())
 .applyTo(configurableApplicationContext.getEnvironment());
 }
 }

We also need to tell Spring to use this class to initialize its context. Let's add this annotation at the class level:

@ContextConfiguration(initializers = { IntegrationTest.Initializer.class })

4. Create an integration test

Now that we have the main test class responsible for starting the Keycloak container and configuring Spring properties, let's create an Userintegration test that calls the controller.

4.1. Get access token

First, let's add a method to the abstract class IntegrationTest that requests a token using Jane Doe's credentials:

URI authorizationURI = new URIBuilder(keycloak.getAuthServerUrl() + "/realms/baeldung/protocol/openid-connect/token").build();
 WebClient webclient = WebClient.builder().build();
 MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
 formData.put("grant_type", Collections.singletonList("password"));
 formData.put("client_id", Collections.singletonList("baeldung-api"));
 formData.put("username", Collections.singletonList("[email protected]"));
 formData.put("password", Collections.singletonList("s3cr3t"));
 String result = webclient.post()
 .uri(authorizationURI)
 .contentType(MediaType.APPLICATION_FORM_URLENCODED)
 .body(BodyInserters.fromFormData(formData))
 .retrieve()
 .bodyToMono(String.class)
 .block();

Here, we use Webflux's WebClient to post a form that contains the different parameters needed to get an access token.

Finally, we'll parse the Keycloak server response to extract the token from it . Specifically, we generate a Bearerclassic authentication string containing keywords, followed by the content of the token, ready to be used in the header:

JacksonJsonParser jsonParser = new JacksonJsonParser();
 return "Bearer " + jsonParser.parseMap(result)
 .get("access_token")
 .toString();

4.2. Creating Integration Tests

Let's quickly set up an integration test against our configured Keycloak container. We will use RestAssured and Hamcrest for testing. Let's add reassurance dependencies:

<dependency>
 <groupId>io.rest-assured</groupId>
 <artifactId>rest-assured</artifactId>
 <scope>test</scope>
 </dependency>

We can now IntegrationTestcreate our test using the abstract class:

@Test
 void givenAuthenticatedUser_whenGetMe_shouldReturnMyInfo() {
 given().header("Authorization", getJaneDoeBearer())
 .when()
 .get("/users/me")
 .then()
 .body("username", equalTo("janedoe"))
 .body("lastname", equalTo("Doe"))
 .body("firstname", equalTo("Jane"))
 .body("email", equalTo("[email protected]"));
 }

As a result, the access token we got from Keycloak is added to the Authorization header of the request.

5 Conclusion

In this article, we set up integration tests against the actual Keycloak managed by Testcontainers . Every time we start the test phase, we import a realm configuration to have a pre-configured environment.


Tags

Technical otaku

Sought technology together

Related Topic

0 Comments

Leave a Reply

+