Skip to content

Commit

Permalink
Add how-to guide for dynamic client registration with custom metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
ddubson committed Sep 28, 2023
1 parent 2dcbc58 commit 8ca1636
Show file tree
Hide file tree
Showing 11 changed files with 588 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +21,7 @@ To enable, add the following configuration:
[[sample.SecurityConfig]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/SecurityConfig.java[]
include::{examples-dir}/main/java/sample/registration/basic/SecurityConfig.java[]
----

<1> Enable the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration Endpoint] with the default configuration.
Expand All @@ -35,7 +36,7 @@ The following listing shows an example client:
[[sample.ClientConfig]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/ClientConfig.java[]
include::{examples-dir}/main/java/sample/registration/basic/ClientConfig.java[]
----

<1> `client_credentials` grant type is configured to obtain access tokens directly.
Expand Down Expand Up @@ -83,23 +84,93 @@ With an access token obtained from the previous step, a client can now be dynami
The "initial" access token can only be used once.
After the client is registered, the access token is invalidated.

[[sample.ClientRegistrar]]
[[sample.basic.ClientRegistrar]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/ClientRegistrar.java[]
include::{examples-dir}/main/java/sample/registration/basic/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].
<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.
<6> After successful registration, assert on the client 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 client metadata parameters that should be populated in the response.
<10> Sample https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration Request] using `WebClient`.
<11> Sample https://openid.net/specs/openid-connect-registration-1_0.html#ReadRequest[Client Read Request] using `WebClient`.
<3> Sample https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration Request] using `WebClient`.
<4> Sample https://openid.net/specs/openid-connect-registration-1_0.html#ReadRequest[Client Read Request] using `WebClient`.

[[sample.basic.ClientRegistrationExample]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/basic/BasicClientRegistrationExample.java[]
----

<1> Example demonstrating client registration and client retrieval.
<2> A sample client registration request object.
<3> Register the client using the "initial" access token and client registration request object.
<4> After successful registration, assert on the client metadata parameters that should be populated in the response.
<5> Extract `registration_access_token` and `registration_client_uri` response parameters, for use in retrieval of the newly registered client.
<6> Retrieve the client using the `registration_access_token` and `registration_client_uri`.
<7> After client retrieval, assert on the client metadata parameters that should be populated in the response.

[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.custom.CustomMetadataConfig]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/custommetadata/CustomMetadataConfig.java[]
----

<1> Create a `Consumer<List<AuthenticationProvider>>` 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.custom.SecurityConfig]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/custommetadata/SecurityConfig.java[]
----

<1> Configure the `Consumer<List<AuthenticationProvider>>` implementation from above with client registration endpoint authentication providers.

