Testcontainers is a Java library that starts Docker containers from test code, so a test runs against a real service. The class KeycloakContainer from the module testcontainers-keycloak applies this to Keycloak. The KeycloakContainer class runs a Keycloak server instance as Docker container and configures a realm from the input file realm-export.json. We use it to test a Spring Boot application that is secured by Keycloak with JUnit, without installing a Keycloak server. This post covers the Maven dependencies, the container setup, fetching an access token and a test that calls a secured REST API.
1. Maven
We add the latest versions of the testcontainers, testcontainers-keycloak and org.testcontainers:junit-jupiter dependencies.
- The Testcontainers library creates, starts and stops Docker containers for unit tests.
- The testcontainers-keycloak module provides a pre-configured Docker container for Keycloak in the class KeycloakContainer. The default container includes a default set of settings, such as a pre-configured realm and users, which we can override or extend.
- The org.testcontainers:junit-jupiter dependency is a JUnit 5 extension that integrates JUnit 5 with Testcontainers through the annotations @Testcontainers and @Container.
<properties>
<testcontainers.version>1.17.6</testcontainers.version>
<testcontainers-keycloak.version>2.4.0</testcontainers-keycloak.version>
<testcontainers-junit-jupiter.version>1.18.0</testcontainers-junit-jupiter.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>${testcontainers-keycloak.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers-junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
2. Setting Up KeycloakContainer
2.1. With Default Values
To start a Keycloak test container with the default values, we create a KeycloakContainer without arguments.
import dasniko.testcontainers.keycloak.KeycloakContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers(disabledWithoutDocker = true)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class KeyCloakTestContainerTest {
@LocalServerPort
private int port;
@Container
static KeycloakContainer keycloak = new KeycloakContainer();
//...
}
- The annotation @Testcontainers(disabledWithoutDocker = true) skips the test when Docker is not available.
- The annotations @SpringBootTest and @LocalServerPort start the application and capture the port number that the tests use to call the REST APIs.
By default, KeycloakContainer downloads the latest version of the Docker image quay.io/keycloak/keycloak and starts the container. The container starts with these default settings.
Realm: master
Client Id: admin-cli
User name: admin
Password: admin
2.2. With Realm File
To test against a specific realm with its own users and roles, we import a previously exported realm file. We place the file in the directory /test/resources/keycloak and pass its path to the constructor of KeycloakContainer.
@Container
static KeycloakContainer keycloak = new KeycloakContainer().withRealmImportFile("keycloak/realm-export.json");
We remove the authorizationSettings tag from the exported realm file, because the tag causes the error Script upload is disabled.
A sample realm file is available in the Github repository.
3. Getting the Access Token
3.1. Using KeycloakBuilder
The class KeycloakBuilder is the API-supported way to fetch the access token from the Keycloak server that runs as the Docker container. We change the values of realm, client_id, username and password to match the configured realm.
protected String getBearerToken() {
try (Keycloak keycloakAdminClient = KeycloakBuilder.builder()
.serverUrl(keycloak.getAuthServerUrl())
.realm("master")
.clientId("admin-cli")
.username(keycloak.getAdminUsername())
.password(keycloak.getAdminPassword())
.build()) {
String access_token = keycloakAdminClient.tokenManager().getAccessToken().getToken();
return "Bearer " + access_token;
} catch (Exception e) {
LOGGER.error("Can't obtain an access token from Keycloak!", e);
}
return null;
}
3.2. Using WebClient
To fetch the access token with the WebClient API instead, we send the request to realms//protocol/openid-connect/token. We change the default admin credentials to the credentials of the custom realm.
protected String getBearerToken() {
try {
URI authorizationURI = new URIBuilder(keycloak.getAuthServerUrl()
+ "realms/master/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("admin-cli"));
formData.put("username", Collections.singletonList(keycloak.getAdminUsername()));
formData.put("password", Collections.singletonList(keycloak.getAdminPassword()));
String result = webclient.post()
.uri(authorizationURI)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
JacksonJsonParser jsonParser = new JacksonJsonParser();
return "Bearer " + jsonParser.parseMap(result).get("access_token").toString();
} catch (URISyntaxException e) {
LOGGER.error("Can't obtain an access token from Keycloak!", e);
}
return null;
}
The code parses the JSON response with JacksonJsonParser to retrieve the access token. Any other JSON library of the project works as well.
4. Demo
We test the REST API with RESTassured. The test sends the bearer access token in the Authorization header.
4.1. REST Controller
The API /users/me is invoked after Spring Security authenticates the user. It injects the authentication principal object and returns the value of its preferred_username field in the field username of the API response.
@RestController
@RequestMapping("/users")
public class UserController {
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class.getName());
@GetMapping("me")
public User getMe(JwtAuthenticationToken principal) {
LOGGER.info("Principal attributes:: " + principal.getTokenAttributes());
String username = principal.getTokenAttributes().get("preferred_username").toString();
/*String name = principal.getTokenAttributes().get("name").toString();
String email = principal.getTokenAttributes().get("email").toString();*/
return new User(1L, username, "", "", "");
}
}
4.2. Token Issuer URI
In OAuth2 authentication with Spring Security, we configure the property spring.security.oauth2.resourceserver.jwt.issuer-uri in the file application.properties. In a unit test, the Keycloak server root URL is generated at test runtime. So we override this property in the test class, and Spring Security then verifies the token against the Keycloak server that issued it.
public class KeyCloakTestContainerTest {
@DynamicPropertySource
static void registerResourceServerIssuerProperty(DynamicPropertyRegistry registry) {
registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri",
() -> keycloak.getAuthServerUrl() + "realms/master");
}
}
4.3. Spring Security
Bearer token-based security is enabled in the security configuration. The method oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) configures the application to validate JSON Web Token (JWT) tokens that arrive in the Authorization header of incoming HTTP requests, as part of the OAuth 2.0 authentication flow.
@Configuration
public class OAuth2Security {
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf()
.disable()
.cors()
.and()
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.build();
}
}
4.4. Test
In the test, we invoke the REST API and pass the access token that we generated in the previous step.
@Test
void givenAuthenticatedUser_whenGetMe_shouldReturnMyInfo() {
given().header("Authorization", getBearerToken())
.when()
.get("/users/me")
.then()
.body("username", equalTo("admin"))
/*.body("lastName", equalTo(""))
.body("firstName", equalTo("admin"))
.body("email", equalTo("test-user@howtodoinjava.com"))*/;
}
5. Conclusion
This post used Testcontainers to run a Keycloak server as a Docker container in a Spring Boot test. We covered the default realm and user settings, a custom realm imported from a file, and two ways to retrieve the access token from the Keycloak server. The test sends the token in the Authorization header to access a secured API.
Happy Learning !!