Introduction
OAuth2 has become the industry standard for secure authorization in web applications. Spring Boot simplifies OAuth2 integration, allowing developers to handle client registration effortlessly. However, Spring Boot’s default implementation often requires client registrations to be predefined in application configuration, making it difficult to adapt to changing environments where client registrations need to be added or removed dynamically.
In this blog post, we’ll explore how to overcome this limitation by creating a custom implementation of ClientRegistrationRepository
that allows dynamic management of OAuth2 clients at runtime. This approach provides the flexibility required for modern applications that need to adapt to varying OAuth2 client configurations.
Understanding Spring Boot’s Default Client Registration
Spring Boot manages OAuth2 client registrations using the ClientRegistrationRepository
interface, which provides a way to store and retrieve client configurations. By default, Spring Boot offers an InMemoryClientRegistrationRepository
implementation. This default implementation stores client registrations in memory, as defined in the application’s configuration files.
While this approach works well for static configurations, it has a significant limitation: the client registrations are immutable. Once registered, a client’s configuration cannot be altered. This is because InMemoryClientRegistrationRepository
creates an unmodifiable map of client registrations:
public InMemoryClientRegistrationRepository(List < ClientRegistration > registrations) {
this(createRegistrationsMap(registrations));
}
private static Map < String, ClientRegistration > createRegistrationsMap(List < ClientRegistration > registrations) {
Assert.notEmpty(registrations, "registrations cannot be empty");
return toUnmodifiableConcurrentMap(registrations);
}
private static Map < String, ClientRegistration > toUnmodifiableConcurrentMap(List < ClientRegistration > registrations) {
ConcurrentHashMap < String, ClientRegistration > result = new ConcurrentHashMap < > ();
for (ClientRegistration registration: registrations) {
Assert.state(!result.containsKey(registration.getRegistrationId()),
() - > String.format("Duplicate key %s", registration.getRegistrationId()));
result.put(registration.getRegistrationId(), registration);
}
return Collections.unmodifiableMap(result);
}
This immutability is by design, ensuring that client registrations remain consistent throughout the application’s lifecycle. However, for applications that require dynamic updates to client registrations—such as SaaS platforms or systems with evolving client needs—this default behavior is insufficient.
Custom Implementation of ClientRegistrationRepository
To support dynamic client registration, we need to implement a custom version of ClientRegistrationRepository
that allows adding and removing clients at runtime. This custom repository will manage a modifiable collection of client registrations.
Here’s how you can create a custom ClientRegistrationRepository
:
public class DynamicClientRegistrationRepository implements ClientRegistrationRepository {
private final ConcurrentHashMap < String, ClientRegistration > clientRegistrations = new ConcurrentHashMap < > ();
public void addClientRegistration(String registrationId, ClientRegistration registration) {
clientRegistrations.put(registrationId, registration);
}
@Override
public ClientRegistration findByRegistrationId(String registrationId) {
return clientRegistrations.get(registrationId);
}
}
This implementation uses a ConcurrentHashMap
to store client registrations, enabling thread-safe access and modification. The addClientRegistration
and removeClientRegistration
methods allow for dynamic management of client registrations, which can be crucial for applications that need to adapt in real time.
clientRegistrationRepository.addClientRegistration(clientRegistration.getRegistrationId(), clientRegistration);
Integrating Custom ClientRegistrationRepository with Spring Security
After implementing the custom ClientRegistrationRepository
, the next step is to integrate it into your Spring Boot application. This involves replacing the default InMemoryClientRegistrationRepository
with your custom implementation.
Here’s how to inject your custom repository into the Spring Security configuration:
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
clientRegistrationRepository = new DynamicClientRegistrationRepository();
clientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
var clientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, clientService);
clientManager.setAuthorizedClientProvider(authorizedClientProvider);
By defining the custom ClientRegistrationRepository
as a bean, Spring Boot will automatically use it in the OAuth2 flow, allowing for dynamic client management.
Acquiring Access Tokens Using Dynamic Client Registrations
Once you have dynamically registered clients, you can proceed with acquiring access tokens using the standard OAuth2 flow. The process remains unchanged, but now includes the capability to work with clients that were registered dynamically.
Here’s an example of acquiring an access token:
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(oauth2Config.getClientId())
.attribute(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue(), clientRegistration)
.build();
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
authorizedClient.getAccessToken().getTokenValue() //-> get token here