For the latest stable version, please use Spring Authorization Server 1.4.1! |
How-to: Register a client dynamically
This guide shows how to configure OpenID Connect Dynamic Client Registration in Spring Authorization Server and walks through an example of how to register a client. Spring Authorization Server implements the OpenID Connect Dynamic Client Registration 1.0 specification, providing the capability to dynamically register and retrieve OpenID Connect clients.
Enable Dynamic Client Registration
By default, dynamic client registration functionality is disabled in Spring Authorization Server. To enable, add the following configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import static sample.registration.CustomClientMetadataConfig.configureCustomClientMetadataConverters;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> oidc.clientRegistrationEndpoint(clientRegistrationEndpoint -> { (1)
clientRegistrationEndpoint
.authenticationProviders(configureCustomClientMetadataConverters()); (2)
}));
http.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
}
1 | Enable the OpenID Connect 1.0 Client Registration Endpoint with the default configuration. |
2 | Optionally, customize the default AuthenticationProvider 's to support custom client metadata parameters. |
In order to support custom client metadata parameters when registering a client, a few additional implementation details are required.
The following example shows a sample implementation of Converter
's that support custom client metadata parameters (logo_uri
and contacts
) and are configured in OidcClientRegistrationAuthenticationProvider
and OidcClientConfigurationAuthenticationProvider
.
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.util.CollectionUtils;
public class CustomClientMetadataConfig {
public static Consumer<List<AuthenticationProvider>> configureCustomClientMetadataConverters() { (1)
List<String> customClientMetadata = List.of("logo_uri", "contacts"); (2)
return (authenticationProviders) -> {
CustomRegisteredClientConverter registeredClientConverter =
new CustomRegisteredClientConverter(customClientMetadata);
CustomClientRegistrationConverter clientRegistrationConverter =
new CustomClientRegistrationConverter(customClientMetadata);
authenticationProviders.forEach((authenticationProvider) -> {
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
provider.setRegisteredClientConverter(registeredClientConverter); (3)
provider.setClientRegistrationConverter(clientRegistrationConverter); (4)
}
if (authenticationProvider instanceof OidcClientConfigurationAuthenticationProvider provider) {
provider.setClientRegistrationConverter(clientRegistrationConverter); (5)
}
});
};
}
private static class CustomRegisteredClientConverter
implements Converter<OidcClientRegistration, RegisteredClient> {
private final List<String> customClientMetadata;
private final OidcClientRegistrationRegisteredClientConverter delegate;
private CustomRegisteredClientConverter(List<String> customClientMetadata) {
this.customClientMetadata = customClientMetadata;
this.delegate = new OidcClientRegistrationRegisteredClientConverter();
}
@Override
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
RegisteredClient registeredClient = this.delegate.convert(clientRegistration);
ClientSettings.Builder clientSettingsBuilder = ClientSettings.withSettings(
registeredClient.getClientSettings().getSettings());
if (!CollectionUtils.isEmpty(this.customClientMetadata)) {
clientRegistration.getClaims().forEach((claim, value) -> {
if (this.customClientMetadata.contains(claim)) {
clientSettingsBuilder.setting(claim, value);
}
});
}
return RegisteredClient.from(registeredClient)
.clientSettings(clientSettingsBuilder.build())
.build();
}
}
private static class CustomClientRegistrationConverter
implements Converter<RegisteredClient, OidcClientRegistration> {
private final List<String> customClientMetadata;
private final RegisteredClientOidcClientRegistrationConverter delegate;
private CustomClientRegistrationConverter(List<String> customClientMetadata) {
this.customClientMetadata = customClientMetadata;
this.delegate = new RegisteredClientOidcClientRegistrationConverter();
}
@Override
public OidcClientRegistration convert(RegisteredClient registeredClient) {
OidcClientRegistration clientRegistration = this.delegate.convert(registeredClient);
Map<String, Object> claims = new HashMap<>(clientRegistration.getClaims());
if (!CollectionUtils.isEmpty(this.customClientMetadata)) {
ClientSettings clientSettings = registeredClient.getClientSettings();
claims.putAll(this.customClientMetadata.stream()
.filter(metadata -> clientSettings.getSetting(metadata) != null)
.collect(Collectors.toMap(Function.identity(), clientSettings::getSetting)));
}
return OidcClientRegistration.withClaims(claims).build();
}
}
}
1 | Define a Consumer<List<AuthenticationProvider>> providing the ability to customize the default AuthenticationProvider 's. |
2 | Define custom client metadata parameters that are supported for client registration. |
3 | Configure OidcClientRegistrationAuthenticationProvider.setRegisteredClientConverter() with a CustomRegisteredClientConverter . |
4 | Configure OidcClientRegistrationAuthenticationProvider.setClientRegistrationConverter() with a CustomClientRegistrationConverter . |
5 | Configure OidcClientConfigurationAuthenticationProvider.setClientRegistrationConverter() with a CustomClientRegistrationConverter . |
Configure client registrar
An existing client is used to register new clients with the authorization server.
The client must be configured with scopes client.create
and optionally client.read
for registering clients and retrieving clients, respectively.
The following listing shows an example client:
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@Configuration
public class ClientConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("registrar-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) (1)
.scope("client.create") (2)
.scope("client.read") (3)
.build();
return new InMemoryRegisteredClientRepository(registrarClient);
}
}
1 | client_credentials grant type is configured to obtain access tokens directly. |
2 | client.create scope is configured to allow the client to register a new client. |
3 | client.read scope is configured to allow the client to retrieve a registered client. |
Obtain initial access token
An "initial" access token is required for the client registration request.
The access token request MUST contain the scope
parameter value client.create
only.
POST /oauth2/token HTTP/1.1
Authorization: Basic <base64-encoded-credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=client.create
The client registration request requires an access token with a single scope of |
To obtain encoded credentials for the above request,
|
Register a client
With an access token obtained from the previous step, a client can now be dynamically registered.
The "initial" access token can only be used once. After the client is registered, the access token is invalidated. |
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.web.reactive.function.client.WebClient;
public class ClientRegistrar {
private final WebClient webClient;
public ClientRegistrar(WebClient webClient) {
this.webClient = webClient;
}
public record ClientRegistrationRequest( (1)
@JsonProperty("client_name") String clientName,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
@JsonProperty("logo_uri") String logoUri,
List<String> contacts,
String scope) {
}
public record ClientRegistrationResponse( (2)
@JsonProperty("registration_access_token") String registrationAccessToken,
@JsonProperty("registration_client_uri") String registrationClientUri,
@JsonProperty("client_name") String clientName,
@JsonProperty("client_id") String clientId,
@JsonProperty("client_secret") String clientSecret,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
@JsonProperty("logo_uri") String logoUri,
List<String> contacts,
String scope) {
}
public void exampleRegistration(String initialAccessToken) { (3)
ClientRegistrationRequest clientRegistrationRequest = new ClientRegistrationRequest( (4)
"client-1",
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
List.of("https://client.example.org/callback", "https://client.example.org/callback2"),
"https://client.example.org/logo",
List.of("contact-1", "contact-2"),
"openid email profile"
);
ClientRegistrationResponse clientRegistrationResponse =
registerClient(initialAccessToken, clientRegistrationRequest); (5)
assert (clientRegistrationResponse.clientName().contentEquals("client-1")); (6)
assert (!Objects.isNull(clientRegistrationResponse.clientSecret()));
assert (clientRegistrationResponse.scope().contentEquals("openid profile email"));
assert (clientRegistrationResponse.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback"));
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback2"));
assert (!clientRegistrationResponse.registrationAccessToken().isEmpty());
assert (!clientRegistrationResponse.registrationClientUri().isEmpty());
assert (clientRegistrationResponse.logoUri().contentEquals("https://client.example.org/logo"));
assert (clientRegistrationResponse.contacts().size() == 2);
assert (clientRegistrationResponse.contacts().contains("contact-1"));
assert (clientRegistrationResponse.contacts().contains("contact-2"));
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); (7)
String registrationClientUri = clientRegistrationResponse.registrationClientUri();
ClientRegistrationResponse retrievedClient = retrieveClient(registrationAccessToken, registrationClientUri); (8)
assert (retrievedClient.clientName().contentEquals("client-1")); (9)
assert (!Objects.isNull(retrievedClient.clientId()));
assert (!Objects.isNull(retrievedClient.clientSecret()));
assert (retrievedClient.scope().contentEquals("openid profile email"));
assert (retrievedClient.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
assert (retrievedClient.redirectUris().contains("https://client.example.org/callback"));
assert (retrievedClient.redirectUris().contains("https://client.example.org/callback2"));
assert (retrievedClient.logoUri().contentEquals("https://client.example.org/logo"));
assert (retrievedClient.contacts().size() == 2);
assert (retrievedClient.contacts().contains("contact-1"));
assert (retrievedClient.contacts().contains("contact-2"));
assert (Objects.isNull(retrievedClient.registrationAccessToken()));
assert (!retrievedClient.registrationClientUri().isEmpty());
}
public ClientRegistrationResponse registerClient(String initialAccessToken, ClientRegistrationRequest request) { (10)
return this.webClient
.post()
.uri("/connect/register")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(initialAccessToken))
.body(Mono.just(request), ClientRegistrationRequest.class)
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}
public ClientRegistrationResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { (11)
return this.webClient
.get()
.uri(registrationClientUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(registrationAccessToken))
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}
}
1 | A minimal representation of a client registration request. You may add additional client metadata parameters as per Client Registration Request. This example request contains custom client metadata parameters logo_uri and contacts . |
2 | A minimal representation of a client registration response. You may add additional client metadata parameters as per Client Registration Response. This example response contains custom client metadata parameters logo_uri and contacts . |
3 | Example demonstrating client registration and client retrieval. |
4 | A sample client registration request object. |
5 | Register the client using the "initial" access token and client registration request object. |
6 | After successful registration, assert on the client metadata parameters that should be populated in the response. |
7 | Extract registration_access_token and registration_client_uri response parameters, for use in retrieval of the newly registered client. |
8 | Retrieve the client using the registration_access_token and registration_client_uri . |
9 | After client retrieval, assert on the client metadata parameters that should be populated in the response. |
10 | Sample Client Registration Request using WebClient . |
11 | Sample Client Read Request using WebClient . |
The Client Read Response should contain the same client metadata parameters as the Client Registration Response, except the registration_access_token parameter.
|