diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 8b426764b4..05f3af6227 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -11,3 +11,4 @@ ** xref:guides/how-to-userinfo.adoc[] ** xref:guides/how-to-jpa.adoc[] ** xref:guides/how-to-custom-claims-authorities.adoc[] +** xref:guides/how-to-dynamic-client-registration.adoc[] diff --git a/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc b/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc new file mode 100644 index 0000000000..5ef34250e2 --- /dev/null +++ b/docs/modules/ROOT/pages/guides/how-to-dynamic-client-registration.adoc @@ -0,0 +1,174 @@ +[[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 in Spring Authorization Server] +- xref:guides/how-to-dynamic-client-registration.adoc#configure-initial-client[Configure initial client] +- xref:guides/how-to-dynamic-client-registration.adoc#fetch-initial-access-token[Fetch initial access token] +- xref:guides/how-to-dynamic-client-registration.adoc#register-client[Register a client] +- xref:guides/how-to-dynamic-client-registration.adoc#fetch-client[Fetch client] + +[[enable]] +== Enable Dynamic Client Registration in Spring Authorization Server + +By default, dynamic client registration functionality is turned off 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 `initial-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. + +[[fetch-initial-access-token]] +== Fetch 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,console] +---- +curl -X POST --location "https://authserver.example.org/oauth2/token" --http1.1 \ + -H "Authorization: Basic " \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "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 +`:`. 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, register a new client with the following request. + +[NOTE] +The access token can only be used once. After a single registration request, the access token is invalidated. + +[source,console] +---- +curl -X POST --location "https://authserver.example.org/connect/register" --http1.1 \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer " \ + -d "{ + \"client_name\": \"My Example\", + \"grant_types\": [ + \"authorization_code\", + \"client_credentials\", + \"refresh_token\" + ], + \"scope\": \"openid profile email\", + \"redirect_uris\": [ + \"https://client.example.org/callback\", + \"https://client.example.org/callback2\" + ], + \"token_endpoint_auth_method\": \"client_secret_basic\", + \"post_logout_redirect_uris\": [ + \"https://client.example.org/logout\" + ] + }" +---- + +An example register client response may be as follows: + +[source,console] +---- +HTTP/1.1 201 +... + +{ + "client_id": "Q_AQ0wUzbTXo-IE4rNJbU9Dv8BBex1zQrjDeJs0mDbM", + "client_id_issued_at": 1690726915, + "client_name": "My Example", + "client_secret": "XleADJhomxA2Rmyom2hmpnS6_CDnyAFBI9JsGeC-XQ0QLa9p9JExXJABiYz7fOXA", + "redirect_uris": [ + "https://client.example.org/callback", + "https://client.example.org/callback2" + ], + "post_logout_redirect_uris": [ + "https://client.example.org/logout" + ], + "grant_types": [ + "refresh_token", + "client_credentials", + "authorization_code" + ], + "response_types": [ + "code" + ], + "scope": "openid profile email", + "token_endpoint_auth_method": "client_secret_basic", + "id_token_signed_response_alg": "RS256", + "registration_client_uri": "https://authserver.example.org/connect/register?client_id=Q_AQ0wUzbTXo-IE4rNJbU9Dv8BBex1zQrjDeJs0mDbM", + "registration_access_token": "", + "client_secret_expires_at": 0 +} +---- + +With the client registered, a `registration_access_token` and a `registration_client_uri` are provided to be able to +read the created client in a follow up request. The next step is optional. + +[[fetch-client]] +== Fetch client + +Using fields `registration_access_token` and `registration_client_uri` from the previous step's response, read the client +with the following request: + +[source,console] +---- +curl -X GET --location "" \ + -H "Authorization: Bearer " \ + -H "Accept: application/json" +---- + +The response should contain the same information about the client as seen when the client was first registered, with +the exception of `registration_access_token` field. diff --git a/docs/modules/ROOT/pages/how-to.adoc b/docs/modules/ROOT/pages/how-to.adoc index 69b6167d07..c725139c9e 100644 --- a/docs/modules/ROOT/pages/how-to.adoc +++ b/docs/modules/ROOT/pages/how-to.adoc @@ -12,3 +12,4 @@ * xref:guides/how-to-userinfo.adoc[Customize the OpenID Connect 1.0 UserInfo response] * xref:guides/how-to-jpa.adoc[Implement core services with JPA] * xref:guides/how-to-custom-claims-authorities.adoc[Add authorities as custom claims in JWT access tokens] +* xref:guides/how-to-dynamic-client-registration.adoc[Register a client dynamically] diff --git a/docs/src/main/java/sample/dcr/DcrConfiguration.java b/docs/src/main/java/sample/dcr/DcrConfiguration.java new file mode 100644 index 0000000000..ec29604b00 --- /dev/null +++ b/docs/src/main/java/sample/dcr/DcrConfiguration.java @@ -0,0 +1,26 @@ +package sample.dcr; + +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.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class DcrConfiguration { + @Bean // <1> + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .oidc(oidc -> + oidc.clientRegistrationEndpoint(Customizer.withDefaults()) // <2> + ); + + return http.build(); + } + +} diff --git a/docs/src/main/java/sample/dcr/RegisteredClientConfiguration.java b/docs/src/main/java/sample/dcr/RegisteredClientConfiguration.java new file mode 100644 index 0000000000..94f9246b20 --- /dev/null +++ b/docs/src/main/java/sample/dcr/RegisteredClientConfiguration.java @@ -0,0 +1,32 @@ +package sample.dcr; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +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(JdbcTemplate jdbcTemplate) { + RegisteredClient initialClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("initial-client") // <2> + .clientSecret("{noop}secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // <3> + .scope("client.create") // <4> + .scope("client.read") // <5> + .build(); + + JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); + registeredClientRepository.save(initialClient); // <6> + + return registeredClientRepository; + } +}