1. Introduction
The Spring Authorization Server comes with a range of sensible defaults that allow us to use it with almost no configuration. This makes it a great choice for using with client applications in test scenarios and when we want to have full control of the user’s login experience.
One feature, although available, is not enabled by default: Dynamic Client Registration.
In this tutorial, we’ll show how to enable and use it from a client application.
2. Why Use Dynamic Registration?
When an OAuth2-based application client or, in OIDC parlance, a relying party (RP) starts an authentication flow, it sends the authorization server its own client identifier to the Identity Provider.
This identifier, in general, is issued to the client using an out-of-band process, which will then add it to the configuration and be used when needed.
For instance, when using popular Identity Provider solutions such as Azure’s EntraID or Auth0, we can use the admin console or APIs to provision a new client. In the process, we’ll need to inform the application name, authorized callback URLs, supported scopes, etc.
Once we’ve supplied the required information, we’ll end up with a new client identifier and, for the so-called “secret” clients, a client secret. We then add these to the application’s configuration, and we are ready to deploy it.
Now, this process works fine when we have a small set of applications, or when we always use a single Identity Provider. For more complex scenarios, though, the registration process needs to be dynamic, and this is where the OpenID Connect Dynamic Client Registration specification comes into play.
For a real-world case, a good example is the UK’s OpenBanking standard, which uses dynamic client registration as one of its core protocols.
3. How Does Dynamic Registration Work?
The OpenID Connect standard uses a single registration URL that clients use to register themselves. This is done with a POST request with a JSON object that has the client metadata required to perform the registration.
Importantly, access to the registration endpoint requires authentication, usually a Bearer token. This, of course, begs the question: how does a wannabe client get a token for this operation?
Unfortunately, the answer is unclear. On one hand, the spec says that the endpoint is a protected resource and, as such, requires some form of authentication. On the other hand, it also mentions the possibility of an open registration endpoint.
For the Spring Authorization Server, the registration requires a bearer token with the client.create scope. To create this token, we use the regular OAuth2’s token endpoint and basic credentials.
This is the resulting sequence for a successful registration:
Once the client completes a successful registration, it can use the returned client id and secret to execute any standard authorization flows.
4. Implementing Dynamic Registration
Now that we understand the required steps let’s create a test scenario using two Spring Boot applications. One will host the Spring Authorization Server, and the other will be a simple WebMVC application that uses the Spring Security Outh2 login starter module.
Instead of using the regular static configuration for clients, the latter will use the dynamic registration endpoint to acquire a client identifier and secret at startup time.
Let’s start with the server.
5. Authorization Server Implementation
We’ll start by adding the required maven dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>1.3.1</version>
</dependency>
The latest version is available on Maven Central.
For a regular Spring Authorization Server application, this dependency would be all we needed. However, for security reasons, dynamic registration is not enabled by default. Also, as of this writing, there’s no way to enable it just using configuration properties.
This means we must add some code – finally.
5.1. Enabling Dynamic Registration
The OAuth2AuthorizationServerConfigurer is the doorway to configure all aspects of the Authorization Server, including the registration endpoint. This configuration should be done as part of the creation of a SecurityFilterChain bean:
@Configuration
@EnableConfigurationProperties(SecurityConfig.RegistrationProperties.class)
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> {
oidc.clientRegistrationEndpoint(Customizer.withDefaults());
});
http.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
http.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
// ... other beans omitted
}
Here, we use the server’s configurer oidc() method to get access to the OidcConfigurer instance. This sub-configurer has methods that allow us to control the endpoints related to the OpenID Connect standard. To enable the registration endpoint, we use the clientRegististrationEndpoint() method with the default configuration. This will enable registration at the /connect/register path, using bearer token authorization. Further configuration options include:
- Defining custom authentication
- Custom processing of the received registration data
- Custom processing of the response sent to the client
Now, since we’re providing a custom SecurityFilterChain, Spring Boot’s auto-configuration will step back, leaving us responsible for adding some extra bits to the configuration.
In particular, we need to add the logic to setup form login authentication:
@Bean
@Order(2)
SecurityFilterChain loginFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(r -> r.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
5.2. Registration Client Configuration
As mentioned above, the registration mechanism itself requires the client to send a bearer token. Spring Authorization Server solves this chicken-and-egg problem by requiring clients to use a client credentials flow to generate this token.
The required scope for this token request is client.create and the client must use one of the supported authentication schemes supported by the server. Here, we’ll use Basic credentials, but, in a real-world scenario, we can use other methods.
This registration client is, from the Authorization Server’s point of view, just another client. As such we’ll create it using the RegisteredClient fluent API:
@Bean
public RegisteredClientRepository registeredClientRepository(RegistrationProperties props) {
RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(props.getRegistrarClientId())
.clientSecret(props.getRegistrarClientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientSettings(ClientSettings.builder()
.requireProofKey(false)
.requireAuthorizationConsent(false)
.build())
.scope("client.create")
.scope("client.read")
.build();
RegisteredClientRepository delegate = new InMemoryRegisteredClientRepository(registrarClient);
return new CustomRegisteredClientRepository(delegate);
}
We’ve used a @ConfigurationProperties class to allow configuring the client ID and secret properties using Spring’s standard Environment mechanism.
This bootstrap registration will be the only one created at startup time. We’ll add it to our custom RegisteredClientRepository before returning it.
5.3. Custom RegisteredClientRepository
Spring Authorization Server uses the configured RegisteredClientRepository implementation to store all registered clients in the server. Out-of-the-box, it comes with memory and JDBC-based implementations, which cover the basic use cases.
Those implementations, however, do not offer any capabilities in terms of customizing the registration before it is saved. In our case, we’d like to modify the default ClientProperties settings so no consent or PKCE will be needed when authorizing a user.
Our implementation delegates most methods to the actual repository passed at construction time. The important exception is the save() method:
@Override
public void save(RegisteredClient registeredClient) {
Set<String> scopes = ( registeredClient.getScopes() == null || registeredClient.getScopes().isEmpty())?
Set.of("openid","email","profile"):
registeredClient.getScopes();
// Disable PKCE & Consent
RegisteredClient modifiedClient = RegisteredClient.from(registeredClient)
.scopes(s -> s.addAll(scopes))
.clientSettings(ClientSettings
.withSettings(registeredClient.getClientSettings().getSettings())
.requireAuthorizationConsent(false)
.requireProofKey(false)
.build())
.build();
delegate.save(modifiedClient);
}
Here, we create a new RegisteredClient based on the received one, changing the ClientSettings as needed. This new registration is then passed to the backend where it will be stored until needed.
This concludes the server implementation. Now, let’s move on to the client side
6. Dynamic Registration Client Implementation
Our client will also be a standard Spring Web MVC application, with a single page displaying current user information. Spring Security, or, more specifically, its OAuth2 Login module, will handle all security aspects.
Let’s start with the required Maven dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>3.3.2</version>
</dependency>
The latest versions of these dependencies are available on Maven Central:
6.1. Security Configuration
By default, SpringBoot ‘s auto-configuration mechanism uses information from the available PropertySources to collect the required data to create one or more ClientRegistration instances, which are then stored in a memory-based ClientRegistrationRepository.
For instance, given this application.yaml:
spring:
security:
oauth2:
client:
provider:
spring-auth-server:
issuer-uri: http://localhost:8080
registration:
test-client:
provider: spring-auth-server
client-name: test-client
client-id: xxxxx
client-secret: yyyy
authorization-grant-type:
- authorization_code
- refresh_token
- client_credentials
scope:
- openid
- email
- profile
Spring will create a ClientRegistration named test-client and pass it to the repository.
Later, when there’s a need to start an authentication flow, the OAuth2 engine queries this repository and recovers the registration by its registration identifier – test-client, in our case.
The key point here is that the authorization server should already know the ClientRegistration returned at this point. This implies that to support dynamic clients, we must implement an alternative repository and expose it as a @Bean.
By doing so, Spring Boot’s auto-configuration will automatically use it instead of the default one.
6.2. Dynamic Client Registration Repository
As expected, our implementation must implement the ClientRegistration interface, which contains just a single method: findByRegistrationId(). This raises a question: How does the OAuth2 engine know which registrations are available? After all, it can list them on the default login page.
As it turns out, Spring Security expects the repository to also implement Iterable
public class DynamicClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {
private final RegistrationDetails registrationDetails;
private final Map<String, ClientRegistration> staticClients;
private final RegistrationRestTemplate registrationClient;
private final Map<String, ClientRegistration> registrations = new HashMap<>();
// ... implementation omitted
}
Our class requires a few inputs to work:
- a RegistrationDetails record with all parameters required to perform the dynamic registration
- a Map of clients that will be dynamically registered
- a RestTemplate used to access the authorization server
Notice that, for this example, we assume that all clients will be registered on the same Authorization Server.
Another important design decision is to define when the dynamic registration will take place. Here, we’ll take a simplistic approach and expose a public doRegistrations() method that will register all known clients and save the returned client identifier and secret for later use:
public void doRegistrations() {
staticClients.forEach((key, value) -> findByRegistrationId(key));
}
The implementation calls findByRegistrationId() for each static client passed to the constructor. This method checks if there’s a valid registration for the given identifier and, in case it is missing, triggers the actual registration process.
6.3. Dynamic Registration
The doRegistration() function is where the real action happens:
private ClientRegistration doRegistration(String registrationId) {
String token = createRegistrationToken();
var staticRegistration = staticClients.get(registrationId);
var body = Map.of(
"client_name", staticRegistration.getClientName(),
"grant_types", List.of(staticRegistration.getAuthorizationGrantType()),
"scope", String.join(" ", staticRegistration.getScopes()),
"redirect_uris", List.of(resolveCallbackUri(staticRegistration)));
var headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
var request = new RequestEntity<>(
body,
headers,
HttpMethod.POST,
registrationDetails.registrationEndpoint());
var response = registrationClient.exchange(request, ObjectNode.class);
// ... error handling omitted
return createClientRegistration(staticRegistration, response.getBody());
}
Firstly, we must get a registration token that we need to call the registration endpoint. Notice that we must get a new token for every registration attempt since, as described in Spring Authorization’s Server documentation, we can only use this token once.
Next, we build the registration payload using data from the static registration object, add the required authorization and content-type headers, and send the request to the registration endpoint.
Finally, we use the response data to create the final ClientRegistration that will be saved in the repository’s cache and returned to the OAuth2 engine.
6.4. Registering the Dynamic Repository @Bean
To complete our client, the last required step is to expose our DynamicClientRegistrationRepository as a @Bean. Let’s create a @Configuration class for that:
@Bean
ClientRegistrationRepository dynamicClientRegistrationRepository( DynamicClientRegistrationRepository.RegistrationRestTemplate restTemplate) {
var registrationDetails = new DynamicClientRegistrationRepository.RegistrationDetails(
registrationProperties.getRegistrationEndpoint(),
registrationProperties.getRegistrationUsername(),
registrationProperties.getRegistrationPassword(),
registrationProperties.getRegistrationScopes(),
registrationProperties.getGrantTypes(),
registrationProperties.getRedirectUris(),
registrationProperties.getTokenEndpoint());
Map<String,ClientRegistration> staticClients = (new OAuth2ClientPropertiesMapper(clientProperties)).asClientRegistrations();
var repo = new DynamicClientRegistrationRepository(registrationDetails, staticClients, restTemplate);
repo.doRegistrations();
return repo;
}
The @Bean-annotated dynamicClientRegistrationRepository() method creates the repository by first populating the RegistrationDetails record from available properties.
Secondly, it creates the staticClient map leveraging the OAuth2ClientPropertiesMapper class available in SpringBoot’s auto-configuration module. This approach allows us to quickly switch from static to dynamic clients and back with minimal effort, as the configuration structure is the same for both.
7. Testing
Finally, let’s do some integration testing. Firstly, we start the server application, which is configured to listen on port 8080:
[ server ] $ mvn spring-boot:run
... lots of messages omitted
[ main] c.b.s.s.a.AuthorizationServerApplication : Started AuthorizationServerApplication in 2.222 seconds (process running for 2.454)
[ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state LivenessState changed to CORRECT
[ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state ReadinessState changed to ACCEPTING_TRAFFIC
Next, it’s time to start the client in another shell:
[client] $ mvn spring-boot:run
// ... lots of messages omitted
[ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
[ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8090 (http) with context path ''
[ restartedMain] d.c.DynamicRegistrationClientApplication : Started DynamicRegistrationClientApplication in 2.063 seconds (process running for 2.425)
Both applications run with the debug property set, so they produce quite a lot of log messages. In particular, we can see a call to the authorization server’s /connect/register endpoint:
[nio-8080-exec-3] o.s.security.web.FilterChainProxy : Securing POST /connect/register
// ... lots of messages omitted
[nio-8080-exec-3] ClientRegistrationAuthenticationProvider : Retrieved authorization with initial access token
[nio-8080-exec-3] ClientRegistrationAuthenticationProvider : Validated client registration request parameters
[nio-8080-exec-3] s.s.a.r.CustomRegisteredClientRepository : Saving registered client: id=30OTlhO1Fb7UF110YdXULEDbFva4Uc8hPBGMfi60Wik, name=test-client
On the client side, we can see a message with the registration identifier (test-client) and the corresponding client_id:
[ restartedMain] s.d.c.c.OAuth2DynamicClientConfiguration : Creating a dynamic client registration repository
[ restartedMain] .c.s.DynamicClientRegistrationRepository : findByRegistrationId: test-client
[ restartedMain] .c.s.DynamicClientRegistrationRepository : doRegistration: registrationId=test-client
[ restartedMain] .c.s.DynamicClientRegistrationRepository : creating ClientRegistration: registrationId=test-client, client_id=30OTlhO1Fb7UF110YdXULEDbFva4Uc8hPBGMfi60Wik
If we open a browser and point it to http://localhost:8090, we’ll be redirected to the login page. Notice that the URL in the address bar changed to http://localhost:8080, which shows us that this page came from the authorization server.
The test credentials are user1/password. Once we put them in the form and send it, we’ll return to the client’s home page. Since we’re now authenticated, we’ll see a page containing some details extracted from the authorization token.
8. Conclusion
In this tutorial, we’ve shown how to enable the Spring Authorization Server’s Dynamic Registration feature and use it from a Spring Security-based client application.
As usual, all code is available over on GitHub.