对于最新的稳定版本,请使用 Spring Authorization Server 1.4.3! |
作方法:动态注册客户端
本指南介绍如何在 Spring Authorization Server 中配置 OpenID Connect 动态客户端注册,并介绍如何注册客户端的示例。 Spring Authorization Server 实现了 OpenID Connect 动态客户端注册 1.0 规范,提供了动态注册和检索 OpenID Connect 客户端的能力。
启用动态客户端注册
默认情况下,动态 Client 端注册功能在 Spring Authorization Server 中处于禁用状态。 要启用,请添加以下配置:
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 client.create
.
If the access token contains additional scope, the client registration request will be denied.
To obtain encoded credentials for the above request, base64
encode the client credentials in the format of <clientId>:<clientSecret>
.
Below is an encoding operation for the example in this guide.
echo -n "registrar-client:secret" | base64
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.