Skip to content

Commit 11fa68c

Browse files
committed
Adds dynamic client registration how-to guide
Closes gh-647
1 parent 99cd1b8 commit 11fa68c

File tree

7 files changed

+452
-0
lines changed

7 files changed

+452
-0
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
** xref:guides/how-to-userinfo.adoc[]
1212
** xref:guides/how-to-jpa.adoc[]
1313
** xref:guides/how-to-custom-claims-authorities.adoc[]
14+
** xref:guides/how-to-dynamic-client-registration.adoc[]
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
[[how-to-dynamic-client-registration]]
2+
= How-to: Register a client dynamically
3+
:index-link: ../how-to.html
4+
:docs-dir: ..
5+
6+
This guide shows how to configure OpenID Connect Dynamic Client Registration 1.0 in Spring Authorization Server and walks through an example of how to register a client.
7+
Spring Authorization Server implements https://openid.net/specs/openid-connect-registration-1_0.html[OpenID Connect Dynamic Client Registration 1.0]
8+
specification, gaining the ability to dynamically register and retrieve OpenID clients.
9+
10+
- xref:guides/how-to-dynamic-client-registration.adoc#enable[Enable Dynamic Client Registration]
11+
- xref:guides/how-to-dynamic-client-registration.adoc#configure-initial-client[Configure initial client]
12+
- xref:guides/how-to-dynamic-client-registration.adoc#obtain-initial-access-token[Obtain initial access token]
13+
- xref:guides/how-to-dynamic-client-registration.adoc#register-client[Register a client]
14+
15+
[[enable]]
16+
== Enable Dynamic Client Registration
17+
18+
By default, dynamic client registration functionality is disabled in Spring Authorization Server.
19+
To enable, add the following configuration:
20+
21+
[[sample.dcrAuthServerConfig]]
22+
[source,java]
23+
----
24+
include::{examples-dir}/main/java/sample/dcr/DcrConfiguration.java[]
25+
----
26+
27+
<1> Add a `SecurityFilterChain` `@Bean` that registers an `OAuth2AuthorizationServerConfigurer`
28+
<2> In the configurer, apply OIDC client registration endpoint customizer with default values.
29+
This enables dynamic client registration functionality.
30+
31+
Please refer to xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[Client Registration Endpoint docs] for in-depth configuration details.
32+
33+
[[configure-initial-client]]
34+
== Configure initial client
35+
36+
An initial client is required in order to register new clients in the authorization server.
37+
The client must be configured with scopes `client.create` and optionally `client.read` for creating clients and reading clients, respectively.
38+
A programmatic example of such a client is below.
39+
40+
[[sample.dcrRegisteredClientConfig]]
41+
[source,java]
42+
----
43+
include::{examples-dir}/main/java/sample/dcr/RegisteredClientConfiguration.java[]
44+
----
45+
46+
<1> A `RegisteredClientRepository` `@Bean` is configured with a set of clients.
47+
<2> An initial client with client id `dcr-client` is configured.
48+
<3> `client_credentials` grant type is set to fetch access tokens directly.
49+
<4> `client.create` scope is configured for the client to ensure they are able to create clients.
50+
<5> `client.read` scope is configured for the client to ensure they are able to fetch and read clients.
51+
<6> The initial client is saved into the data store.
52+
53+
After configuring the above, run the authorization server in your preferred environment.
54+
55+
[[obtain-initial-access-token]]
56+
== Obtain initial access token
57+
58+
An initial access token is required to be able to create client registration requests.
59+
The token request must contain a request for scope `client.create` only.
60+
61+
[source,httprequest]
62+
----
63+
POST /oauth2/token HTTP/1.1
64+
Authorization: Basic <base64-encoded-credentials>
65+
Content-Type: application/x-www-form-urlencoded
66+
67+
grant_type=client_credentials&scope=client.create
68+
----
69+
70+
[WARNING]
71+
====
72+
If you provide more than one scope in the request, you will not be able to register a client.
73+
The client creation request requires an access token with a single scope of `client.create`
74+
====
75+
76+
[TIP]
77+
====
78+
To obtain encoded credentials for the above request, `base64` encode the client credentials in the format of
79+
`<clientId>:<clientSecret>`. Below is an encoding operation for the example in this guide.
80+
81+
[source,console]
82+
----
83+
echo -n "initial-app:secret" | base64
84+
----
85+
====
86+
87+
[[register-client]]
88+
== Register a client
89+
90+
With an access token obtained from the previous step, a client can now be dynamically registered.
91+
92+
[NOTE]
93+
The access token can only be used once. After a single registration request, the access token is invalidated.
94+
95+
[[sample.dcrClientRegistration]]
96+
[source,java]
97+
----
98+
include::{examples-dir}/main/java/sample/dcr/DcrClient.java[]
99+
----
100+
101+
<1> A minimal client registration request object.
102+
You may add additional fields as per https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[OpenID Connect Dynamic Client Registration 1.0 spec - Client Registration Request].
103+
<2> A minimal client registration response object.
104+
You may add additional response fields as per https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse[OpenID Connect Dynamic Client Registration 1.0 spec - Client Registration Response].
105+
<3> A sample client registration request object which will be used to register a sample client.
106+
<4> Example dynamic client registration procedure, demonstrating dynamic registration and client retrieval.
107+
<5> Register a client using sample request from step 2, using initial access token from previous step.
108+
Skip to step 10 for implementation.
109+
<6> After registration, assert on the fields that should be populated in the response upon successful registration.
110+
<7> Extract `registration_access_token` and `registration_client_uri` fields, for use in retrieval of the newly registered client.
111+
<8> Retrieve client. Skip to step 11 for implementation.
112+
<9> After client retrieval, assert on the fields that should be populated in the response.
113+
<10> Sample client registration procedure using Spring WebFlux's `WebClient`.
114+
Note that the `WebClient` must have `baseUrl` of the authorization server configured.
115+
<11> Sample client retrieval procedure using Spring WebFlux's `WebClient`.
116+
Note that the `WebClient` must have `baseUrl` of the authorization server configured.
117+
118+
The retrieve client response should contain the same information about the client as seen when the client was first
119+
registered, except for `registration_access_token` field.

