Securing APIs is a critical part of building resilient microservice architectures. In service‑to‑service scenarios, using user-based authentication falls short.
Instead, service accounts and OAuth2 client credentials provide a cleaner, more scalable alternative without hardcoding secrets or credentials.
In this article, we’ll explore:
- A Java based secure REST API using Keycloak and JWT
- A stateless OAuth2 flow between a service client and the API server
- Role‑based access control with custom JWT role mapping
You can find the full reference implementation in the secure-rest-api module of my RealWorld Java Solutions repository.
I’ll walk through the theory, essential code, and security rationale without recreating the entire project here.
The Use Case
Imagine you have a background service (e.g., an inventory sync worker) that must invoke a protected /products
endpoint on another service. There is no interactive user, just two services requiring secure communication.
Security Goals
- Enforce Zero Trust: every request must be authenticated
- Use client credentials grant: no user data, just service identity
- Provide role-based access so only specific services can perform certain actions
Sequence Flow
sequenceDiagram participant Client participant Keycloak participant API Client->>Keycloak: POST /token (client_credentials) Keycloak-->>Client: JWT access token Client->>API: GET /products (Authorization: Bearer …) API->>Keycloak: Validate JWT via JWKS API-->>Client: 200 OK or 403 Forbidden
Why Keycloak?
Keycloak is an open-source, enterprise-grade Identity & Access Management system. Features include:
- OpenID Connect and OAuth2 compliance
- Realm, client, and role management
- JWKS endpoint for public key distribution
- Seamless integration with Java via Spring Security
Alternatively, you could use Auth0, AWS Cognito, or any OAuth2 provider. But Keycloak is ideal for local development and open-source stacks.
System Architecture
Below is a simplified architecture diagram of the system:
graph TD Client[Client Service] -->|client_credentials| Keycloak[Keycloak Auth Server] Client -->|Bearer token| ApiServer[Secure REST API] ApiServer -->|Validate JWT| Keycloak[Keycloak Auth Server]
Core Security Implementation
Securing the REST API involves configuring Spring Security to:
- Accept and validate JWTs issued by Keycloak
- Extract role claims from the token payload
- Map those roles to Spring Security’s authorization model
- Enforce fine-grained access control on HTTP endpoints
This is done in a modular and extensible way using a SecurityConfig
class.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/**").hasRole("READER")
)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtAuthenticationConverter())));
return http.build();
}
}
And the configuration:
spring:
application:
name: secure-rest-api-server
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8081/realms/demo
jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
Let’s break down what’s happening:
@EnableMethodSecurity
enables method-level annotations like@PreAuthorize
.- We configure the app as an OAuth2 Resource Server, which means it expects JWTs from an Authorization Server (Keycloak).
- JWTs are verified against the Keycloak JWKS endpoint.
- A custom converter is used to extract
realm_access.roles
from the token payload and translate them into SpringGrantedAuthority
s. This is crucial, because Keycloak’s default token structure doesn’t align out-of-the-box with Spring’s expectations.
Service-to-Service Client Implementation
The client is a standalone Spring Boot app that simulates a non-human service actor — a common case in microservice architectures. It uses the OAuth2 Client Credentials Grant to obtain a token and then calls the secured API.
At a high level, it does three things:
- Requests a token from Keycloak using a
client_id
andclient_secret
- Extracts the
access_token
from the response - Uses that token to call the
/products
endpoint of the secured API
Simplified Token Service:
public class TokenService {
public String fetchAccessToken() {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
body.add("client_id", "secure-rest-api");
body.add("client_secret", "secure-api-secret");
Map<String, Object> tokenResponse = RestClient.create().post()
.uri("http://localhost:8081/realms/demo/protocol/openid-connect/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(Map.class);
return tokenResponse.get("access_token").toString();
}
}
And simplified Client Service class that uses the token:
public class ApiClientService {
public void callSecureApi(String token) {
String response = RestClient.create().get()
.uri("http://localhost:8080/products")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.retrieve()
.body(String.class);
System.out.println("Response: " + response);
}
}
From an architectural perspective, this mirrors what real services would do in production — authenticate with the auth server, obtain a JWT, and use it as a bearer token in HTTP requests. This avoids managing static API keys or user sessions.
Conclusion
Securing backend services via OAuth2 and Keycloak isn’t just about authentication—it’s about designing for Zero Trust, resilience, and enterprise readiness. This system is small, modular, and easy to understand, yet powerful through role-based access, stateless tokens, and tested flows.
Explore the code on GitHub, star the project, and stay tuned for future modules demonstrating cloud-native patterns and enterprise pipelines.
- Fully working solution on GitHub: RealWorld Java Solutions – secure-rest-api module