-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Adds dynamic client registration how-to guide #1320
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
Closed
ddubson
wants to merge
1
commit into
spring-projects:main
from
ddubson:ddubson/issue-647/how-to/dynamic-client-registration
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
[[how-to-dynamic-client-registration]] | ||
= How-to: Register a client dynamically | ||
:index-link: ../how-to.html | ||
:docs-dir: .. | ||
|
||
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. | ||
Spring Authorization Server implements https://openid.net/specs/openid-connect-registration-1_0.html[OpenID Connect Dynamic Client Registration 1.0] | ||
specification, gaining the ability to dynamically register and retrieve OpenID clients. | ||
|
||
- xref:guides/how-to-dynamic-client-registration.adoc#enable[Enable Dynamic Client Registration] | ||
- xref:guides/how-to-dynamic-client-registration.adoc#configure-initial-client[Configure initial client] | ||
- 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] | ||
|
||
[[enable]] | ||
== Enable Dynamic Client Registration | ||
|
||
By default, dynamic client registration functionality is disabled in Spring Authorization Server. | ||
To enable, add the following configuration: | ||
|
||
[[sample.dcrAuthServerConfig]] | ||
[source,java] | ||
---- | ||
include::{examples-dir}/main/java/sample/dcr/DcrConfiguration.java[] | ||
---- | ||
|
||
<1> Add a `SecurityFilterChain` `@Bean` that registers an `OAuth2AuthorizationServerConfigurer` | ||
<2> In the configurer, apply OIDC client registration endpoint customizer with default values. | ||
This enables dynamic client registration functionality. | ||
|
||
Please refer to xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[Client Registration Endpoint docs] for in-depth configuration details. | ||
|
||
[[configure-initial-client]] | ||
== Configure initial client | ||
|
||
An initial client is required in order to register new clients in the authorization server. | ||
The client must be configured with scopes `client.create` and optionally `client.read` for creating clients and reading clients, respectively. | ||
A programmatic example of such a client is below. | ||
|
||
[[sample.dcrRegisteredClientConfig]] | ||
[source,java] | ||
---- | ||
include::{examples-dir}/main/java/sample/dcr/RegisteredClientConfiguration.java[] | ||
---- | ||
|
||
<1> A `RegisteredClientRepository` `@Bean` is configured with a set of clients. | ||
<2> An initial client with client id `dcr-client` is configured. | ||
<3> `client_credentials` grant type is set to fetch access tokens directly. | ||
<4> `client.create` scope is configured for the client to ensure they are able to create clients. | ||
<5> `client.read` scope is configured for the client to ensure they are able to fetch and read clients. | ||
<6> The initial client is saved into the data store. | ||
|
||
After configuring the above, run the authorization server in your preferred environment. | ||
|
||
[[obtain-initial-access-token]] | ||
== Obtain initial access token | ||
|
||
An initial access token is required to be able to create client registration requests. | ||
The token request must contain a request for scope `client.create` only. | ||
|
||
[source,httprequest] | ||
---- | ||
POST /oauth2/token HTTP/1.1 | ||
Authorization: Basic <base64-encoded-credentials> | ||
Content-Type: application/x-www-form-urlencoded | ||
|
||
grant_type=client_credentials&scope=client.create | ||
---- | ||
|
||
[WARNING] | ||
==== | ||
If you provide more than one scope in the request, you will not be able to register a client. | ||
The client creation request requires an access token with a single scope of `client.create` | ||
==== | ||
|
||
[TIP] | ||
==== | ||
To obtain encoded credentials for the above request, `base64` encode the client credentials in the format of | ||
`<clientId>:<clientSecret>`. Below is an encoding operation for the example in this guide. | ||
|
||
[source,console] | ||
---- | ||
echo -n "initial-app:secret" | base64 | ||
---- | ||
==== | ||
|
||
[[register-client]] | ||
== Register a client | ||
|
||
With an access token obtained from the previous step, a client can now be dynamically registered. | ||
|
||
[NOTE] | ||
The access token can only be used once. After a single registration request, the access token is invalidated. | ||
|
||
[[sample.dcrClientRegistration]] | ||
[source,java] | ||
---- | ||
include::{examples-dir}/main/java/sample/dcr/DcrClient.java[] | ||
---- | ||
|
||
<1> A minimal client registration request object. | ||
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]. | ||
<2> A minimal client registration response object. | ||
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]. | ||
<3> A sample client registration request object which will be used to register a sample client. | ||
<4> Example dynamic client registration procedure, demonstrating dynamic registration and client retrieval. | ||
<5> Register a client using sample request from step 2, using initial access token from previous step. | ||
Skip to step 10 for implementation. | ||
<6> After registration, assert on the fields that should be populated in the response upon successful registration. | ||
<7> Extract `registration_access_token` and `registration_client_uri` fields, for use in retrieval of the newly registered client. | ||
<8> Retrieve client. Skip to step 11 for implementation. | ||
<9> After client retrieval, assert on the fields that should be populated in the response. | ||
<10> Sample client registration procedure using Spring WebFlux's `WebClient`. | ||
Note that the `WebClient` must have `baseUrl` of the authorization server configured. | ||
<11> Sample client retrieval procedure using Spring WebFlux's `WebClient`. | ||
Note that the `WebClient` must have `baseUrl` of the authorization server configured. | ||
|
||
The retrieve client response should contain the same information about the client as seen when the client was first | ||
registered, except for `registration_access_token` field. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
* 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.dcr; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
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; | ||
import reactor.core.publisher.Mono; | ||
|
||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
public class DcrClient { | ||
// @fold:on | ||
private final WebClient webClient; | ||
|
||
public DcrClient(final WebClient webClient) { | ||
this.webClient = webClient; | ||
} | ||
// @fold:off | ||
|
||
public record DcrRequest( // <1> | ||
@JsonProperty("client_name") String clientName, | ||
@JsonProperty("grant_types") List<String> grantTypes, | ||
@JsonProperty("redirect_uris") List<String> redirectUris, | ||
String scope) { | ||
} | ||
|
||
public record DcrResponse( // <2> | ||
@JsonProperty("registration_access_token") String registrationAccessToken, | ||
@JsonProperty("registration_client_uri") String registrationClientUri, | ||
@JsonProperty("client_name") String clientName, | ||
@JsonProperty("client_secret") String clientSecret, | ||
@JsonProperty("grant_types") List<String> grantTypes, | ||
@JsonProperty("redirect_uris") List<String> redirectUris, | ||
String scope) { | ||
} | ||
|
||
public static final DcrRequest SAMPLE_CLIENT_REGISTRATION_REQUEST = new DcrRequest( // <3> | ||
"client-1", | ||
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), | ||
List.of("https://client.example.org/callback", "https://client.example.org/callback2"), | ||
"openid email profile" | ||
); | ||
|
||
public void exampleRegistration(String initialAccessToken) { // <4> | ||
DcrResponse clientRegistrationResponse = | ||
this.registerClient(initialAccessToken, SAMPLE_CLIENT_REGISTRATION_REQUEST); // <5> | ||
|
||
assert (clientRegistrationResponse.clientName().contentEquals("client-1")); // <6> | ||
assert (!Objects.isNull(clientRegistrationResponse.clientSecret())); | ||
assert (clientRegistrationResponse.scope().contentEquals("openid profile email")); | ||
assert (clientRegistrationResponse.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())); | ||
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback")); | ||
assert (clientRegistrationResponse.redirectUris().contains("https://client.example.org/callback2")); | ||
assert (!clientRegistrationResponse.registrationAccessToken().isEmpty()); | ||
assert (!clientRegistrationResponse.registrationClientUri().isEmpty()); | ||
|
||
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <7> | ||
String registrationClientUri = clientRegistrationResponse.registrationClientUri(); | ||
|
||
DcrResponse retrievedClient = this.retrieveClient(registrationAccessToken, registrationClientUri); // <8> | ||
|
||
assert (retrievedClient.clientName().contentEquals("client-1")); // <9> | ||
assert (!Objects.isNull(retrievedClient.clientSecret())); | ||
assert (retrievedClient.scope().contentEquals("openid profile email")); | ||
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 (Objects.isNull(retrievedClient.registrationAccessToken())); | ||
assert (!retrievedClient.registrationClientUri().isEmpty()); | ||
} | ||
|
||
public DcrResponse registerClient(String initialAccessToken, DcrRequest 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), DcrRequest.class) | ||
.retrieve() | ||
.bodyToMono(DcrResponse.class) | ||
.block(); | ||
} | ||
|
||
public DcrResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { // <11> | ||
return this.webClient | ||
.get() | ||
.uri(registrationClientUri) | ||
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(registrationAccessToken)) | ||
.retrieve() | ||
.bodyToMono(DcrResponse.class) | ||
.block(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* 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.dcr; | ||
|
||
import com.nimbusds.jose.jwk.JWKSet; | ||
import com.nimbusds.jose.jwk.RSAKey; | ||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet; | ||
import com.nimbusds.jose.jwk.source.JWKSource; | ||
import com.nimbusds.jose.proc.SecurityContext; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.config.Customizer; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.core.userdetails.UserDetailsService; | ||
import org.springframework.security.oauth2.jwt.JwtDecoder; | ||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; | ||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; | ||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; | ||
import org.springframework.security.provisioning.InMemoryUserDetailsManager; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
|
||
import java.security.KeyPair; | ||
import java.security.KeyPairGenerator; | ||
import java.security.interfaces.RSAPrivateKey; | ||
import java.security.interfaces.RSAPublicKey; | ||
import java.util.Collections; | ||
import java.util.UUID; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
public class DcrConfiguration { | ||
@Bean // <1> | ||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { | ||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); | ||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) | ||
.oidc(oidc -> oidc.clientRegistrationEndpoint(Customizer.withDefaults())); // <2> | ||
http.oauth2ResourceServer(oauth2ResourceServer -> | ||
oauth2ResourceServer.jwt(Customizer.withDefaults())); | ||
|
||
return http.build(); | ||
} | ||
// @fold:on | ||
|
||
@Bean | ||
public UserDetailsService userDetailsService() { | ||
// This example uses client credentials grant type - no need for any users. | ||
return new InMemoryUserDetailsManager(Collections.emptyList()); | ||
} | ||
|
||
@Bean | ||
public JWKSource<SecurityContext> jwkSource() { | ||
// @formatter:off | ||
KeyPair keyPair; | ||
try { | ||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); | ||
keyPairGenerator.initialize(2048); | ||
keyPair = keyPairGenerator.generateKeyPair(); | ||
} catch (Exception ex) { | ||
throw new IllegalStateException(ex); | ||
} | ||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); | ||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); | ||
RSAKey rsaKey = new RSAKey.Builder(publicKey) | ||
.privateKey(privateKey) | ||
.keyID(UUID.randomUUID().toString()) | ||
.build(); | ||
// @formatter:on | ||
JWKSet jwkSet = new JWKSet(rsaKey); | ||
return new ImmutableJWKSet<>(jwkSet); | ||
} | ||
|
||
@Bean | ||
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { | ||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); | ||
} | ||
|
||
@Bean | ||
public AuthorizationServerSettings authorizationServerSettings() { | ||
return AuthorizationServerSettings.builder().build(); | ||
} | ||
// @fold:off | ||
} |
43 changes: 43 additions & 0 deletions
43
docs/src/main/java/sample/dcr/RegisteredClientConfiguration.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/* | ||
* 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.dcr; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.oauth2.core.AuthorizationGrantType; | ||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod; | ||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; | ||
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 RegisteredClientConfiguration { | ||
@Bean // <1> | ||
public RegisteredClientRepository registeredClientRepository() { | ||
RegisteredClient initialClient = RegisteredClient.withId(UUID.randomUUID().toString()) | ||
.clientId("dcr-client") // <2> | ||
.clientSecret("{noop}secret") | ||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) | ||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // <3> | ||
.scope("client.create") // <4> | ||
.scope("client.read") // <5> | ||
.build(); | ||
|
||
return new InMemoryRegisteredClientRepository(initialClient); // <6> | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.