認可されたクライアント機能

このセクションでは、OAuth2 クライアント用に Spring Security が提供する追加機能について説明します。

承認済みクライアントの解決

@RegisteredOAuth2AuthorizedClient アノテーションは、メソッドパラメーターを型 OAuth2AuthorizedClient の引数値に解決する機能を提供します。これは、OAuth2AuthorizedClientManager または OAuth2AuthorizedClientService を使用して OAuth2AuthorizedClient にアクセスする場合に比べて便利な代替手段です。次の例は、@RegisteredOAuth2AuthorizedClient の使用方法を示しています。

  • Java

  • Kotlin

@Controller
public class OAuth2ClientController {

	@GetMapping("/")
	public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
		OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

		...

		return "index";
	}
}
@Controller
class OAuth2ClientController {
    @GetMapping("/")
    fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String {
        val accessToken = authorizedClient.accessToken

        ...

        return "index"
    }
}

@RegisteredOAuth2AuthorizedClient アノテーションは OAuth2AuthorizedClientArgumentResolver によって処理されます。OAuth2AuthorizedClientArgumentResolverOAuth2AuthorizedClientManager を直接使用するため、その機能を継承します。

RestClient 統合

RestClient のサポートは、OAuth2ClientHttpRequestInterceptor によって提供されます。このインターセプターは、送信リクエストの Authorization ヘッダーに Bearer トークンを配置することで、保護されたリソースリクエストを行う機能を提供します。インターセプターは OAuth2AuthorizedClientManager を直接使用するため、次の機能を継承します。

  • クライアントがまだ承認されていない場合は、OAuth 2.0 アクセストークンリクエストを実行して OAuth2AccessToken を取得します。

    • authorization_code: フローを開始するために認可リクエストのリダイレクトをトリガーします

    • client_credentials: アクセストークンはトークンエンドポイントから直接取得されます

    • password: アクセストークンはトークンエンドポイントから直接取得されます

    • 追加の許可型は、拡張許可型を有効にすることでサポートされます。

  • 既存の OAuth2AccessToken が期限切れの場合はリフレッシュされます (または更新)

次の例では、デフォルトの OAuth2AuthorizedClientManager を使用して、各リクエストの Authorization ヘッダーに Bearer トークンを配置することで、保護されたリソースにアクセスできる RestClient を構成します。

ClientHttpRequestInterceptor を使用して RestClient を構成する
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)

		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

}

clientRegistrationId の提供

OAuth2ClientHttpRequestInterceptor は、ClientRegistrationIdResolver を使用して、アクセストークンを取得するためにどのクライアントを使用するかを決定します。デフォルトでは、RequestAttributeClientRegistrationIdResolver を使用して、HttpRequest#attributes() から clientRegistrationId を解決します。

次の例は、属性を介して clientRegistrationId を提供する方法を示しています。

属性を介して clientRegistrationId を提供する
  • Java

  • Kotlin

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@Controller
public class ResourceController {

	private final RestClient restClient;

	public ResourceController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/")
	public String index() {
		String resourceUri = "...";

		String body = this.restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))   (1)
				.retrieve()
				.body(String.class);

		// ...

		return "index";
	}

}
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
import org.springframework.web.client.body

@Controller
class ResourceController(private restClient: RestClient) {

	@GetMapping("/")
	fun index(): String {
		val resourceUri = "..."

		val body: String = restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))   (1)
				.retrieve()
				.body<String>()

		// ...

		return "index"
	}

}
1clientRegistrationId() は RequestAttributeClientRegistrationIdResolver の static メソッドです。

あるいは、カスタム ClientRegistrationIdResolver を提供することもできます。次の例では、現在のユーザーから clientRegistrationId を解決するカスタム実装を構成します。

カスタム ClientRegistrationIdResolver で ClientHttpRequestInterceptor を構成する
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver());

		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

	private static ClientRegistrationIdResolver clientRegistrationIdResolver() {
		return (request) -> {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			return (authentication instanceof OAuth2AuthenticationToken principal)
					? principal.getAuthorizedClientRegistrationId() : null;
		};
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver())

		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

	fun clientRegistrationIdResolver(): ClientRegistrationIdResolver {
		return ClientRegistrationIdResolver { request ->
			val authentication = SecurityContextHolder.getContext().getAuthentication()
			return if (authentication instanceof OAuth2AuthenticationToken) {
				authentication.getAuthorizedClientRegistrationId()
			} else {
                null
			}
		}
	}

}

principal の提供

