Repository navigation
Adopt JWT Authentication using Client Credentials Flow for protected API routes #132
Description
Activity
coderabbitai commented on Dec 30, 2025
🤖 CodeRabbit Planning
Planning is in progress. This will take a moment...
coderabbitai commented on Dec 30, 2025
Coding Plan
Approach
Ticket Summary
Implement JWT-based authentication using OAuth 2.0 Client Credentials Flow to secure mutating API endpoints (POST, PUT, DELETE) while keeping GET endpoints publicly accessible.
Observations
Codebase Summary
This is a Spring Boot 4.0.0 REST API following a layered architecture with controllers, services, repositories, and models packages. The project uses reverse-domain naming convention (ar.com.nanotaboada.java.samples.spring.boot), Jakarta Bean Validation for request validation, and application.properties for configuration. Testing follows @WebMvcTest patterns with MockMvc and test data builders. Currently, the API has no security implementation, making it completely open.
Assumptions
Assumption 1: JWT signing approach for token generation
Options Considered:
- Use HMAC-SHA256 with a secret key loaded from application.properties
- Use RSA asymmetric keys with public/private key pair
Chosen Option: HMAC-SHA256 with a secret key loaded from application.properties
Rationale: This aligns with the ticket's suggested approach, keeps implementation simple for machine-to-machine authentication, and can be configured per environment. The secret will be externalized to application.properties following the existing configuration pattern.
Assumption 2: Token endpoint request format handling
Options Considered:
- Support only application/x-www-form-urlencoded (standard OAuth 2.0 format)
- Support only application/json (simpler for REST APIs)
- Support both content types
Chosen Option: Support only application/x-www-form-urlencoded (standard OAuth 2.0 format)
Rationale: Support application/x-www-form-urlencoded primarily as shown in the ticket's curl example, with the controller accepting form parameters rather than JSON request body. This aligns with RFC 6749 standard and the ticket's sample request, ensuring interoperability with standard OAuth 2.0 clients.
Assumption 3: Actuator endpoint security
Options Considered:
- Apply same JWT authentication to Actuator endpoints
- Leave Actuator endpoints unsecured on the separate port
- Configure different security rules for management port
Chosen Option: Leave Actuator endpoints unsecured on the separate port
Rationale: Since Actuator runs on a separate management port, it can be secured independently (network-level restrictions, separate security configuration) without complicating the JWT implementation. This keeps the scope focused on the ticket requirements and follows the principle of "build only what's needed."
💡 User Tips
Regenerate the plan with different choices with @coderabbitai <feedback>.
Implementation Steps
Phase 1: Add Dependencies and Configuration
Set up the necessary libraries and configuration properties for JWT authentication.
Task 1: Add Spring Security and JJWT Dependencies
Add required Maven dependencies to pom.xml for Spring Security framework and JWT token handling.
- Add
spring-boot-starter-securitydependency to enable Spring Security framework - Add JJWT 0.13.0 dependencies:
jjwt-api(compile scope),jjwt-impl(runtime scope), andjjwt-jackson(runtime scope) for JWT generation and parsing - These versions align with Spring Boot 4.0.0 and provide the modern parserBuilder API
Task 2: Configure JWT Properties
Define JWT-related configuration in application.properties for secret key and token expiration.
- Add properties for JWT secret key (jwt.secret.key), token expiration time (jwt.expiration.seconds), and issuer (jwt.issuer)
- Use environment variable substitution pattern like
${JWT_SECRET_KEY:default-dev-secret-key}following the existing STORAGE_PATH pattern - Set default expiration to 3600 seconds (1 hour) as suggested in ticket
🤖 Prompt for AI agents
Configure Spring Security and JWT dependencies in pom.xml, then add JWT
configuration properties to application.properties.
Add Maven dependencies:
- spring-boot-starter-security
- jjwt-api version 0.13.0 (compile scope)
- jjwt-impl version 0.13.0 (runtime scope)
- jjwt-jackson version 0.13.0 (runtime scope)
Add JWT configuration properties to application.properties:
- jwt.secret.key with environment variable substitution pattern
- jwt.expiration.seconds with default of 3600
- jwt.issuer
Phase 2: Implement JWT Token Issuance
Create the token endpoint and supporting components to issue JWTs in exchange for valid client credentials.
Task 1: Create Token Request and Response DTOs
Add data transfer objects in the models package for type-safe token endpoint communication.
- Create
TokenRequest.javain models package with fields for grant_type, client_id, and client_secret - Create
TokenResponse.javain models package with fields for access_token, token_type, and expires_in - Apply Jakarta validation annotations (@notblank) on TokenRequest fields following the existing BookDTO validation pattern
- Use record classes or standard POJOs with getters/setters to align with existing DTO style
Task 2: Implement Client Credentials Configuration
Create a configuration class to define the in-memory client store.
- Create
ClientConfig.javain a new config package at the root of the base package - Define a bean returning
Map<String, String>with client_id as key and client_secret as value - Use the example credentials from ticket: "foobarbaz" with the provided secret
- Add @configuration annotation to register as Spring configuration class
Task 3: Implement JWT Service
Create a service layer component for JWT token generation and validation.
- Create
JwtService.javain services package for JWT operations - Inject JWT configuration properties using @value annotations for secret key, expiration, and issuer
- Implement
generateToken(String clientId)method using JJWT 0.13.0 builder API with Jwts.builder() setting subject, issued-at, expiration, and signing with HMAC-SHA256 - Implement
parseToken(String token)method using JJWT 0.13.0 parser API with Jwts.parser().verifyWith(key).build().parseSignedClaims(token) pattern - Handle JwtException for expired, malformed, or invalid tokens
Task 4: Create Token Endpoint Controller
Add a REST controller for the token issuance endpoint.
- Create
TokenController.javain controllers package with @RestController and @RequestMapping("/auth/token") - Inject JwtService and client store Map dependencies
- Implement POST endpoint accepting @RequestParam for form-urlencoded format (grant_type, client_id, client_secret)
- Validate grant_type is "client_credentials" - return 400 with error "unsupported_grant_type" if invalid
- Validate client credentials against client store - return 401 with error "invalid_client" if authentication fails
- On success, call JwtService.generateToken() and return TokenResponse with 200 OK
- Format error responses as JSON with "error" and "error_description" fields per RFC 6749
🤖 Prompt for AI agents
Implement OAuth 2.0 Client Credentials token issuance with JWT generation.
Create DTOs in models package:
- TokenRequest with grant_type, client_id, client_secret fields and Jakarta
validation
- TokenResponse with access_token, token_type, expires_in fields
Create ClientConfig in config package:
- @Configuration class with Map<String, String> bean for client credentials
- Add example client "foobarbaz" with provided secret
Create JwtService in services package:
- Inject JWT properties (secret, expiration, issuer) via @Value
- Implement generateToken(String clientId) using JJWT builder with HMAC-SHA256
- Implement parseToken(String token) using JJWT parser for validation
- Handle JwtException appropriately
Create TokenController in controllers package:
- POST /auth/token endpoint accepting form-urlencoded parameters
- Validate grant_type and client credentials
- Return TokenResponse on success or RFC 6749 error format on failure
Phase 3: Implement JWT Authentication Filter and Security Configuration
Configure Spring Security to validate JWT tokens and protect mutating endpoints.
Task 1: Create JWT Authentication Filter
Implement a servlet filter to extract and validate JWT tokens from Authorization headers.
- Create
JwtAuthenticationFilter.javain a new security package extending OncePerRequestFilter - Inject JwtService as constructor dependency
- In doFilterInternal, extract Bearer token from Authorization header
- Call JwtService.parseToken() and create UsernamePasswordAuthenticationToken with subject from claims
- Set Authentication in SecurityContextHolder for valid tokens
- Let exceptions propagate to be handled by AuthenticationEntryPoint (don't catch and set status directly)
- Continue filter chain with filterChain.doFilter() for all cases
Task 2: Configure Spring Security Filter Chain
Set up Spring Security configuration to enforce JWT authentication on mutating endpoints.
- Create
SecurityConfig.javain security package with @configuration and @EnableWebSecurity - Define SecurityFilterChain bean using HttpSecurity with lambda DSL syntax for Spring Security 7.x
- Configure CSRF as disabled with
.csrf(csrf -> csrf.disable()) - Configure authorization rules with
.authorizeHttpRequests()using lambda syntax:- Permit all GET requests with
.requestMatchers(HttpMethod.GET, "/**").permitAll() - Require authentication for POST, PUT, DELETE with
.requestMatchers(HttpMethod.POST/PUT/DELETE, "/**").authenticated() - Permit /auth/token endpoint without authentication with explicit matcher
- Permit all GET requests with
- Add JwtAuthenticationFilter before UsernamePasswordAuthenticationFilter
- Configure sessionManagement as stateless with
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
🤖 Prompt for AI agents
Configure Spring Security with JWT token validation to protect mutating
endpoints while keeping GET endpoints public.
Create JwtAuthenticationFilter in security package:
- Extend OncePerRequestFilter
- Extract Bearer token from Authorization header
- Validate token using JwtService.parseToken()
- Set Authentication in SecurityContextHolder for valid tokens
- Continue filter chain in all cases
Create SecurityConfig in security package:
- @Configuration and @EnableWebSecurity
- Define SecurityFilterChain bean with lambda DSL
- Disable CSRF
- Configure authorization: permit all GET requests, require authentication for
POST/PUT/DELETE, permit /auth/token
- Add JwtAuthenticationFilter before UsernamePasswordAuthenticationFilter
- Set session management to stateless
Phase 4: Add Error Handling for Authentication Failures
Implement consistent JSON error responses for authentication and authorization failures.
Task 1: Create Authentication Entry Point
Implement custom entry point to return JSON responses for unauthenticated requests.
- Create
JwtAuthenticationEntryPoint.javain security package implementing AuthenticationEntryPoint - In commence() method, set response status to 401 and content type to application/json
- Write JSON response body with "error" field set to "unauthorized" and "error_description" explaining authentication is required
- Use simple JSON string construction or ObjectMapper to build response
Task 2: Register Error Handlers in Security Configuration
Update SecurityFilterChain to use custom error handlers.
- In SecurityConfig.java, add
.exceptionHandling()configuration with lambda DSL - Register JwtAuthenticationEntryPoint with
.authenticationEntryPoint(jwtAuthenticationEntryPoint) - This ensures consistent JSON error responses for authentication failures instead of default Spring Security behavior
🤖 Prompt for AI agents
Implement custom JSON error responses for authentication failures.
Create JwtAuthenticationEntryPoint in security package:
- Implement AuthenticationEntryPoint interface
- In commence(), return 401 status with JSON body containing "error" and
"error_description" fields
Update SecurityConfig:
- Add exceptionHandling configuration with lambda DSL
- Register JwtAuthenticationEntryPoint
Phase 5: Add Integration Tests
Create comprehensive tests for token issuance and protected endpoint access following existing test patterns.
Task 1: Create Token Controller Integration Tests
Add tests for the /auth/token endpoint covering success and error scenarios.
- Create
TokenControllerTests.javain test controllers package using @WebMvcTest(TokenController.class) - Mock JwtService and client store beans with @MockitoBean
- Test successful token issuance with valid credentials returns 200 with access_token, token_type, and expires_in
- Test invalid grant_type returns 400 with "unsupported_grant_type" error
- Test invalid client credentials return 401 with "invalid_client" error
- Test missing parameters return appropriate error responses
- Follow existing test naming pattern:
given<Action>_when<Condition>_then<Expected>with @DisplayName
Task 2: Create Security Integration Tests for Protected Endpoints
Add tests verifying JWT authentication on mutating endpoints.
- Create
BooksControllerSecurityTests.javain test controllers package using @WebMvcTest(BooksController.class) - Configure security test support with @AutoConfigureMockMvc and mock security beans
- Test POST /books without token returns 401
- Test PUT /books without token returns 401
- Test DELETE /books/{isbn} without token returns 401
- Test POST/PUT/DELETE with valid JWT token succeeds (mock JWT validation in filter)
- Test GET /books remains accessible without authentication
- Use MockMvc with
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)pattern for authenticated requests
Task 3: Create JWT Service Unit Tests
Add unit tests for JWT token generation and validation logic.
- Create
JwtServiceTests.javain test services package - Test generateToken() creates valid JWT with correct subject, expiration, and signature
- Test parseToken() successfully extracts claims from valid tokens
- Test parseToken() throws appropriate exceptions for expired tokens
- Test parseToken() throws appropriate exceptions for malformed tokens
- Test parseToken() throws appropriate exceptions for tokens with invalid signatures
- Use @TestConfiguration to provide test JWT configuration values
🤖 Prompt for AI agents
Create comprehensive tests for token issuance, JWT service, and endpoint
security following existing @WebMvcTest patterns.
Create TokenControllerTests:
- @WebMvcTest(TokenController.class) with mocked JwtService and client store
- Test successful token issuance with valid credentials
- Test error cases: invalid grant_type, invalid credentials, missing parameters
- Follow existing naming pattern with @DisplayName
Create BooksControllerSecurityTests:
- @WebMvcTest(BooksController.class) with @AutoConfigureMockMvc
- Test POST/PUT/DELETE without token returns 401
- Test POST/PUT/DELETE with valid token succeeds
- Test GET remains publicly accessible
- Use Authorization header with Bearer token pattern
Create JwtServiceTests:
- Unit tests for generateToken() and parseToken() methods
- Test valid token generation and parsing
- Test error cases: expired, malformed, and invalid signature tokens
- Use @TestConfiguration for test JWT properties
🚀 Next Steps
🤖 All AI agent prompts combined
Task: 1
Configure Spring Security and JWT dependencies in pom.xml, then add JWT
configuration properties to application.properties.
Add Maven dependencies:
- spring-boot-starter-security
- jjwt-api version 0.13.0 (compile scope)
- jjwt-impl version 0.13.0 (runtime scope)
- jjwt-jackson version 0.13.0 (runtime scope)
Add JWT configuration properties to application.properties:
- jwt.secret.key with environment variable substitution pattern
- jwt.expiration.seconds with default of 3600
- jwt.issuer
===============================================================================
Task: 2
Implement OAuth 2.0 Client Credentials token issuance with JWT generation.
Create DTOs in models package:
- TokenRequest with grant_type, client_id, client_secret fields and Jakarta
validation
- TokenResponse with access_token, token_type, expires_in fields
Create ClientConfig in config package:
- @Configuration class with Map<String, String> bean for client credentials
- Add example client "foobarbaz" with provided secret
Create JwtService in services package:
- Inject JWT properties (secret, expiration, issuer) via @Value
- Implement generateToken(String clientId) using JJWT builder with HMAC-SHA256
- Implement parseToken(String token) using JJWT parser for validation
- Handle JwtException appropriately
Create TokenController in controllers package:
- POST /auth/token endpoint accepting form-urlencoded parameters
- Validate grant_type and client credentials
- Return TokenResponse on success or RFC 6749 error format on failure
===============================================================================
Task: 3
Configure Spring Security with JWT token validation to protect mutating
endpoints while keeping GET endpoints public.
Create JwtAuthenticationFilter in security package:
- Extend OncePerRequestFilter
- Extract Bearer token from Authorization header
- Validate token using JwtService.parseToken()
- Set Authentication in SecurityContextHolder for valid tokens
- Continue filter chain in all cases
Create SecurityConfig in security package:
- @Configuration and @EnableWebSecurity
- Define SecurityFilterChain bean with lambda DSL
- Disable CSRF
- Configure authorization: permit all GET requests, require authentication for
POST/PUT/DELETE, permit /auth/token
- Add JwtAuthenticationFilter before UsernamePasswordAuthenticationFilter
- Set session management to stateless
===============================================================================
Task: 4
Implement custom JSON error responses for authentication failures.
Create JwtAuthenticationEntryPoint in security package:
- Implement AuthenticationEntryPoint interface
- In commence(), return 401 status with JSON body containing "error" and
"error_description" fields
Update SecurityConfig:
- Add exceptionHandling configuration with lambda DSL
- Register JwtAuthenticationEntryPoint
===============================================================================
Task: 5
Create comprehensive tests for token issuance, JWT service, and endpoint
security following existing @WebMvcTest patterns.
Create TokenControllerTests:
- @WebMvcTest(TokenController.class) with mocked JwtService and client store
- Test successful token issuance with valid credentials
- Test error cases: invalid grant_type, invalid credentials, missing parameters
- Follow existing naming pattern with @DisplayName
Create BooksControllerSecurityTests:
- @WebMvcTest(BooksController.class) with @AutoConfigureMockMvc
- Test POST/PUT/DELETE without token returns 401
- Test POST/PUT/DELETE with valid token succeeds
- Test GET remains publicly accessible
- Use Authorization header with Bearer token pattern
Create JwtServiceTests:
- Unit tests for generateToken() and parseToken() methods
- Test valid token generation and parsing
- Test error cases: expired, malformed, and invalid signature tokens
- Use @TestConfiguration for test JWT properties
💡 Iterate on the plan with:
@coderabbitai <feedback>
Example Feedback
- @coderabbitai You can skip phase 3. Add a simple unit test case for phase 2.
- @coderabbitai For assumption 1 go ahead with option 3 and replan.
💬 Have feedback or questions? Drop into our discord!
Description
Secure the API's mutating endpoints (
POST,PUT, andDELETE) by introducing JWT-based authentication following the OAuth 2.0 Client Credentials Flow.In this flow, machine-to-machine communication is secured by issuing JWTs to clients authenticated using a
client_idandclient_secret. These short-lived tokens are then required in theAuthorizationheader for protected endpoints. This ensures only registered clients can perform state-changing operations on the API.sequenceDiagram participant Client as Client (Machine-to-Machine app) participant Server as Server (Spring Boot RESTful API) Note over Client,Server: Step 1 - Obtain JWT Access Token Client->>Server: POST /auth/token (client_id, client_secret) Server-->>Client: 200 OK { access_token, expires_in, token_type } Note over Client,Server: Step 2 - Access Protected Resources Client->>Server: POST /{resource} (Authorization: Bearer {access_token}) Server-->>Client: 201 Created Client->>Server: PUT /{resource}/{id} (Authorization: Bearer {access_token}) Server-->>Client: 204 No Content Client->>Server: DELETE /{resource}/{id} (Authorization: Bearer {access_token}) Server-->>Client: 204 No ContentProposed Solution
Implement JWT issuance and validation using Spring Security and a lightweight in-memory client registry (or configurable store). The JWTs will be signed using a shared secret (HMAC) or an asymmetric key pair (RSA) depending on the security posture.
Key points:
/tokenendpoint for clients to exchange their credentials for a JWT.Suggested Approach
1. Define a configuration for client credentials
2. Token issuance endpoint
3. JWT generation logic
4. Security configuration
5. JWT Authentication Filter
Sample request with
curlAcceptance Criteria
/tokenendpoint exists and accepts validclient_id/client_secretcredentials to return a JWT.sub,iat, andexpclaims at minimum.POST,PUT,DELETE) are protected and require a valid JWT viaAuthorization: Bearer <token>.401 Unauthorized.GETroutes.