使い方: クライアントを動的に登録する

このガイドでは、Spring Authorization Server で OpenID Connect 動的クライアント登録を構成する方法を示し、クライアントを登録する方法の例を説明します。Spring Authorization Server は OpenID Connect 動的クライアント登録 1.0 (英語) 仕様を実装し、OpenID Connect クライアントを動的に登録および取得する機能を提供します。

動的クライアント登録を有効にする

デフォルトでは、Spring Authorization Server では動的クライアント登録機能が無効になっています。有効にするには、次の構成を追加します。

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.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;

import static sample.registration.CustomClientMetadataConfig.configureCustomClientMetadataConverters;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
				.oidc(oidc -> oidc.clientRegistrationEndpoint(clientRegistrationEndpoint -> {	(1)
					clientRegistrationEndpoint
							.authenticationProviders(configureCustomClientMetadataConverters());	(2)
				}));
		http.oauth2ResourceServer(oauth2ResourceServer ->
				oauth2ResourceServer.jwt(Customizer.withDefaults()));

		return http.build();
	}

}
1 デフォルト設定で OpenID Connect 1.0 クライアント登録エンドポイントを有効にします。
2 必要に応じて、カスタムクライアントメタデータパラメーターをサポートするようにデフォルトの AuthenticationProvider をカスタマイズします。

クライアントの登録時にカスタムクライアントメタデータパラメーターをサポートするには、実装の詳細をいくつか追加する必要があります。

次の例は、カスタムクライアントメタデータパラメーター (logo_uri および contacts) をサポートし、OidcClientRegistrationAuthenticationProvider および OidcClientConfigurationAuthenticationProvider で構成されている Converter のサンプル実装を示しています。

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;

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;

public class CustomClientMetadataConfig {

	public static Consumer<List<AuthenticationProvider>> configureCustomClientMetadataConverters() {	(1)
		List<String> customClientMetadata = List.of("logo_uri", "contacts");	(2)

		return (authenticationProviders) -> {
			CustomRegisteredClientConverter registeredClientConverter =
					new CustomRegisteredClientConverter(customClientMetadata);
			CustomClientRegistrationConverter clientRegistrationConverter =
					new CustomClientRegistrationConverter(customClientMetadata);

			authenticationProviders.forEach((authenticationProvider) -> {
				if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
					provider.setRegisteredClientConverter(registeredClientConverter);	(3)
					provider.setClientRegistrationConverter(clientRegistrationConverter);	(4)
				}
				if (authenticationProvider instanceof OidcClientConfigurationAuthenticationProvider provider) {
					provider.setClientRegistrationConverter(clientRegistrationConverter);	(5)
				}
			});
		};
	}

	private static class CustomRegisteredClientConverter
			implements Converter<OidcClientRegistration, RegisteredClient> {

		private final List<String> customClientMetadata;
		private final OidcClientRegistrationRegisteredClientConverter delegate;

		private CustomRegisteredClientConverter(List<String> customClientMetadata) {
			this.customClientMetadata = customClientMetadata;
			this.delegate = new OidcClientRegistrationRegisteredClientConverter();
		}

		@Override
		public RegisteredClient convert(OidcClientRegistration clientRegistration) {
			RegisteredClient registeredClient = this.delegate.convert(clientRegistration);
			ClientSettings.Builder clientSettingsBuilder = ClientSettings.withSettings(
					registeredClient.getClientSettings().getSettings());
			if (!CollectionUtils.isEmpty(this.customClientMetadata)) {
				clientRegistration.getClaims().forEach((claim, value) -> {
					if (this.customClientMetadata.contains(claim)) {
						clientSettingsBuilder.setting(claim, value);
					}
				});
			}

			return RegisteredClient.from(registeredClient)
					.clientSettings(clientSettingsBuilder.build())
					.build();
		}
	}

	private static class CustomClientRegistrationConverter
			implements Converter<RegisteredClient, OidcClientRegistration> {

		private final List<String> customClientMetadata;
		private final RegisteredClientOidcClientRegistrationConverter delegate;

		private CustomClientRegistrationConverter(List<String> customClientMetadata) {
			this.customClientMetadata = customClientMetadata;
			this.delegate = new RegisteredClientOidcClientRegistrationConverter();
		}

		@Override
		public OidcClientRegistration convert(RegisteredClient registeredClient) {
			OidcClientRegistration clientRegistration = this.delegate.convert(registeredClient);
			Map<String, Object> claims = new HashMap<>(clientRegistration.getClaims());
			if (!CollectionUtils.isEmpty(this.customClientMetadata)) {
				ClientSettings clientSettings = registeredClient.getClientSettings();
				claims.putAll(this.customClientMetadata.stream()
						.filter(metadata -> clientSettings.getSetting(metadata) != null)
						.collect(Collectors.toMap(Function.identity(), clientSettings::getSetting)));
			}

			return OidcClientRegistration.withClaims(claims).build();
		}

	}

}
1 デフォルトの AuthenticationProvider をカスタマイズできる機能を提供する Consumer<List<AuthenticationProvider>> を定義します。
2 クライアント登録でサポートされるカスタムクライアントメタデータパラメーターを定義します。
3OidcClientRegistrationAuthenticationProvider.setRegisteredClientConverter() を CustomRegisteredClientConverter で構成します。
4OidcClientRegistrationAuthenticationProvider.setClientRegistrationConverter() を CustomClientRegistrationConverter で構成します。
5OidcClientConfigurationAuthenticationProvider.setClientRegistrationConverter() を CustomClientRegistrationConverter で構成します。

