Spring Boot, Keycloak and TestContainer Example

Learn to use KeycloakContainer used to fire up a test container while unit testing with JUnit and Spring boot. The KeycloakContainer class runs a Keycloak server instance as Docker container, and configures a realm using the input realm-export.json file.

1. Maven

Start with adding the latest versions of testcontainers, testcontainers-keycloak and org.testcontainers:junit-jupiter dependencies.

  • The test containers provide a simple and convenient way to create, start and stop Docker containers for unit tests.
  • The testcontainers-keycloak provides a pre-configured Docker container for Keycloak (KeycloakContainer class). The default container includes a default set of settings, such as a pre-configured realm and users, which we can override or extend as needed.
  • The org.testcontainers:junit-jupiter dependency is a JUnit 5 extension that provides integration between JUnit 5 and Testcontainers using @Testcontainers and @Container annotations.
<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 setup a keycloak test container with default values, we can use the following code:

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 ‘@Testcontainers(disabledWithoutDocker = true)’ ensures that the test doesn’t run if the docker is not available.
  • The @SpringBootTest and @LocalServerPort annotations start the application and capture the port number for testing the rest apis.

The default KeycloakContainer downloads the latest version of docker image quay.io/keycloak/keycloak and starts the container. The default settings are:

Realm: master
Client Id: admin-cli
User name: admin
Password: admin

2.2. With Realm File

We can import a previously exported realm file if we want to test against a specific realm and set of users and roles. To import the realm file, place it in the /test/resources/keycloak directory and input the file path to KeycloakContainer constructor.

@Container
static KeycloakContainer keycloak = new KeycloakContainer().withRealmImportFile("keycloak/realm-export.json");

Do not forget to remove the “authorizationSettings” tag from the exported realm file to prevent the ‘Script upload is disabled‘ error.

A sample realm file can is saved in Github repository.

3. Getting the Access Token

3.1. Using KeycloakBuilder

The KeycloakBuilder is the API-supported way to fetch the access token from the Keycloak server running as docker container. You can change the realm, client_id, username and password values based on the realm you have configured.

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

For any reason, if we want to use WebClient API to fetch the access token from Keycloak server, we can send the request to realms//protocol/openid-connect/token as follows. Again, change the default admin credentials to your custom realm setup.

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;
}

We have used JacksonJsonParser to parse the JSON response and retrieve the access token. You can use any other library available in your project.

4. Demo

Let us test a REST API using RESTassured. While invoking the REST API, we will send the bearer access token in “Authorization” header.

4.1. REST Controller

The following /users/me API will be invoked after the Spring security is able to authenticate the user. We are injecting the authentication principal object, and we will return the preferred_username field value in the username field 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 using Spring security, we configure the spring.security.oauth2.resourceserver.jwt.issuer-uri in the application.properties file. But in unit testing, the keycloak server root URL is generated at the test runtime. So we need to override this URI in the test class so that Spring security will verify the passed token to the same keycloak server that generated 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

We have enabled the bearer token-based security as follows. The oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) configures this server to validate JWT (JSON Web Token) tokens as part of the OAuth 2.0 authentication flow (in the Authorization header of incoming HTTP requests).

@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

Now, we can invoke the REST API and pass the access token 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

In this Spring boot tutorial, we learned to use TestContainer to deploy a Keycloak server as docker container. We learned about the default realm/user settings and how to create a custom realm. We also learned different ways to retrieve the access token from the keycloak server and send the token in Authorization header to access a secured API.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.