From 1358a04d14d1bf4f6fef55ad01b3601ff1b5b996 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 31 Oct 2023 09:46:08 -0400 Subject: [PATCH] WIP: condensing sample code --- .../how-to-dynamic-client-registration.adoc | 59 ++++++++++ .../sample/registration/ClientRegistrar.java | 14 +++ .../registration/CustomMetadataConfig.java | 109 ++++++++++++++++++ .../sample/registration/SecurityConfig.java | 11 +- .../DynamicClientRegistrationTests.java | 2 +- 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 docs/src/main/java/sample/registration/CustomMetadataConfig.java 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 85fc06a30e..cf86d60cb2 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 @@ -10,6 +10,7 @@ Spring Authorization Server implements the https://openid.net/specs/openid-conne * xref:guides/how-to-dynamic-client-registration.adoc#configure-client-registrar[Configure client registrar] * xref:guides/how-to-dynamic-client-registration.adoc#obtain-initial-access-token[Obtain initial access token] * xref:guides/how-to-dynamic-client-registration.adoc#register-client[Register a client] +* xref:guides/how-to-dynamic-client-registration.adoc#customize-metadata[Customize client metadata] [[enable-dynamic-client-registration]] == Enable Dynamic Client Registration @@ -103,3 +104,61 @@ include::{examples-dir}/main/java/sample/registration/ClientRegistrar.java[] [NOTE] The https://openid.net/specs/openid-connect-registration-1_0.html#ReadResponse[Client Read Response] should contain the same client metadata parameters as the https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse[Client Registration Response], except the `registration_access_token` parameter. + +[[customize-metadata]] +== Customize client metadata + +In order to accept custom client metadata when registering a client, a few additional implementation details +are necessary. + +[NOTE] +==== +The following example depicts example 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` instance. +<4> Add a custom registered client `Converter` (implementation in #6) +<5> Add a custom client registration `Converter` (implementation in #7) +<6> Custom registered client `Converter` implementation that adds custom claims to registered client settings. +<7> Custom client registration `Converter` implementation that modifies client registration claims with custom metadata. + +[[sample.custommetadata.SecurityConfig]] +[source,java] +---- +include::{examples-dir}/main/java/sample/registration/SecurityConfig.java[] +---- + +<1> Configure the `Consumer>` implementation from above with client registration endpoint authentication providers. + +Once the authorization server is configured as per steps outlined above, a client with custom metadata can now be registered. + +[NOTE] +==== +The registration and retrieval implementations of a client can be found in xref:guides/how-to-dynamic-client-registration.adoc#register-client[Register a client] section. +==== + +[[sample.custommetadata.ClientRegistrar]] +[source,java] +---- +include::{examples-dir}/main/java/sample/registration/ClientRegistrar.java[] +---- + +<1> A minimal representation of a client registration request with added custom metadata fields `logo_uri` and `contacts`. +<2> A minimal representation of a client registration response with added custom metadata fields `logo_uri` and `contacts`. +<3> Example demonstrating client registration and client retrieval. +<4> A sample client registration request object with custom metadata. +<5> Register the client using the "initial" access token and client registration request object. +<6> After successful registration, assert on the client custom 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 custom client metadata parameters that should be populated in the response. diff --git a/docs/src/main/java/sample/registration/ClientRegistrar.java b/docs/src/main/java/sample/registration/ClientRegistrar.java index f199a4357e..942387f12e 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 0000000000..2673654617 --- /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); // + } + }); + }; + } + + static class CustomRegisteredClientConverter implements Converter { // <6> + 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 { // <7> + 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 b55be7e76d..247edd7a85 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 41874e751e..eb4cdcb4d7 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())