クライアントレジストラを構成する

既存のクライアントは、新しいクライアントを認可サーバーに登録するために使用されます。クライアントの登録とクライアントの取得には、それぞれスコープ client.create とオプションで client.read を使用してクライアントを構成する必要があります。次のリストはクライアントの例を示しています。

import java.util.UUID;

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;

@Configuration
public class ClientConfig {

	@Bean
	public RegisteredClientRepository registeredClientRepository() {
		RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
				.clientId("registrar-client")
				.clientSecret("{noop}secret")
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)	(1)
				.scope("client.create")	(2)
				.scope("client.read")	(3)
				.build();

		return new InMemoryRegisteredClientRepository(registrarClient);
	}

}
1client_credentials 許可型は、アクセストークンを直接取得するように構成されています。
2client.create スコープは、クライアントが新しいクライアントを登録できるように構成されています。
3client.read スコープは、クライアントが登録されたクライアントを取得できるように構成されています。

初期アクセストークンの取得

クライアント登録リクエストには「初期」アクセストークンが必要です。アクセストークンリクエスト MUST には、scope パラメーター値 client.create のみが含まれます。

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

クライアント登録リクエストには、client.create の単一スコープを持つアクセストークンが必要です。アクセストークンに追加のスコープが含まれている場合、クライアント登録リクエストは拒否されます。

上記のリクエストのエンコードされた資格情報を取得するには、base64 はクライアントの資格情報を <clientId>:<clientSecret> の形式でエンコードします。以下は、このガイドの例のエンコード操作です。

echo -n "registrar-client:secret" | base64

クライアントを登録する

前の手順で取得したアクセストークンを使用して、クライアントを動的に登録できるようになります。

「初期」アクセストークンは 1 回のみ使用できます。クライアント登録後、アクセストークンは無効化されます。
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 {
	private final WebClient webClient;

	public ClientRegistrar(WebClient webClient) {
		this.webClient = webClient;
	}

	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) {
	}

	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,
		 	@JsonProperty("logo_uri") String logoUri,
		 	List<String> contacts,
			String scope) {
	}

	public void exampleRegistration(String initialAccessToken) {	(3)
		ClientRegistrationRequest clientRegistrationRequest = new ClientRegistrationRequest(	(4)
				"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"
		);

		ClientRegistrationResponse clientRegistrationResponse =
				registerClient(initialAccessToken, clientRegistrationRequest);	(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());
		assert (clientRegistrationResponse.logoUri().contentEquals("https://client.example.org/logo"));
		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();

		ClientRegistrationResponse retrievedClient = retrieveClient(registrationAccessToken, registrationClientUri);	(8)

		assert (retrievedClient.clientName().contentEquals("client-1"));	(9)
		assert (!Objects.isNull(retrievedClient.clientId()));
		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 (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());
	}

	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();
	}

}
1 クライアント登録リクエストの最小限の表現。クライアント登録リクエスト (英語) に従って、追加のクライアントメタデータパラメーターを追加できます。このリクエスト例には、カスタムクライアントメタデータパラメーター logo_uri および contacts が含まれています。
2 クライアント登録レスポンスの最小限の表現。クライアント登録レスポンス (英語) に従って、追加のクライアントメタデータパラメーターを追加できます。このレスポンス例には、カスタムクライアントメタデータパラメーター logo_uri および contacts が含まれています。
3 クライアントの登録とクライアントの取得を示す例。
4 サンプルのクライアント登録リクエストオブジェクト。
5「初期」アクセストークンとクライアント登録リクエストオブジェクトを使用してクライアントを登録します。
6 登録が成功したら、レスポンスに入力する必要があるクライアントメタデータパラメーターをアサートします。
7 新しく登録されたクライアントの取得に使用するために、registration_access_token および registration_client_uri レスポンスパラメーターを抽出します。
8registration_access_token および registration_client_uri を使用してクライアントを取得します。
9 クライアントを取得した後、レスポンスに入力する必要があるクライアントメタデータパラメーターをアサートします。
10WebClient を使用してクライアント登録リクエスト (英語) をサンプルします。
11WebClient を使用してクライアント読み取りリクエスト (英語) をサンプルします。
クライアント読み取りレスポンス (英語) には、registration_access_token パラメーターを除き、クライアント登録レスポンス (英語) と同じクライアントメタデータパラメーターが含まれている必要があります。