A robust OAuth2 implementation for the Golf Academy application using Spring Boot 3.3+. This project demonstrates a complete OAuth2 setup with an authorization server, resource server, and client applications.
In particular this application is showing off the new RestClient support for OAuth2 in Spring Security 6.4.
https://spring.io/blog/2024/10/28/restclient-support-for-oauth2-in-spring-security-6-4
The project consists of three main components:
- Authorization Server (Port 9000) - Handles authentication and issues OAuth2 tokens
- Resource Server (Port 8081) - Provides protected golf lesson endpoints
- Client Applications:
- OAuth2 Client (Spring Security implementation)
- No-Auth Client (RestClient without Authorization)
graph TD
A[Client Application] -->|OAuth2 Token Request| B[Authorization Server]
B -->|Issues Token| A
A -->|Request with Token| C[Resource Server]
C -->|Validates Token with| B
C -->|Returns Protected Resources| A
- Java 23
- Spring Boot 3.3.5+
- Maven 3.6+
- Spring Security 6.4+
- OAuth2 Authorization Server implementation
- JWT token-based authentication
- Resource server with protected endpoints
- Client credential flow implementation
- RestClient with OAuth2 support
- Start the authorization server:
cd authorization-server
./mvnw spring-boot:run
The server will start on port 9000 with the following configuration:
spring:
security:
oauth2:
authorizationserver:
issuer: http://localhost:9000
- Start the resource server:
cd resource-server
./mvnw spring-boot:run
The resource server runs on port 8081 and is configured to validate tokens with the authorization server:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000
Configuration example:
spring:
security:
oauth2:
client:
registration:
golf-client:
client-id: golf-client
client-secret: golf-secret
authorization-grant-type: client_credentials
scope: read
To access protected resources:
@RestController
public class LessonsController {
private final RestClient restClient;
@GetMapping("/lessons")
public String fetchLessons() {
return restClient.get()
.uri("http://localhost:8081/lessons")
.attributes(clientRegistrationId("golf-client"))
.retrieve()
.body(String.class);
}
}
The authorization server is configured with in-memory client registration:
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("golf-client")
.clientSecret(passwordEncoder().encode("golf-secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
The resource server is configured to require authentication for all requests:
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
GET /lessons
- Retrieves available golf lessons
Example response:
[
{
"title": "Beginner Golf Basics",
"description": "An introduction to the fundamentals of golf.",
"instructor": "John Doe",
"schedule": "2024-11-05T10:00:00"
}
]
The project includes JUnit tests for each component. Run tests using:
./mvnw test
The client applications include comprehensive error handling for OAuth2-related issues:
.defaultStatusHandler(HttpStatusCode::is4xxClientError, (request, response) -> {
if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Unauthorized access to lessons API");
}
throw new ResponseStatusException(response.getStatusCode(),
"Client error occurred");
})