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:
Next, let's create our realm. We call it baeldung:
We need to add a client, we will name it baeldung-api:
Finally, let's add the Jane Doe user using the Users menu:
Now that we have created the user, we must assign it a password. Let's select s3cr3t and uncheck the temporary button:
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 NullAuthenticatedSessionStrategy
as the session strategy . Also, @ConditionalOnProperty
allows us to disable the Keycloak configuration by setting the keycloak.enable
property to .false
Finally, let's application.properties
add 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:
Unfortunately, Keycloak doesn't export users. In this case, we have to manually edit the generated realm-export.json
file 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.json
the project . src/test/resources/keycloak
We'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 withRealmImportFile
specify 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.properties
the configuration defined in keycloak.auth-server-url
. To 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 User
integration 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 Bearer
classic 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 IntegrationTest
create 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.
0 Comments