docs/spring-authorization-server-docs.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
5757
implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
5858
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
59+
implementation "org.springframework:spring-webflux"
5960
implementation project(":spring-security-oauth2-authorization-server")
6061
runtimeOnly "com.h2database:h2"
6162
testImplementation "org.springframework.boot:spring-boot-starter-test"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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.dcr;
17+
18+
import com.fasterxml.jackson.annotation.JsonProperty;
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.http.MediaType;
21+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
22+
import org.springframework.web.reactive.function.client.WebClient;
23+
import reactor.core.publisher.Mono;
24+
25+
import java.util.List;
26+
import java.util.Objects;
27+
28+
public class DcrClient {
29+
// @fold:on
30+
private final WebClient webClient;
31+
32+
public DcrClient(final WebClient webClient) {
33+
this.webClient = webClient;
34+
}
35+
// @fold:off
36+
37+
public record DcrRequest( // <1>
38+
@JsonProperty("client_name") String clientName,
39+
@JsonProperty("grant_types") List<String> grantTypes,
40+
@JsonProperty("redirect_uris") List<String> redirectUris,
41+
String scope) {
42+
}
43+
44+
public record DcrResponse( // <2>
45+
@JsonProperty("registration_access_token") String registrationAccessToken,
46+
@JsonProperty("registration_client_uri") String registrationClientUri,
47+
@JsonProperty("client_name") String clientName,
48+
@JsonProperty("client_secret") String clientSecret,
49+
@JsonProperty("grant_types") List<String> grantTypes,
50+
@JsonProperty("redirect_uris") List<String> redirectUris,
51+
String scope) {
52+
}
53+
54+
public static final DcrRequest SAMPLE_CLIENT_REGISTRATION_REQUEST = new DcrRequest( // <3>
55+
"client-1",
56+
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
57+
List.of("https://client.example.org/callback", "https://client.example.org/callback2"),
58+
"openid email profile"
59+
);
60+
61+
public void exampleRegistration(String initialAccessToken) { // <4>
62+
DcrResponse clientRegistrationResponse =
63+
this.registerClient(initialAccessToken, SAMPLE_CLIENT_REGISTRATION_REQUEST); // <5>
64+
65+
assert (clientRegistrationResponse.clientName().contentEquals("client-1")); // <6>
66+
assert (!Objects.isNull(clientRegistrationResponse.clientSecret()));
67+
assert (clientRegistrationResponse.scope().contentEquals("openid profile email"));
68+
assert (clientRegistrationResponse.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
69+
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback"));
70+
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback2"));
71+
assert (!clientRegistrationResponse.registrationAccessToken().isEmpty());
72+
assert (!clientRegistrationResponse.registrationClientUri().isEmpty());
73+
74+
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <7>
75+
String registrationClientUri = clientRegistrationResponse.registrationClientUri();
76+
77+
DcrResponse retrievedClient = this.retrieveClient(registrationAccessToken, registrationClientUri); // <8>
78+
79+
assert (retrievedClient.clientName().contentEquals("client-1")); // <9>
80+
assert (!Objects.isNull(retrievedClient.clientSecret()));
81+
assert (retrievedClient.scope().contentEquals("openid profile email"));
82+
assert (retrievedClient.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
83+
assert (retrievedClient.redirectUris().contains("https://client.example.org/callback"));
84+
assert (retrievedClient.redirectUris().contains("https://client.example.org/callback2"));
85+
assert (Objects.isNull(retrievedClient.registrationAccessToken()));
86+
assert (!retrievedClient.registrationClientUri().isEmpty());
87+
}
88+
89+
public DcrResponse registerClient(String initialAccessToken, DcrRequest request) { // <10>
90+
return this.webClient
91+
.post()
92+
.uri("/connect/register")
93+
.contentType(MediaType.APPLICATION_JSON)
94+
.accept(MediaType.APPLICATION_JSON)
95+
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(initialAccessToken))
96+
.body(Mono.just(request), DcrRequest.class)
97+
.retrieve()
98+
.bodyToMono(DcrResponse.class)
99+
.block();
100+
}
101+
102+
public DcrResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { // <11>
103+
return this.webClient
104+
.get()
105+
.uri(registrationClientUri)
106+
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(registrationAccessToken))
107+
.retrieve()
108+
.bodyToMono(DcrResponse.class)
109+
.block();
110+
}
111+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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.dcr;
17+
18+
import com.nimbusds.jose.jwk.JWKSet;
19+
import com.nimbusds.jose.jwk.RSAKey;
20+
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
21+
import com.nimbusds.jose.jwk.source.JWKSource;
22+
import com.nimbusds.jose.proc.SecurityContext;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.security.config.Customizer;
26+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
27+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
28+
import org.springframework.security.core.userdetails.UserDetailsService;
29+
import org.springframework.security.oauth2.jwt.JwtDecoder;
30+
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
31+
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
32+
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
33+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
34+
import org.springframework.security.web.SecurityFilterChain;
35+
36+
import java.security.KeyPair;
37+
import java.security.KeyPairGenerator;
38+
import java.security.interfaces.RSAPrivateKey;
39+
import java.security.interfaces.RSAPublicKey;
40+
import java.util.Collections;
41+
import java.util.UUID;
42+
43+
@Configuration
44+
@EnableWebSecurity
45+
public class DcrConfiguration {
46+
@Bean // <1>
47+
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
48+
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
49+
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
50+
.oidc(oidc -> oidc.clientRegistrationEndpoint(Customizer.withDefaults())); // <2>
51+
http.oauth2ResourceServer(oauth2ResourceServer ->
52+
oauth2ResourceServer.jwt(Customizer.withDefaults()));
53+
54+
return http.build();
55+
}
56+
// @fold:on
57+
58+
@Bean
59+
public UserDetailsService userDetailsService() {
60+
// This example uses client credentials grant type - no need for any users.
61+
return new InMemoryUserDetailsManager(Collections.emptyList());
62+
}
63+
64+
@Bean
65+
public JWKSource<SecurityContext> jwkSource() {
66+
// @formatter:off
67+
KeyPair keyPair;
68+
try {
69+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
70+
keyPairGenerator.initialize(2048);
71+
keyPair = keyPairGenerator.generateKeyPair();
72+
} catch (Exception ex) {
73+
throw new IllegalStateException(ex);
74+
}
75+
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
76+
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
77+
RSAKey rsaKey = new RSAKey.Builder(publicKey)
78+
.privateKey(privateKey)
79+
.keyID(UUID.randomUUID().toString())
80+
.build();
81+
// @formatter:on
82+
JWKSet jwkSet = new JWKSet(rsaKey);
83+
return new ImmutableJWKSet<>(jwkSet);
84+
}
85+
86+
@Bean
87+
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
88+
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
89+
}
90+
91+
@Bean
92+
public AuthorizationServerSettings authorizationServerSettings() {
93+
return AuthorizationServerSettings.builder().build();
94+
}
95+
// @fold:off
96+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.dcr;
17+
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
21+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
22+
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
23+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
24+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
25+
26+
import java.util.UUID;
27+
28+
@Configuration
29+
public class RegisteredClientConfiguration {
30+
@Bean // <1>
31+
public RegisteredClientRepository registeredClientRepository() {
32+
RegisteredClient initialClient = RegisteredClient.withId(UUID.randomUUID().toString())
33+
.clientId("dcr-client") // <2>
34+
.clientSecret("{noop}secret")
35+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
36+
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // <3>
37+
.scope("client.create") // <4>
38+
.scope("client.read") // <5>
39+
.build();
40+
41+
return new InMemoryRegisteredClientRepository(initialClient); // <6>
42+
}
43+
}

0 commit comments

Comments
 (0)