使い方: PKCE を使用した単一ページアプリケーションを使用した認証

このガイドでは、コード交換用の証明キー (PKCE) を使用してシングルページアプリケーション (SPA) をサポートするように Spring Authorization Server を構成する方法を説明します。このガイドの目的は、パブリッククライアントをサポートし、クライアント認証に PKCE を要求する方法を示すことです。

Spring Authorization Server はパブリッククライアントに対してリフレッシュトークンを発行しません。パブリッククライアントを公開する代わりに、フロントエンド用バックエンド (BFF) パターンをお勧めします。詳細については、gh-297 [GitHub] (英語) を参照してください。

CORS を有効にする

SPA は、さまざまな方法でデプロイできる静的リソースで構成されます。CDN や別個の Web サーバーなどを使用してバックエンドとは別にデプロイすることも、Spring Boot を使用してバックエンドと並行してデプロイすることもできます。

SPA が別のドメインでホストされている場合、Cross Origin Resource Sharing (CORS) を使用して、アプリケーションがバックエンドと通信できるようにすることができます。

例: Angular 開発サーバーがポート 4200 でローカルに実行されている場合、次の例のように、CorsConfigurationSource@Bean を定義し、cors() DSL を使用してプリフライトリクエストを許可するように Spring Security を構成できます。

CORS を有効にする
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
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 org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	@Order(1)
	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
			throws Exception {
		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
			.oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
		http
			// Redirect to the login page when not authenticated from the
			// authorization endpoint
			.exceptionHandling((exceptions) -> exceptions
				.defaultAuthenticationEntryPointFor(
					new LoginUrlAuthenticationEntryPoint("/login"),
					new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
				)
			)
			// Accept access tokens for User Info and/or Client Registration
			.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));

		return http.cors(Customizer.withDefaults()).build();
	}

	@Bean
	@Order(2)
	public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
			throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			// Form login handles the redirect to the login page from the
			// authorization server filter chain
			.formLogin(Customizer.withDefaults());

		return http.cors(Customizer.withDefaults()).build();
	}

	@Bean
	public CorsConfigurationSource corsConfigurationSource() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.addAllowedHeader("*");
		config.addAllowedMethod("*");
		config.addAllowedOrigin("http://127.0.0.1:4200");
		config.setAllowCredentials(true);
		source.registerCorsConfiguration("/**", config);
		return source;
	}

}
完全な例を表示するには、上のコードサンプルの 折りたたまれたテキストを展開する アイコンをクリックします。

パブリッククライアントを構成する

SPA は資格情報を安全に保存できないため、パブリッククライアント [IETF] (英語) として扱う必要があります。パブリッククライアントはコード交換用の証明キー [IETF] (英語) (PKCE) を使用する必要があります。

の例の続きとして、次の例のように、クライアント認証方式 none を使用してパブリッククライアントをサポートし、PKCE を要求するように Spring Authorization Server を構成できます。

  • ヤムル

  • Java

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          public-client:
            registration:
              client-id: "public-client"
              client-authentication-methods:
                - "none"
              authorization-grant-types:
                - "authorization_code"
              redirect-uris:
                - "http://127.0.0.1:4200"
              scopes:
                - "openid"
                - "profile"
            require-authorization-consent: true
            require-proof-key: true
@Bean
public RegisteredClientRepository registeredClientRepository() {
	RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
		.clientId("public-client")
		.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
		.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
		.redirectUri("http://127.0.0.1:4200")
		.scope(OidcScopes.OPENID)
		.scope(OidcScopes.PROFILE)
		.clientSettings(ClientSettings.builder()
			.requireAuthorizationConsent(true)
			.requireProofKey(true)
			.build()
		)
		.build();

	return new InMemoryRegisteredClientRepository(publicClient);
}
PKCE ダウングレード攻撃 [IETF] (英語) を防ぐために、requireProofKey 設定が重要です。

クライアントで認証する

サーバーがパブリッククライアントをサポートするように構成されたら、よくある質問は、「クライアントを認証してアクセストークンを取得するにはどうすればよいですか ? 」というものです。簡単に言うと、他のクライアントに対する場合と同じ方法です。

SPA はブラウザーベースのアプリケーションであるため、他のクライアントと同じリダイレクトベースのフローを使用します。この質問は通常、認証が REST API 経由で実行できるという期待に関連していますが、OAuth2 の場合はそうではありません。

より詳細な答えを得るには、OAuth2 と OpenID Connect に関連するフロー (この場合は認証コードフロー) を理解する必要があります。認証コードフローの手順は次のとおりです。

  1. クライアントは、認可エンドポイントへのリダイレクトを介して OAuth2 リクエストを開始します。パブリッククライアントの場合、このステップには code_verifier の生成と code_challenge の計算が含まれ、その後クエリパラメーターとして送信されます。

  2. ユーザーが認証されていない場合、認可サーバーはログインページにリダイレクトします。認証後、ユーザーは再び認可エンドポイントにリダイレクトされます。

  3. ユーザーがリクエストされたスコープに同意しておらず、同意が必要な場合は、同意ページが表示されます。

  4. ユーザーが同意すると、認可サーバーは authorization_code を生成し、redirect_uri 経由でクライアントにリダイレクトします。

  5. クライアントはクエリパラメーターを介して authorization_code を取得し、トークンエンドポイントへのリクエストを実行します。パブリッククライアントの場合、この手順には、認証用の資格情報の代わりに code_verifier パラメーターの送信が含まれます。

ご覧のとおり、フローはかなり複雑であり、この概要は表面をなぞっただけです。

認可コードフローを処理するには、シングルページアプリフレームワークでサポートされている堅牢なクライアント側ライブラリを使用することをお勧めします。