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:
Token Request: A client application sends user credentials to the Authorization Server (Keycloak) to request a token.
Token Issuance: If the credentials are valid, the server issues a JWT.
API Access: The client sends this JWT in the Authorization header as a Bearer Token to the Spring Boot API (the Resource Server).
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