OAuth2ClientHttpRequestInterceptor は、PrincipalResolver を使用して、アクセストークンに関連付けられているプリンシパル名を決定します。これにより、アプリケーションは、保存されている OAuth2AuthorizedClient のスコープ方法を選択できます。デフォルトでは、SecurityContextHolderPrincipalResolver を使用して、SecurityContextHolder から現在の principal を解決します。

あるいは、次の例に示すように、RequestAttributePrincipalResolver を構成することによって、principal を HttpRequest#attributes() から解決することもできます。

RequestAttributePrincipalResolver を使用して ClientHttpRequestInterceptor を構成する
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		requestInterceptor.setPrincipalResolver(new RequestAttributePrincipalResolver());

		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		requestInterceptor.setPrincipalResolver(RequestAttributePrincipalResolver())

		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

}

次の例は、OAuth2AuthorizedClient のスコープを現在のユーザーではなくアプリケーションに設定する属性を介して principal 名を提供する方法を示しています。

属性を介して principal 名を提供する
  • Java

  • Kotlin

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;
import static org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal;

@Controller
public class ResourceController {

	private final RestClient restClient;

	public ResourceController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/")
	public String index() {
		String resourceUri = "...";

		String body = this.restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))
				.attributes(principal("my-application"))   (1)
				.retrieve()
				.body(String.class);

		// ...

		return "index";
	}

}
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
import org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal
import org.springframework.web.client.body

@Controller
class ResourceController(private restClient: RestClient) {

    @GetMapping("/")
	fun index(): String {
		val resourceUri = "..."

		val body: String = restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))
				.attributes(principal("my-application"))   (1)
				.retrieve()
				.body<String>()

		// ...

		return "index"
	}

}
1principal() は RequestAttributePrincipalResolver の static メソッドです。

失敗の処理

アクセストークンが何らかの理由で無効になった場合 (期限切れのトークンなど)、アクセストークンを削除して再度使用できないようにすることで、障害に対処することが有効です。アクセストークンを削除する OAuth2AuthorizationFailureHandler を提供することで、インターセプターがこれを自動的に実行するように設定できます。

次の例では、OAuth2AuthorizedClientRepository を使用して、HttpServletRequest のコンテキストで無効な OAuth2AuthorizedClient を削除する OAuth2AuthorizationFailureHandler を設定します。

OAuth2AuthorizedClientRepository を使用して OAuth2AuthorizationFailureHandler を構成する
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager,
			OAuth2AuthorizedClientRepository authorizedClientRepository) {

		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		OAuth2AuthorizationFailureHandler authorizationFailureHandler =
			OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository);
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler);

		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager,
			authorizedClientRepository: OAuth2AuthorizedClientRepository): RestClient {

		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)

		val authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor
			.authorizationFailureHandler(authorizedClientRepository)
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler)

		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

}

あるいは、次の例に示すように、OAuth2AuthorizedClientService を使用して、HttpServletRequest のコンテキスト外にある無効な OAuth2AuthorizedClient を削除することもできます。

OAuth2AuthorizedClientService を使用して OAuth2AuthorizationFailureHandler を構成する
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager,
			OAuth2AuthorizedClientService authorizedClientService) {

		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		OAuth2AuthorizationFailureHandler authorizationFailureHandler =
			OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientService);
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler);

		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager,
			authorizedClientService: OAuth2AuthorizedClientService): RestClient {

		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)

		val authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor
			.authorizationFailureHandler(authorizedClientService)
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler)

		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

}

WebClient サーブレット環境の統合

OAuth 2.0 クライアントのサポートは、ExchangeFilterFunction を使用して WebClient と統合されます。

ServletOAuth2AuthorizedClientExchangeFilterFunction は、OAuth2AuthorizedClient を使用し、関連付けられた OAuth2AccessToken をベアラートークンとして含めることにより、保護されたリソースをリクエストするためのメカニズムを提供します。OAuth2AuthorizedClientManager を直接使用するため、次の機能を継承します。

  • クライアントがまだ承認されていない場合は、OAuth2AccessToken がリクエストされます。

    • authorization_code: 認可リクエストのリダイレクトをトリガーして、フローを開始します。

    • client_credentials: アクセストークンは、トークンエンドポイントから直接取得されます。

    • password: アクセストークンは、トークンエンドポイントから直接取得されます。

  • OAuth2AccessToken の有効期限が切れている場合、認可を実行するために OAuth2AuthorizedClientProvider が使用可能であれば、OAuth2AccessToken はリフレッシュ(またはリフレッシュ)されます。

次のコードは、OAuth 2.0 クライアントサポートを使用して WebClient を構成する方法の例を示しています。

  • Java

  • Kotlin

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
	ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
			new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
	return WebClient.builder()
			.apply(oauth2Client.oauth2Configuration())
			.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
    val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build()
}