[[sample.custom.ClientRegistrar]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/custommetadata/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`.

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.

Once the authorization server is configured as per steps outlined above, a client with custom metadata can now be registered.

[[sample.custom.CustomMetadataClientRegistrationExample]]
[source,java]
----
include::{examples-dir}/main/java/sample/registration/custommetadata/CustomMetadataClientRegistrationExample.java[]
----

<1> Example demonstrating client registration and client retrieval.
<2> A sample client registration request object with custom metadata.
<3> Register the client using the "initial" access token and client registration request object.
<4> After successful registration, assert on the client custom metadata parameters that should be populated in the response.
<5> Extract `registration_access_token` and `registration_client_uri` response parameters, for use in retrieval of the newly registered client.
<6> Retrieve the client using the `registration_access_token` and `registration_client_uri`.
<7> After client retrieval, assert on the custom client metadata parameters that should be populated in the response.

Original file line number Diff line number Diff line change
Expand Up @@ -13,58 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.registration;
package sample.registration.basic;

import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Mono;

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.web.reactive.function.client.WebClient;

public class ClientRegistrar {
// @fold:on
private final WebClient webClient;

public ClientRegistrar(WebClient webClient) {
this.webClient = webClient;
}
// @fold:off

public record ClientRegistrationRequest( // <1>
@JsonProperty("client_name") String clientName,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
String scope) {
}
import java.util.List;
import java.util.Objects;

public record ClientRegistrationResponse( // <2>
@JsonProperty("registration_access_token") String registrationAccessToken,
@JsonProperty("registration_client_uri") String registrationClientUri,
@JsonProperty("client_name") String clientName,
@JsonProperty("client_id") String clientId,
@JsonProperty("client_secret") String clientSecret,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
String scope) {
}
import static sample.registration.basic.ClientRegistrar.*;

public void exampleRegistration(String initialAccessToken) { // <3>
ClientRegistrationRequest clientRegistrationRequest = new ClientRegistrationRequest( // <4>
public class BasicClientRegistrationExample {
public void register(ClientRegistrar registrar, String initialAccessToken) { // <1>
ClientRegistrationRequest clientRegistrationRequest = new ClientRegistrationRequest( // <2>
"client-1",
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
List.of("https://client.example.org/callback", "https://client.example.org/callback2"),
"openid email profile"
);

ClientRegistrationResponse clientRegistrationResponse =
registerClient(initialAccessToken, clientRegistrationRequest); // <5>
registrar.registerClient(initialAccessToken, clientRegistrationRequest); // <3>

assert (clientRegistrationResponse.clientName().contentEquals("client-1")); // <6>
assert (clientRegistrationResponse.clientName().contentEquals("client-1")); // <4>
assert (!Objects.isNull(clientRegistrationResponse.clientSecret()));
assert (clientRegistrationResponse.scope().contentEquals("openid profile email"));
assert (clientRegistrationResponse.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
Expand All @@ -73,12 +43,12 @@ public void exampleRegistration(String initialAccessToken) { // <3>
assert (!clientRegistrationResponse.registrationAccessToken().isEmpty());
assert (!clientRegistrationResponse.registrationClientUri().isEmpty());

String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <7>
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <5>
String registrationClientUri = clientRegistrationResponse.registrationClientUri();

ClientRegistrationResponse retrievedClient = retrieveClient(registrationAccessToken, registrationClientUri); // <8>
ClientRegistrationResponse retrievedClient = registrar.retrieveClient(registrationAccessToken, registrationClientUri); // <6>

assert (retrievedClient.clientName().contentEquals("client-1")); // <9>
assert (retrievedClient.clientName().contentEquals("client-1")); // <7>
assert (!Objects.isNull(retrievedClient.clientId()));
assert (!Objects.isNull(retrievedClient.clientSecret()));
assert (retrievedClient.scope().contentEquals("openid profile email"));
Expand All @@ -88,28 +58,4 @@ public void exampleRegistration(String initialAccessToken) { // <3>
assert (Objects.isNull(retrievedClient.registrationAccessToken()));
assert (!retrievedClient.registrationClientUri().isEmpty());
}

public ClientRegistrationResponse registerClient(String initialAccessToken, ClientRegistrationRequest request) { // <10>
return this.webClient
.post()
.uri("/connect/register")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(initialAccessToken))
.body(Mono.just(request), ClientRegistrationRequest.class)
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}

public ClientRegistrationResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { // <11>
return this.webClient
.get()
.uri(registrationClientUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(registrationAccessToken))
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.registration;

import java.util.UUID;
package sample.registration.basic;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -25,9 +23,10 @@
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;

import java.util.UUID;

@Configuration
public class ClientConfig {

@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
Expand All @@ -41,5 +40,4 @@ public RegisteredClientRepository registeredClientRepository() {

return new InMemoryRegisteredClientRepository(registrarClient);
}

}
75 changes: 75 additions & 0 deletions docs/src/main/java/sample/registration/basic/ClientRegistrar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.basic;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.List;
public class ClientRegistrar {
// @fold:on
private final WebClient webClient;

public ClientRegistrar(WebClient webClient) {
this.webClient = webClient;
}
// @fold:off

public record ClientRegistrationRequest( // <1>
@JsonProperty("client_name") String clientName,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
String scope) {
}

public record ClientRegistrationResponse( // <2>
@JsonProperty("registration_access_token") String registrationAccessToken,
@JsonProperty("registration_client_uri") String registrationClientUri,
@JsonProperty("client_name") String clientName,
@JsonProperty("client_id") String clientId,
@JsonProperty("client_secret") String clientSecret,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
String scope) {
}

public ClientRegistrationResponse registerClient(String initialAccessToken, ClientRegistrationRequest request) { // <3>
return this.webClient
.post()
.uri("/connect/register")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(initialAccessToken))
.body(Mono.just(request), ClientRegistrationRequest.class)
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}

public ClientRegistrationResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { // <4>
return this.webClient
.get()
.uri(registrationClientUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(registrationAccessToken))
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.registration;
package sample.registration.basic;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down
Loading

0 comments on commit 8ca1636

Please sign in to comment.