diff --git a/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc b/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc index 85fc06a30..eefaa74a1 100644 --- a/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc +++ b/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc @@ -23,7 +23,32 @@ To enable, add the following configuration: include::{examples-dir}/main/java/sample/registration/SecurityConfig.java[] ---- -<1> Enable the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration Endpoint] with the default configuration. +<1> Enable the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration Endpoint] with client registration endpoint authentication providers for providing custom metadata. Providing custom metadata is optional. + +In order to accept custom client metadata when registering a client, a few additional implementation details +are necessary. + +[NOTE] +==== +The following example depicts custom metadata `logo_uri` (string type) and `contacts` (string array type) +==== + +Create a set of custom `Converter` classes in order to retain custom client claims. + +[[sample.CustomMetadataConfig]] +[source,java] +---- +include::{examples-dir}/main/java/sample/registration/CustomMetadataConfig.java[] +---- + +<1> Create a `Consumer>` implementation. +<2> Identify custom fields that should be accepted during client registration. +<3> Filter for `OidcClientRegistrationAuthenticationProvider` and `OidcClientConfigurationAuthenticationProvider` instances. +<4> Add a custom registered client `Converter` (implementation in #7) +<5> Add a custom client registration `Converter` to `OidcClientRegistrationAuthenticationProvider` (implementation in #8) +<6> Add a custom client registration `Converter` to `OidcClientConfigurationAuthenticationProvider` (implementation in #8) +<7> Custom registered client `Converter` implementation that adds custom claims to registered client settings. +<8> Custom client registration `Converter` implementation that modifies client registration claims with custom metadata. [[configure-client-registrar]] == Configure client registrar @@ -89,8 +114,8 @@ After the client is registered, the access token is invalidated. include::{examples-dir}/main/java/sample/registration/ClientRegistrar.java[] ---- -<1> A minimal representation of a client registration request. You may add additional client metadata parameters as per https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration Request]. -<2> A minimal representation of a client registration response. You may add additional client metadata parameters as per https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse[Client Registration Response]. +<1> A minimal representation of a client registration request. You may add additional client metadata parameters as per https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration Request]. This example request contains custom metadata fields `logo_uri` and `contacts`. +<2> A minimal representation of a client registration response. You may add additional client metadata parameters as per https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse[Client Registration Response]. This example response contains custom metadata fields `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. diff --git a/docs/src/main/java/sample/registration/ClientRegistrar.java b/docs/src/main/java/sample/registration/ClientRegistrar.java index f199a4357..942387f12 100644 --- a/docs/src/main/java/sample/registration/ClientRegistrar.java +++ b/docs/src/main/java/sample/registration/ClientRegistrar.java @@ -39,6 +39,8 @@ public record ClientRegistrationRequest( // <1> @JsonProperty("client_name") String clientName, @JsonProperty("grant_types") List grantTypes, @JsonProperty("redirect_uris") List redirectUris, + @JsonProperty("logo_uri") String logoUri, + List contacts, String scope) { } @@ -50,6 +52,8 @@ public record ClientRegistrationResponse( // <2> @JsonProperty("client_secret") String clientSecret, @JsonProperty("grant_types") List grantTypes, @JsonProperty("redirect_uris") List redirectUris, + @JsonProperty("logo_uri") String logoUri, + List contacts, String scope) { } @@ -58,6 +62,8 @@ public void exampleRegistration(String initialAccessToken) { // <3> "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" ); @@ -72,6 +78,10 @@ public void exampleRegistration(String initialAccessToken) { // <3> 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")); // <6> + 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(); @@ -85,6 +95,10 @@ public void exampleRegistration(String initialAccessToken) { // <3> 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()); } diff --git a/docs/src/main/java/sample/registration/CustomMetadataConfig.java b/docs/src/main/java/sample/registration/CustomMetadataConfig.java new file mode 100644 index 000000000..874b3c555 --- /dev/null +++ b/docs/src/main/java/sample/registration/CustomMetadataConfig.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.registration; + +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; + +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; + +public class CustomMetadataConfig { + public static Consumer> registeredClientConverters() { + List customClientMetadata = List.of("logo_uri", "contacts"); // <1> + + return authenticationProviders -> // <2> + { + CustomRegisteredClientConverter registeredClientConverter = new CustomRegisteredClientConverter(customClientMetadata); + CustomClientRegistrationConverter clientRegistrationConverter = new CustomClientRegistrationConverter(customClientMetadata); + + authenticationProviders.forEach(authenticationProvider -> { + if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) { // <3> + provider.setRegisteredClientConverter(registeredClientConverter); // <4> + provider.setClientRegistrationConverter(clientRegistrationConverter); // <5> + } + + if (authenticationProvider instanceof OidcClientConfigurationAuthenticationProvider provider) { + provider.setClientRegistrationConverter(clientRegistrationConverter); // <6> + } + }); + }; + } + + static class CustomRegisteredClientConverter implements Converter { // <7> + private final List customMetadata; + + private final OidcClientRegistrationRegisteredClientConverter delegate; + + CustomRegisteredClientConverter(List customMetadata) { + this.customMetadata = customMetadata; + this.delegate = new OidcClientRegistrationRegisteredClientConverter(); + } + + public RegisteredClient convert(OidcClientRegistration clientRegistration) { + RegisteredClient convertedClient = delegate.convert(clientRegistration); + ClientSettings.Builder clientSettingsBuilder = ClientSettings + .withSettings(convertedClient.getClientSettings().getSettings()); + + if (!CollectionUtils.isEmpty(this.customMetadata)) { + clientRegistration.getClaims().forEach((claim, value) -> { + if (this.customMetadata.contains(claim)) { + clientSettingsBuilder.setting(claim, value); + } + }); + } + + return RegisteredClient.from(convertedClient).clientSettings(clientSettingsBuilder.build()).build(); + } + } + + static class CustomClientRegistrationConverter implements Converter { // <8> + private final List customMetadata; + + private final RegisteredClientOidcClientRegistrationConverter delegate; + + CustomClientRegistrationConverter(List customMetadata) { + this.customMetadata = customMetadata; + this.delegate = new RegisteredClientOidcClientRegistrationConverter(); + } + + public OidcClientRegistration convert(RegisteredClient registeredClient) { + var clientRegistration = delegate.convert(registeredClient); + Map claims = new HashMap<>(clientRegistration.getClaims()); + if (!CollectionUtils.isEmpty(customMetadata)) { + ClientSettings clientSettings = registeredClient.getClientSettings(); + + claims.putAll(customMetadata.stream() + .filter(metadatum -> clientSettings.getSetting(metadatum) != null) + .collect(Collectors.toMap(Function.identity(), clientSettings::getSetting))); + } + return OidcClientRegistration.withClaims(claims).build(); + } + } + +} diff --git a/docs/src/main/java/sample/registration/SecurityConfig.java b/docs/src/main/java/sample/registration/SecurityConfig.java index b55be7e76..247edd7a8 100644 --- a/docs/src/main/java/sample/registration/SecurityConfig.java +++ b/docs/src/main/java/sample/registration/SecurityConfig.java @@ -24,6 +24,8 @@ import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.web.SecurityFilterChain; +import static sample.registration.CustomMetadataConfig.registeredClientConverters; + @Configuration @EnableWebSecurity public class SecurityConfig { @@ -31,10 +33,13 @@ public class SecurityConfig { @Bean public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) - .oidc(oidc -> oidc.clientRegistrationEndpoint(Customizer.withDefaults())); // <1> - http.oauth2ResourceServer(oauth2ResourceServer -> - oauth2ResourceServer.jwt(Customizer.withDefaults())); + .oidc(oidc -> oidc.clientRegistrationEndpoint(endpoint -> { + endpoint.authenticationProviders(registeredClientConverters()); // <1> + })); + + http.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(Customizer.withDefaults())); return http.build(); } diff --git a/docs/src/test/java/sample/registration/DynamicClientRegistrationTests.java b/docs/src/test/java/sample/registration/DynamicClientRegistrationTests.java index 41874e751..eb4cdcb4d 100644 --- a/docs/src/test/java/sample/registration/DynamicClientRegistrationTests.java +++ b/docs/src/test/java/sample/registration/DynamicClientRegistrationTests.java @@ -56,7 +56,7 @@ public class DynamicClientRegistrationTests { private String port; @Test - public void dynamicallyRegisterClient() throws Exception { + public void dynamicallyRegisterClientWithCustomMetadata() throws Exception { MockHttpServletResponse tokenResponse = this.mvc.perform(post("/oauth2/token") .with(httpBasic("registrar-client", "secret")) .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())