Skip to content

Commit b964893

Browse files
author
Dmitriy Dubson
committed
Add custom metadata example in dynamic client registration how-to
Fixes gh-1044
1 parent fe8c78c commit b964893

File tree

5 files changed

+160
-7
lines changed

5 files changed

+160
-7
lines changed

docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,32 @@ To enable, add the following configuration:
2323
include::{examples-dir}/main/java/sample/registration/SecurityConfig.java[]
2424
----
2525

26-
<1> Enable the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration Endpoint] with the default configuration.
26+
<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.
27+
28+
In order to accept custom client metadata when registering a client, a few additional implementation details
29+
are necessary.
30+
31+
[NOTE]
32+
====
33+
The following example depicts custom metadata `logo_uri` (string type) and `contacts` (string array type)
34+
====
35+
36+
Create a set of custom `Converter` classes in order to retain custom client claims.
37+
38+
[[sample.CustomMetadataConfig]]
39+
[source,java]
40+
----
41+
include::{examples-dir}/main/java/sample/registration/CustomMetadataConfig.java[]
42+
----
43+
44+
<1> Create a `Consumer<List<AuthenticationProvider>>` implementation.
45+
<2> Identify custom fields that should be accepted during client registration.
46+
<3> Filter for `OidcClientRegistrationAuthenticationProvider` and `OidcClientConfigurationAuthenticationProvider` instances.
47+
<4> Add a custom registered client `Converter` (implementation in #7)
48+
<5> Add a custom client registration `Converter` to `OidcClientRegistrationAuthenticationProvider` (implementation in #8)
49+
<6> Add a custom client registration `Converter` to `OidcClientConfigurationAuthenticationProvider` (implementation in #8)
50+
<7> Custom registered client `Converter` implementation that adds custom claims to registered client settings.
51+
<8> Custom client registration `Converter` implementation that modifies client registration claims with custom metadata.
2752

2853
[[configure-client-registrar]]
2954
== Configure client registrar
@@ -89,8 +114,8 @@ After the client is registered, the access token is invalidated.
89114
include::{examples-dir}/main/java/sample/registration/ClientRegistrar.java[]
90115
----
91116

92-
<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].
93-
<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].
117+
<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`.
118+
<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`.
94119
<3> Example demonstrating client registration and client retrieval.
95120
<4> A sample client registration request object.
96121
<5> Register the client using the "initial" access token and client registration request object.

docs/src/main/java/sample/registration/ClientRegistrar.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public record ClientRegistrationRequest( // <1>
3939
@JsonProperty("client_name") String clientName,
4040
@JsonProperty("grant_types") List<String> grantTypes,
4141
@JsonProperty("redirect_uris") List<String> redirectUris,
42+
@JsonProperty("logo_uri") String logoUri,
43+
List<String> contacts,
4244
String scope) {
4345
}
4446

@@ -50,6 +52,8 @@ public record ClientRegistrationResponse( // <2>
5052
@JsonProperty("client_secret") String clientSecret,
5153
@JsonProperty("grant_types") List<String> grantTypes,
5254
@JsonProperty("redirect_uris") List<String> redirectUris,
55+
@JsonProperty("logo_uri") String logoUri,
56+
List<String> contacts,
5357
String scope) {
5458
}
5559

@@ -58,6 +62,8 @@ public void exampleRegistration(String initialAccessToken) { // <3>
5862
"client-1",
5963
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
6064
List.of("https://client.example.org/callback", "https://client.example.org/callback2"),
65+
"https://client.example.org/logo",
66+
List.of("contact-1", "contact-2"),
6167
"openid email profile"
6268
);
6369

@@ -72,6 +78,10 @@ public void exampleRegistration(String initialAccessToken) { // <3>
7278
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback2"));
7379
assert (!clientRegistrationResponse.registrationAccessToken().isEmpty());
7480
assert (!clientRegistrationResponse.registrationClientUri().isEmpty());
81+
assert (clientRegistrationResponse.logoUri().contentEquals("https://client.example.org/logo")); // <6>
82+
assert (clientRegistrationResponse.contacts().size() == 2);
83+
assert (clientRegistrationResponse.contacts().contains("contact-1"));
84+
assert (clientRegistrationResponse.contacts().contains("contact-2"));
7585

7686
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <7>
7787
String registrationClientUri = clientRegistrationResponse.registrationClientUri();
@@ -85,6 +95,10 @@ public void exampleRegistration(String initialAccessToken) { // <3>
8595
assert (retrievedClient.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
8696
assert (retrievedClient.redirectUris().contains("https://client.example.org/callback"));
8797
assert (retrievedClient.redirectUris().contains("https://client.example.org/callback2"));
98+
assert (retrievedClient.logoUri().contentEquals("https://client.example.org/logo"));
99+
assert (retrievedClient.contacts().size() == 2);
100+
assert (retrievedClient.contacts().contains("contact-1"));
101+
assert (retrievedClient.contacts().contains("contact-2"));
88102
assert (Objects.isNull(retrievedClient.registrationAccessToken()));
89103
assert (!retrievedClient.registrationClientUri().isEmpty());
90104
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample.registration;
17+
18+
import org.springframework.core.convert.converter.Converter;
19+
import org.springframework.security.authentication.AuthenticationProvider;
20+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
21+
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
22+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
23+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
24+
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
25+
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
26+
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
27+
import org.springframework.util.CollectionUtils;
28+
29+
import java.util.HashMap;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.function.Consumer;
33+
import java.util.function.Function;
34+
import java.util.stream.Collectors;
35+
36+
public class CustomMetadataConfig {
37+
public static Consumer<List<AuthenticationProvider>> registeredClientConverters() {
38+
List<String> customClientMetadata = List.of("logo_uri", "contacts"); // <1>
39+
40+
return authenticationProviders -> // <2>
41+
{
42+
CustomRegisteredClientConverter registeredClientConverter = new CustomRegisteredClientConverter(customClientMetadata);
43+
CustomClientRegistrationConverter clientRegistrationConverter = new CustomClientRegistrationConverter(customClientMetadata);
44+
45+
authenticationProviders.forEach(authenticationProvider -> {
46+
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) { // <3>
47+
provider.setRegisteredClientConverter(registeredClientConverter); // <4>
48+
provider.setClientRegistrationConverter(clientRegistrationConverter); // <5>
49+
}
50+
51+
if (authenticationProvider instanceof OidcClientConfigurationAuthenticationProvider provider) {
52+
provider.setClientRegistrationConverter(clientRegistrationConverter); // <6>
53+
}
54+
});
55+
};
56+
}
57+
58+
static class CustomRegisteredClientConverter implements Converter<OidcClientRegistration, RegisteredClient> { // <7>
59+
private final List<String> customMetadata;
60+
61+
private final OidcClientRegistrationRegisteredClientConverter delegate;
62+
63+
CustomRegisteredClientConverter(List<String> customMetadata) {
64+
this.customMetadata = customMetadata;
65+
this.delegate = new OidcClientRegistrationRegisteredClientConverter();
66+
}
67+
68+
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
69+
RegisteredClient convertedClient = delegate.convert(clientRegistration);
70+
ClientSettings.Builder clientSettingsBuilder = ClientSettings
71+
.withSettings(convertedClient.getClientSettings().getSettings());
72+
73+
if (!CollectionUtils.isEmpty(this.customMetadata)) {
74+
clientRegistration.getClaims().forEach((claim, value) -> {
75+
if (this.customMetadata.contains(claim)) {
76+
clientSettingsBuilder.setting(claim, value);
77+
}
78+
});
79+
}
80+
81+
return RegisteredClient.from(convertedClient).clientSettings(clientSettingsBuilder.build()).build();
82+
}
83+
}
84+
85+
static class CustomClientRegistrationConverter implements Converter<RegisteredClient, OidcClientRegistration> { // <8>
86+
private final List<String> customMetadata;
87+
88+
private final RegisteredClientOidcClientRegistrationConverter delegate;
89+
90+
CustomClientRegistrationConverter(List<String> customMetadata) {
91+
this.customMetadata = customMetadata;
92+
this.delegate = new RegisteredClientOidcClientRegistrationConverter();
93+
}
94+
95+
public OidcClientRegistration convert(RegisteredClient registeredClient) {
96+
var clientRegistration = delegate.convert(registeredClient);
97+
Map<String, Object> claims = new HashMap<>(clientRegistration.getClaims());
98+
if (!CollectionUtils.isEmpty(customMetadata)) {
99+
ClientSettings clientSettings = registeredClient.getClientSettings();
100+
101+
claims.putAll(customMetadata.stream()
102+
.filter(metadatum -> clientSettings.getSetting(metadatum) != null)
103+
.collect(Collectors.toMap(Function.identity(), clientSettings::getSetting)));
104+
}
105+
return OidcClientRegistration.withClaims(claims).build();
106+
}
107+
}
108+
109+
}

docs/src/main/java/sample/registration/SecurityConfig.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,22 @@
2424
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
2525
import org.springframework.security.web.SecurityFilterChain;
2626

27+
import static sample.registration.CustomMetadataConfig.registeredClientConverters;
28+
2729
@Configuration
2830
@EnableWebSecurity
2931
public class SecurityConfig {
3032

3133
@Bean
3234
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
3335
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
36+
3437
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
35-
.oidc(oidc -> oidc.clientRegistrationEndpoint(Customizer.withDefaults())); // <1>
36-
http.oauth2ResourceServer(oauth2ResourceServer ->
37-
oauth2ResourceServer.jwt(Customizer.withDefaults()));
38+
.oidc(oidc -> oidc.clientRegistrationEndpoint(endpoint -> {
39+
endpoint.authenticationProviders(registeredClientConverters()); // <1>
40+
}));
41+
42+
http.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
3843

3944
return http.build();
4045
}

docs/src/test/java/sample/registration/DynamicClientRegistrationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public class DynamicClientRegistrationTests {
5656
private String port;
5757

5858
@Test
59-
public void dynamicallyRegisterClient() throws Exception {
59+
public void dynamicallyRegisterClientWithCustomMetadata() throws Exception {
6060
MockHttpServletResponse tokenResponse = this.mvc.perform(post("/oauth2/token")
6161
.with(httpBasic("registrar-client", "secret"))
6262
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())

0 commit comments

Comments
 (0)