Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add how-to guide for dynamic client registration with custom metadata #1376

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions docs/src/main/java/sample/registration/ClientRegistrar.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ 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) {
}

Expand All @@ -50,6 +52,8 @@ public record ClientRegistrationResponse( // <2>
@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) {
}

Expand All @@ -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"
);

Expand All @@ -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();
Expand All @@ -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());
}
Expand Down
109 changes: 109 additions & 0 deletions docs/src/main/java/sample/registration/CustomMetadataConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
11 changes: 8 additions & 3 deletions docs/src/main/java/sample/registration/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,22 @@
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 {

@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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down