浏览代码

Add customize client metadata example in dynamic client registration how-to

Closes gh-1044
Dmitriy Dubson 1 年之前
父节点
当前提交
ef6b1aceca

+ 28 - 3
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<List<AuthenticationProvider>>` 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.

+ 14 - 0
docs/src/main/java/sample/registration/ClientRegistrar.java

@@ -39,6 +39,8 @@ public class ClientRegistrar {
 			@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) {
 	}
 
@@ -50,6 +52,8 @@ public class ClientRegistrar {
 			@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) {
 	}
 
@@ -58,6 +62,8 @@ public class ClientRegistrar {
 				"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 class ClientRegistrar {
 		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 class ClientRegistrar {
 		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());
 	}

+ 109 - 0
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<List<AuthenticationProvider>> registeredClientConverters() {
+		List<String> 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<OidcClientRegistration, RegisteredClient> { // <7>
+		private final List<String> customMetadata;
+
+		private final OidcClientRegistrationRegisteredClientConverter delegate;
+
+		CustomRegisteredClientConverter(List<String> 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<RegisteredClient, OidcClientRegistration> { // <8>
+		private final List<String> customMetadata;
+
+		private final RegisteredClientOidcClientRegistrationConverter delegate;
+
+		CustomClientRegistrationConverter(List<String> customMetadata) {
+			this.customMetadata = customMetadata;
+			this.delegate = new RegisteredClientOidcClientRegistrationConverter();
+		}
+
+		public OidcClientRegistration convert(RegisteredClient registeredClient) {
+			var clientRegistration = delegate.convert(registeredClient);
+			Map<String, Object> 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();
+		}
+	}
+
+}

+ 8 - 3
docs/src/main/java/sample/registration/SecurityConfig.java

@@ -24,6 +24,8 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio
 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();
 	}

+ 1 - 1
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())