8/05/2025

Securing Spring Boot APIs with JWT and OAuth2 using Keycloak & Testcontainers : A Hands-On Guide : Part 3

 

This is the third episode of this SpringBoot series [ Part 1 ].


This guide demonstrates how to secure a Spring Boot REST API using OAuth2 with JWT (JSON Web Token), employing Keycloak as the authorization server and Testcontainers for robust integration testing. The implementation details are based on the

security branch of the myfintech-payment-service project, and the complete source code is available on GitHub.



This document provides a high-level overview of the security implementation, explains the core concepts of JWT and OAuth2, and details the necessary configurations and code for securing your application.


1. Core Concepts: JWT and OAuth2


What is JWT? 


JSON Web Token (JWT) is a compact, URL-safe standard for creating access tokens that assert some number of claims. As a self-contained JSON object, it's a popular choice for securely transmitting information for authentication and authorization in web applications. A JWT consists of three parts: a header, a payload, and a signature.


What is OAuth2? 


OAuth2 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service. It delegates user authentication to the service that hosts the user's account, allowing third-party applications to gain access without sharing the user's credentials.



How They Work Together


In this setup, the typical flow is as follows:


  1. Token Request: A client application sends user credentials to the Authorization Server (Keycloak) to request a token.

  2. Token Issuance: If the credentials are valid, the server issues a JWT.

  3. API Access: The client sends this JWT in the Authorization header as a Bearer Token to the Spring Boot API (the Resource Server).

  4. Token Validation: The API validates the token's signature and claims with the Authorization Server. If valid, it processes the request.

The diagram below illustrates the interaction between the test environment, the API, and the Keycloak container.


2. Project Configuration


The Spring Boot application is configured as an OAuth2 Resource Server, responsible for validating JWTs issued by Keycloak. The key features of this implementation include:


  • JWT-based OAuth2 Security

  • Profile-Based Security that can be disabled for local development

  • Integration Testing with Keycloak using Testcontainers


Step 1: Add Dependencies


First, add the necessary Spring Security and Testcontainers dependencies to your pom.xml:


XML


<!-- security-->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>

</dependency>

<!--Test containers-->

<dependency>

<groupId>org.testcontainers</groupId>

<artifactId>junit-jupiter</artifactId>

<version>${testcontainers.version}</version>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.testcontainers</groupId>

<artifactId>postgresql</artifactId>

<version>1.19.7</version>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.testcontainers</groupId>

<artifactId>testcontainers</artifactId>

<version>${testcontainers.version}</version>

<scope>test</scope>

</dependency>

<dependency>

<groupId>com.github.dasniko</groupId>

<artifactId>testcontainers-keycloak</artifactId>

<version>${testcontainers.keycloak.version}</version>

<scope>test</scope>

<exclusions>

<exclusion>

<groupId>io.quarkus</groupId>

<artifactId>quarkus-junit4-mock</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>io.rest-assured</groupId>

<artifactId>rest-assured</artifactId>

<version>${restassured.version}</version>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.hamcrest</groupId>

<artifactId>hamcrest</artifactId>

<version>${hamcrest.version}</version>

<scope>test</scope>

</dependency>


Step 2: Configure JWT Issuer


In your application.yml (or application-containers.yaml for tests), configure the JWT issuer URI. Spring Security will use this to auto-discover the OpenID configuration and JWK set URI needed to validate tokens.



YAML



ecurity:

   oauth2:

      resourceserver:

        jwt:

          issuer-uri: http://localhost:8081/realms/myrealm

          jwk-set-uri: http://localhost:8081/realms/myrealm/protocol/openid-connect/certs




Step 3: Secure Endpoints


Use SecurityFilterChain to protect your endpoints. The configuration below permits access to swagger documentation endpoints while requiring authentication for all other requests. This configuration is active for all profiles except


test and non-security.



Java



@Configuration
@EnableWebSecurity
@Profile ({"!test & !non-security"})
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/api-docs/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
.permitAll() // Allow public access
.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}


Step 4: Disable Security for Development (Optional)


For local development, you can create a security configuration under a

non-security profile that permits all requests.



Java



@Configuration
@Profile ("non-security")
public class NoSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}

Activate this profile by running the application with


#Spring Boot Operations



#to bootup with security

$ mvn spring-boot:run -Denv=local -Dspring-boot.run.profiles=local


--------------------------------------------------------------

#to boot up without security

$ mvn spring-boot:run -Denv=local -Dspring-boot.run.profiles=non-security

--------------------------------------------------------------

#to test

$ mvn clean test -Ptest

#to build and test

$ mvn clean install -Ptest



3. Integration Testing with Testcontainers


Integration tests use Testcontainers to spin up a Keycloak instance in isolation. This ensures that your security configuration is tested in an environment that closely mirrors production.



Keycloak Container Setup


The test class initializes a

KeycloakContainer, configuring it with an admin user and importing a realm definition from a JSON file.



Java



static final KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:26.3")
.withExposedPorts(8080)
.withEnv("KEYCLOAK_ADMIN", "admin")
.withEnv("KEYCLOAK_ADMIN_PASSWORD", "admin")
.withCopyFileToContainer(MountableFile.forClasspathResource("realms/myrealm-realm.json"),
"/opt/keycloak/data/import/myrealm-realm.json")
.withCustomCommand("start-dev --import-realm")
.waitingFor(Wait.forHttp("/realms/myrealm/.well-known/openid-configuration").forStatusCode(200));

static {
keycloak.start();
}


Testing Endpoints


Tests can then programmatically fetch an access token from the Keycloak container and use it to make authenticated requests with RestAssured.



Java



String token = getAccessToken();
given()
.port(port)
.auth().oauth2(token)
.when()
.get("/api/v1/clients")
.then()
.statusCode(200);

For more details on the testing implementation, see the

ClientControllerRestAssuredTest.java file.



Sample Realm Configuration


A sample

myrealm-realm.json file is used to configure the Keycloak instance with a predefined realm, user, and client for testing purposes.



JSON



{
"realm": "myrealm",
"enabled": true,
"users": [
{
"username": "testuser",
"enabled": true,
"realmRoles": [
"user"
]
}
],
"clients": [
{
"clientId": "demo-client",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"directAccessGrantsEnabled": true
}
]
}

No comments:

Post a Comment