認定クライアントの提供

ServletOAuth2AuthorizedClientExchangeFilterFunction は、ClientRequest.attributes() (リクエスト属性)から OAuth2AuthorizedClient を解決することにより、(リクエストに)使用するクライアントを決定します。

次のコードは、OAuth2AuthorizedClient をリクエスト属性として設定する方法を示しています。

  • Java

  • Kotlin

@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
	String resourceUri = ...

	String body = webClient
			.get()
			.uri(resourceUri)
			.attributes(oauth2AuthorizedClient(authorizedClient))   (1)
			.retrieve()
			.bodyToMono(String.class)
			.block();

	...

	return "index";
}
@GetMapping("/")
fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String {
    val resourceUri: String = ...
    val body: String = webClient
            .get()
            .uri(resourceUri)
            .attributes(oauth2AuthorizedClient(authorizedClient)) (1)
            .retrieve()
            .bodyToMono()
            .block()

    ...

    return "index"
}
1oauth2AuthorizedClient() は ServletOAuth2AuthorizedClientExchangeFilterFunction の static メソッドです。

次のコードは、ClientRegistration.getRegistrationId() をリクエスト属性として設定する方法を示しています。

  • Java

  • Kotlin

@GetMapping("/")
public String index() {
	String resourceUri = ...

	String body = webClient
			.get()
			.uri(resourceUri)
			.attributes(clientRegistrationId("okta"))   (1)
			.retrieve()
			.bodyToMono(String.class)
			.block();

	...

	return "index";
}
@GetMapping("/")
fun index(): String {
    val resourceUri: String = ...

    val body: String = webClient
            .get()
            .uri(resourceUri)
            .attributes(clientRegistrationId("okta"))  (1)
            .retrieve()
            .bodyToMono()
            .block()

    ...

    return "index"
}
1clientRegistrationId() は ServletOAuth2AuthorizedClientExchangeFilterFunction の static メソッドです。

次のコードは、Authentication をリクエスト属性として設定する方法を示しています。

  • Java

  • Kotlin

@GetMapping("/")
public String index() {
	String resourceUri = ...

	Authentication anonymousAuthentication = new AnonymousAuthenticationToken(
			"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
	String body = webClient
			.get()
			.uri(resourceUri)
			.attributes(authentication(anonymousAuthentication))   (1)
			.retrieve()
			.bodyToMono(String.class)
			.block();

	...

	return "index";
}
@GetMapping("/")
fun index(): String {
    val resourceUri: String = ...

    val anonymousAuthentication: Authentication = AnonymousAuthenticationToken(
            "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))
    val body: String = webClient
            .get()
            .uri(resourceUri)
            .attributes(authentication(anonymousAuthentication))  (1)
            .retrieve()
            .bodyToMono()
            .block()

    ...

    return "index"
}
1authentication() は ServletOAuth2AuthorizedClientExchangeFilterFunction の static メソッドです。

すべての HTTP リクエストは、提供されたプリンシパルにバインドされたアクセストークンを受け取るため、この機能には注意することをお勧めします。

承認済みクライアントのデフォルト設定

OAuth2AuthorizedClient も ClientRegistration.getRegistrationId() もリクエスト属性として提供されていない場合、ServletOAuth2AuthorizedClientExchangeFilterFunction は、その構成に応じて、使用するデフォルトのクライアントを判別できます。

setDefaultOAuth2AuthorizedClient(true) が構成されていて、ユーザーが HttpSecurity.oauth2Login() を使用して認証されている場合、現在の OAuth2AuthenticationToken に関連付けられている OAuth2AccessToken が使用されます。

次のコードは、特定の構成を示しています。

  • Java

  • Kotlin

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
	ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
			new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
	oauth2Client.setDefaultOAuth2AuthorizedClient(true);
	return WebClient.builder()
			.apply(oauth2Client.oauth2Configuration())
			.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
    val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    oauth2Client.setDefaultOAuth2AuthorizedClient(true)
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build()
}

すべての HTTP リクエストはアクセストークンを受け取るため、この機能には注意してください。

あるいは、setDefaultClientRegistrationId("okta") が有効な ClientRegistration で構成されている場合、OAuth2AuthorizedClient に関連付けられた OAuth2AccessToken が使用されます。

次のコードは、特定の構成を示しています。

  • Java

  • Kotlin

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
	ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
			new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
	oauth2Client.setDefaultClientRegistrationId("okta");
	return WebClient.builder()
			.apply(oauth2Client.oauth2Configuration())
			.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
    val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    oauth2Client.setDefaultClientRegistrationId("okta")
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build()
}

すべての HTTP リクエストはアクセストークンを受け取るため、この機能には注意してください。