12. OAuth2

12.1 OAuth 2.0 ログイン

OAuth 2.0 ログイン機能は、OAuth 2.0 プロバイダー(例: GitHub)または OpenID Connect 1.0 プロバイダー(Google など)の既存のアカウントを使用してユーザーにアプリケーションにログインさせる機能をアプリケーションに提供します。OAuth 2.0 Login は、「Google でログイン」または「GitHub でログイン」というユースケースを実装しています。

[Note] メモ

OZZ 2.0 Login は、OAuth 2.0 認証フレームワーク (英語) および OpenID Connect コア 1.0 (英語) で指定されている 認証コード付与を使用して実装されます。

12.1.1 Spring Boot 2.x サンプル

Spring Boot 2.x は、OAuth 2.0 ログインの完全な自動構成機能を提供します。

このセクションでは、 Google 認証プロバイダーとして使用して OAuth 2.0 ログインサンプルを構成する方法を示し、次のトピックを扱います。

初期設定

ログインに Google の OAuth 2.0 認証システムを使用するには、Google API Console でプロジェクトを設定して OAuth 2.0 認証情報を取得する必要があります。

[Note] メモ

認証用の Google の OAuth 2.0 の実装 (英語) OpenID Connect 1.0 (英語) 仕様に準拠しており、OpenID 認定 (英語) です。

「OAuth 2.0 のセットアップ」セクションから始まる OpenID Connect (英語) ページの指示に従ってください。

「OAuth 2.0 資格情報の取得」の手順を完了すると、クライアント ID とクライアントシークレットで構成される資格情報を持つ新しい OAuth クライアントが必要になります。

リダイレクト URI の設定

リダイレクト URI は、エンドユーザーのユーザーエージェントが Google で認証 れ、同意ページで OAuth クライアント 前の手順で作成された へのアクセスを許可した後にリダイレクトされるアプリケーション内のパスです。

「リダイレクト URI の設定」サブセクションで、 承認されたリダイレクト URI フィールドが  http://localhost:8080/login/oauth2/code/google に設定されていることを確認します。

[Tip] ヒント

デフォルトのリダイレクト URI テンプレートは  {baseUrl}/login/oauth2/code/{registrationId} です。 registrationId ClientRegistration の一意の識別子です。

[Important] 重要

OAuth クライアントがプロキシサーバーの背後で実行されている場合は、プロキシサーバー構成をチェックして、アプリケーションが正しく構成されていることを確認することをお勧めします。また、 redirect-uri でサポートされている URI テンプレート変数も参照してください。

application.yml を設定する

Google で新しい OAuth クライアントを作成したため、 認証フローに OAuth クライアントを使用するようにアプリケーションを構成する必要があります。そうするには:

  1. application.yml に移動して、次の構成を設定します。

    spring:
      security:
        oauth2:
          client:
            registration:   1
              google:   2
                client-id: google-client-id
                client-secret: google-client-secret

    例 12.1: OAuth クライアントのプロパティ

    1

    spring.security.oauth2.client.registration は、OAuth クライアントプロパティの基本プロパティプレフィックスです。

    2

    ベースプロパティプレフィックスの後には、google などの ClientRegistration の ID が続きます。


  2. client-id および  client-secret プロパティの値を、前に作成した OAuth 2.0 資格情報に置き換えます。

アプリケーションを起動する

Spring Boot 2.x サンプルを起動し、 http://localhost:8080 に移動します。次に、Google へのリンクを表示するデフォルトの 自動生成されたログインページにリダイレクトされます。

Google リンクをクリックすると、認証のために Google にリダイレクトされます。

Google アカウントの認証情報で認証した後、表示される次のページは同意画面です。同意画面では、以前に作成した OAuth クライアントへのアクセスを認可または拒否するように求められます。 許可するをクリックして、OAuth クライアントがメールアドレスと基本的なプロファイル情報にアクセスすることを認可します。

この時点で、OAuth クライアントは UserInfo エンドポイント (英語) からメールアドレスと基本プロファイル情報を取得し、認証済みセッションを確立します。

12.1.2 Spring Boot 2.x プロパティマッピング

次の表に、Spring Boot 2.x OAuth クライアントプロパティの ClientRegistration プロパティへのマッピングの概要を示します。

Spring Boot 2.x ClientRegistration

spring.security.oauth2.client.registration. [registrationId]

registrationId

spring.security.oauth2.client.registration. [registrationId].client-id

clientId

spring.security.oauth2.client.registration. [registrationId].client-secret

clientSecret

spring.security.oauth2.client.registration. [registrationId].client-authentication-method

clientAuthenticationMethod

spring.security.oauth2.client.registration. [registrationId].authorization-grant-type

authorizationGrantType

spring.security.oauth2.client.registration. [registrationId].redirect-uri

redirectUriTemplate

spring.security.oauth2.client.registration. [registrationId].scope

scopes

spring.security.oauth2.client.registration. [registrationId].client-name

clientName

spring.security.oauth2.client.provider. [providerId].authorization-uri

providerDetails.authorizationUri

spring.security.oauth2.client.provider. [providerId].token-uri

providerDetails.tokenUri

spring.security.oauth2.client.provider. [providerId].jwk-set-uri

providerDetails.jwkSetUri

spring.security.oauth2.client.provider. [providerId].user-info-uri

providerDetails.userInfoEndpoint.uri

spring.security.oauth2.client.provider. [providerId].user-info-authentication-method

providerDetails.userInfoEndpoint.authenticationMethod

spring.security.oauth2.client.provider. [providerId].userNameAttribute

providerDetails.userInfoEndpoint.userNameAttributeName

[Tip] ヒント

ClientRegistration は、 spring.security.oauth2.client.provider. [providerId].issuer-uri プロパティを指定することにより、OpenID Connect プロバイダーの構成エンドポイント (英語) または認可サーバーのメタデータエンドポイント (英語) の検出を使用して最初に構成できます。

12.1.3 CommonOAuth2Provider

CommonOAuth2Provider は、多数の有名なプロバイダー(Google、GitHub、Facebook、Okta)の一連のデフォルトクライアントプロパティを事前に定義しています。

例:  authorization-uri token-uri user-info-uri は、プロバイダーでは頻繁に変更されません。必要な構成を減らすためにデフォルト値を提供することは理にかなっています。

前に示した ように、Google クライアント構成したときは、 client-id プロパティと  client-secret プロパティのみが必要です。

次のリストに例を示します。

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-client-secret
[Tip] ヒント

registrationId ( google)は  CommonOAuth2Provider の  GOOGLE enum (大文字と小文字を区別しない)と一致するため、クライアントプロパティの自動デフォルト設定はここでシームレスに機能します。

google-login など、別の  registrationId を指定する場合は、 provider プロパティを構成することにより、クライアントプロパティの自動デフォルト設定を活用できます。

次のリストに例を示します。

spring:
  security:
    oauth2:
      client:
        registration:
          google-login: 1
            provider: google    2
            client-id: google-client-id
            client-secret: google-client-secret

1

registrationId は  google-login に設定されます。

2

provider プロパティは  google に設定され、 CommonOAuth2Provider.GOOGLE.getBuilder() で設定されたクライアントプロパティの自動デフォルト設定を活用します。

12.1.4 カスタムプロバイダープロパティの構成

マルチテナンシーをサポートする OAuth 2.0 プロバイダーがいくつかあります。これにより、テナント(またはサブドメイン)ごとに異なるプロトコルエンドポイントが作成されます。

例: Okta に登録された OAuth クライアントは特定のサブドメインに割り当てられ、独自のプロトコルエンドポイントを持ちます。

これらの場合のために、Spring Boot 2.x はカスタムプロバイダープロパティを構成するための次の基本プロパティを提供します:  spring.security.oauth2.client.provider. [providerId]

次のリストに例を示します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
        provider:
          okta: 1
            authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
            token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
            user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
            user-name-attribute: sub
            jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys

1

基本プロパティ( spring.security.oauth2.client.provider.okta)により、プロトコルエンドポイントの場所をカスタム構成できます。

12.1.5 Spring Boot 2.x 自動構成のオーバーライド

OAuth クライアントをサポートするための Spring Boot 2.x 自動構成クラスは  OAuth2ClientAutoConfiguration です。

次のタスクを実行します。

  • 構成された OAuth クライアントプロパティからの  ClientRegistration で構成される  ClientRegistrationRepository @Bean を登録します。
  • WebSecurityConfigurerAdapter @Configuration を提供し、 httpSecurity.oauth2Login() を介した OAuth 2.0 ログインを有効にします。

特定の要件に基づいて自動構成をオーバーライドする必要がある場合、次の方法でオーバーライドできます。

ClientRegistrationRepository @Bean を登録する

次の例は、 ClientRegistrationRepository @Bean を登録する方法を示しています。

@Configuration
public class OAuth2LoginConfig {

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope("openid", "profile", "email", "address", "phone")
            .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
            .tokenUri("https://www.googleapis.com/oauth2/v4/token")
            .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
            .userNameAttributeName(IdTokenClaimNames.SUB)
            .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
            .clientName("Google")
            .build();
    }
}

WebSecurityConfigurerAdapter を提供する

次の例は、 WebSecurityConfigurerAdapter に  @EnableWebSecurity を提供し、 httpSecurity.oauth2Login() を介して OAuth 2.0 ログインを有効にする方法を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .anyRequest().authenticated()
            )
            .oauth2Login(withDefaults());
    }
}

自動構成を完全にオーバーライドする

次の例は、 ClientRegistrationRepository @Bean を登録し、 WebSecurityConfigurerAdapter を提供することにより、自動構成を完全にオーバーライドする方法を示しています。

@Configuration
public class OAuth2LoginConfig {

    @EnableWebSecurity
    public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests(authorizeRequests ->
                    authorizeRequests
                        .anyRequest().authenticated()
                )
                .oauth2Login(withDefaults());
        }
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope("openid", "profile", "email", "address", "phone")
            .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
            .tokenUri("https://www.googleapis.com/oauth2/v4/token")
            .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
            .userNameAttributeName(IdTokenClaimNames.SUB)
            .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
            .clientName("Google")
            .build();
    }
}

12.1.6 Spring Boot 2.x を使用しない Java 構成

Spring Boot 2.x を使用できず、 CommonOAuth2Provider で事前定義されたプロバイダーの 1 つ(Google など)を構成する場合は、次の構成を適用します。

@Configuration
public class OAuth2LoginConfig {

    @EnableWebSecurity
    public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests(authorizeRequests ->
                    authorizeRequests
                        .anyRequest().authenticated()
                )
                .oauth2Login(withDefaults());
        }
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
            ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }

    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }

    private ClientRegistration googleClientRegistration() {
        return CommonOAuth2Provider.GOOGLE.getBuilder("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .build();
    }
}

12.1.7 高度な構成

HttpSecurity.oauth2Login() は、OAuth 2.0 ログインをカスタマイズするための多くの構成オプションを提供します。主な構成オプションは、対応するプロトコルエンドポイントにグループ化されます。

例:  oauth2Login().authorizationEndpoint() では 認証エンドポイントを構成できますが、 oauth2Login().tokenEndpoint() では トークンエンドポイントを構成できます。

次のコードは例を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .authorizationEndpoint(authorizationEndpoint ->
                        authorizationEndpoint
                            ...
                    )
                    .redirectionEndpoint(redirectionEndpoint ->
                        redirectionEndpoint
                            ...
                    )
                    .tokenEndpoint(tokenEndpoint ->
                        tokenEndpoint
                            ...
                    )
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            ...
                    )
            );
    }
}

oauth2Login() DSL の主なゴールは、仕様で定義されているように、命名に厳密に整合することでした。

OAuth 2.0 認可フレームワークは、プロトコルエンドポイント (英語) を次のように定義します。

認可プロセスでは、2 つの認可サーバーエンドポイント(HTTP リソース)を使用します。

  • 認可エンドポイント: クライアントがユーザーエージェントリダイレクトを介してリソース所有者から認可を取得するために使用します。
  • トークンエンドポイント: 通常はクライアント認証で、アクセストークンの認可付与を交換するためにクライアントによって使用されます。

1 つのクライアントエンドポイントと同様に:

  • リダイレクトエンドポイント: リソース所有者のユーザーエージェントを介してクライアントに認証資格情報を含むレスポンスを返すために認証サーバーによって使用されます。

OpenID Connect Core 1.0 仕様では、UserInfo エンドポイント (英語) を次のように定義しています。

UserInfo エンドポイントは、認証されたエンドユーザーに関するクレームを返す OAuth 2.0 保護リソースです。エンドユーザーに関するリクエストされたクレームを取得するために、クライアントは OpenID Connect 認証を通じて取得されたアクセストークンを使用して UserInfo エンドポイントにリクエストを行います。通常、これらのクレームは、クレームの名前と値のペアのコレクションを含む JSON オブジェクトによって表されます。

次のコードは、 oauth2Login() DSL で使用可能な完全な構成オプションを示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .clientRegistrationRepository(this.clientRegistrationRepository())
                    .authorizedClientRepository(this.authorizedClientRepository())
                    .authorizedClientService(this.authorizedClientService())
                    .loginPage("/login")
                    .authorizationEndpoint(authorizationEndpoint ->
                        authorizationEndpoint
                            .baseUri(this.authorizationRequestBaseUri())
                            .authorizationRequestRepository(this.authorizationRequestRepository())
                            .authorizationRequestResolver(this.authorizationRequestResolver())
                    )
                    .redirectionEndpoint(redirectionEndpoint ->
                         redirectionEndpoint
                            .baseUri(this.authorizationResponseBaseUri())
                    )
                    .tokenEndpoint(tokenEndpoint ->
                        tokenEndpoint
                            .accessTokenResponseClient(this.accessTokenResponseClient())
                    )
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            .userAuthoritiesMapper(this.userAuthoritiesMapper())
                            .userService(this.oauth2UserService())
                            .oidcUserService(this.oidcUserService())
                            .customUserType(GitHubOAuth2User.class, "github")
                    )
            );
    }
}

以下のセクションでは、使用可能な各構成オプションについて詳しく説明します。

OAuth 2.0 ログインページ

デフォルトでは、OAuth 2.0 ログインページは  DefaultLoginPageGeneratingFilter によって自動生成されます。デフォルトのログインページには、設定された各 OAuth クライアントとその  ClientRegistration.clientName がリンクとして表示され、認可リクエスト(または OAuth 2.0 ログイン)を開始できます。

[Note] メモ

DefaultLoginPageGeneratingFilter が構成済みの OAuth クライアントのリンクを表示するには、登録された  ClientRegistrationRepository が  Iterable<ClientRegistration> も実装する必要があります。参考のために  InMemoryClientRegistrationRepository を参照してください。

各 OAuth クライアントのリンクの宛先は、デフォルトで次のようになります。

OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"

次の行に例を示します。

<a href="/oauth2/authorization/google">Google</a>

デフォルトのログインページをオーバーライドするには、 oauth2Login().loginPage() および(オプションで)  oauth2Login().authorizationEndpoint().baseUri() を構成します。

次のリストに例を示します。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .loginPage("/login/oauth2")
                    ...
                    .authorizationEndpoint(authorizationEndpoint ->
                        authorizationEndpoint
                            .baseUri("/login/oauth2/authorization")
                            ...
                    )
            );
    }
}
[Important] 重要

カスタムログインページを表示できる  @RequestMapping("/login/oauth2") を備えた  @Controller を提供する必要があります。

[Tip] ヒント

前述のように、 oauth2Login().authorizationEndpoint().baseUri() の構成はオプションです。ただし、カスタマイズする場合は、各 OAuth クライアントへのリンクが  authorizationEndpoint().baseUri() と一致することを確認してください。

次の行に例を示します。

<a href="/login/oauth2/authorization/google">Google</a>

リダイレクトエンドポイント

リダイレクトエンドポイントは、認可サーバーがリソース所有者ユーザーエージェントを介してクライアントに認可レスポンス(認可資格情報を含む)を返すために使用されます。

[Tip] ヒント

OAuth 2.0 Login は認証コード付与を活用します。認証情報は認証コードです。

デフォルトの Authorization Response  baseUri (リダイレクトエンドポイント)は  /login/oauth2/code/* であり、 OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI で定義されています。

Authorization Response  baseUri をカスタマイズする場合は、次の例に示すように構成します。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .redirectionEndpoint(redirectionEndpoint ->
                        redirectionEndpoint
                            .baseUri("/login/oauth2/callback/*")
                            ...
                    )
            );
    }
}
[Important] 重要

また、 ClientRegistration.redirectUriTemplate がカスタム Authorization Response  baseUri と一致することを確認する必要があります。

次のリストに例を示します。

return CommonOAuth2Provider.GOOGLE.getBuilder("google")
    .clientId("google-client-id")
    .clientSecret("google-client-secret")
    .redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}")
    .build();

UserInfo エンドポイント

UserInfo エンドポイントには、次のサブセクションで説明するように、いくつかの構成オプションが含まれています。

ユーザー権限のマッピング

ユーザーが OAuth 2.0 プロバイダーで正常に認証された後、 OAuth2User.getAuthorities() (または  OidcUser.getAuthorities())が新しいセットの  GrantedAuthority インスタンスにマッピングされ、認証の補完時に  OAuth2AuthenticationToken に提供されます。

[Tip] ヒント

OAuth2AuthenticationToken.getAuthorities() は、 hasRole('USER') や  hasRole('ADMIN') などでリクエストを認可するために使用されます。

ユーザー権限をマッピングするときに選択できるオプションがいくつかあります。

GrantedAuthoritiesMapper を使用する

GrantedAuthoritiesMapper の実装を提供し、次の例に示すように構成します。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            .userAuthoritiesMapper(this.userAuthoritiesMapper())
                            ...
                    )
            );
    }

    private GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

                    // Map the claims found in idToken and/or userInfo
                    // to one or more GrantedAuthority's and add it to mappedAuthorities

                } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;

                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                    // Map the attributes found in userAttributes
                    // to one or more GrantedAuthority's and add it to mappedAuthorities

                }
            });

            return mappedAuthorities;
        };
    }
}

または、次の例に示すように、 GrantedAuthoritiesMapper @Bean を登録して、構成に自動的に適用することもできます。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(withDefaults());
    }

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        ...
    }
}
OAuth2UserService を使用した委譲ベースの戦略

この戦略は  GrantedAuthoritiesMapper を使用するよりも高度ですが、 OAuth2UserRequest および  OAuth2User (OAuth 2.0 UserService を使用する場合)または  OidcUserRequest および  OidcUser (OpenID Connect 1.0 UserService を使用する場合)にアクセスできるため、より柔軟です。

OAuth2UserRequest (および  OidcUserRequest)は、関連する  OAuth2AccessToken へのアクセスを提供します。これは、ユーザーのカスタム権限をマップする前に、 委譲者が保護リソースから権限情報をフェッチする必要がある場合に非常に便利です。

次の例は、OpenID Connect 1.0 UserService を使用して、委譲ベースの戦略を実装および構成する方法を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            .oidcUserService(this.oidcUserService())
                            ...
                    )
            );
    }

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcUserService delegate = new OidcUserService();

        return (userRequest) -> {
            // Delegate to the default implementation for loading a user
            OidcUser oidcUser = delegate.loadUser(userRequest);

            OAuth2AccessToken accessToken = userRequest.getAccessToken();
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            // TODO
            // 1) Fetch the authority information from the protected resource using accessToken
            // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

            // 3) Create a copy of oidcUser but use the mappedAuthorities instead
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

            return oidcUser;
        };
    }
}
カスタム OAuth2User の構成

CustomUserTypesOAuth2UserService は、カスタム  OAuth2User タイプのサポートを提供する  OAuth2UserService の実装です。

デフォルトの実装( DefaultOAuth2User)がニーズに合わない場合は、 OAuth2User の独自の実装を定義できます。

次のコードは、GitHub のカスタム  OAuth2User タイプを登録する方法を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            .customUserType(GitHubOAuth2User.class, "github")
                            ...
                    )
            );
    }
}

次のコードは、GitHub のカスタム  OAuth2User タイプの例を示しています。

public class GitHubOAuth2User implements OAuth2User {
    private List<GrantedAuthority> authorities =
        AuthorityUtils.createAuthorityList("ROLE_USER");
    private Map<String, Object> attributes;
    private String id;
    private String name;
    private String login;
    private String email;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        if (this.attributes == null) {
            this.attributes = new HashMap<>();
            this.attributes.put("id", this.getId());
            this.attributes.put("name", this.getName());
            this.attributes.put("login", this.getLogin());
            this.attributes.put("email", this.getEmail());
        }
        return attributes;
    }

    public String getId() {
        return this.id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLogin() {
        return this.login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getEmail() {
        return this.email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
[Tip] ヒント

id name login email は、GitHub の UserInfo レスポンスで返される属性です。UserInfo エンドポイントから返される詳細情報については、「認証されたユーザーを取得する」 (英語) の API ドキュメントを参照してください。

OAuth 2.0 UserService

DefaultOAuth2UserService は、標準の OAuth 2.0 プロバイダーをサポートする  OAuth2UserService の実装です。

[Note] メモ

OAuth2UserService は、エンドユーザー(リソース所有者)のユーザー属性を UserInfo エンドポイントから取得し(認可フロー中にクライアントに付与されたアクセストークンを使用して)、 AuthenticatedPrincipal を  OAuth2User の形式で返します。

DefaultOAuth2UserService は、UserInfo エンドポイントでユーザー属性をリクエストするときに  RestOperations を使用します。

UserInfo リクエストの前処理をカスタマイズする必要がある場合は、 DefaultOAuth2UserService.setRequestEntityConverter() にカスタム  Converter<OAuth2UserRequest, RequestEntity<?>> を提供できます。デフォルトの実装  OAuth2UserRequestEntityConverter は、デフォルトで  Authorization ヘッダーに  OAuth2AccessToken を設定する UserInfo リクエストの  RequestEntity 表現を構築します。

一方、UserInfo レスポンスのリアクティブ処理をカスタマイズする必要がある場合は、 DefaultOAuth2UserService.setRestOperations() にカスタム構成の  RestOperations を提供する必要があります。デフォルトの  RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラー(400 間違ったリクエスト)を処理できる  ResponseErrorHandler です。OAuth 2.0 Error パラメーターを  OAuth2Error に変換するために  OAuth2ErrorHttpMessageConverter を使用します。

DefaultOAuth2UserService をカスタマイズする場合でも、 OAuth2UserService の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            .userService(this.oauth2UserService())
                            ...
                    )
            );
    }

    private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        ...
    }
}
OpenID Connect 1.0 UserService

OidcUserService は、OpenID Connect 1.0 プロバイダーをサポートする  OAuth2UserService の実装です。

OidcUserService は、UserInfo エンドポイントでユーザー属性をリクエストするときに  DefaultOAuth2UserService を活用します。

UserInfo リクエストの前処理および / または UserInfo レスポンスのリアクティブ処理をカスタマイズする必要がある場合、 OidcUserService.setOauth2UserService() にカスタム構成の  DefaultOAuth2UserService を提供する必要があります。

OidcUserService をカスタマイズするか、OpenID Connect 1.0 プロバイダーの  OAuth2UserService の独自の実装を提供するかどうかにかかわらず、次の例に示すように構成する必要があります。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint
                            .oidcUserService(this.oidcUserService())
                            ...
                    )
            );
    }

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        ...
    }
}

ID トークン署名検証

OpenID Connect 1.0 認証は ID トークン (英語) を導入します。ID トークン (英語) は、クライアントが使用する場合の認可サーバーによるエンドユーザーの認証に関するクレームを含むセキュリティトークンです。

ID トークンは JSON Web トークン (英語) (JWT)として表され、JSON Web 署名 (英語) (JWS)を使用して署名する必要があります。

OidcIdTokenDecoderFactory は、 OidcIdToken 署名検証に使用される  JwtDecoder を提供します。デフォルトのアルゴリズムは  RS256 ですが、クライアントの登録時に割り当てられる場合は異なる場合があります。これらの場合、特定のクライアントに割り当てられた予想される JWS アルゴリズムを返すようにリゾルバーを構成できます。

JWS アルゴリズムリゾルバーは、 ClientRegistration を受け入れ、クライアントに期待される  JwsAlgorithm を返す  Function です。 SignatureAlgorithm.RS256 または  MacAlgorithm.HS256

次のコードは、すべての  ClientRegistration に対してデフォルトで  MacAlgorithm.HS256 になるように  OidcIdTokenDecoderFactory @Bean を構成する方法を示しています。

@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
    OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
    idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
    return idTokenDecoderFactory;
}
[Note] メモ

HS256 HS384 または  HS512 などの MAC ベースのアルゴリズムの場合、 client-id に対応する  client-secret が署名検証の対称キーとして使用されます。

[Tip] ヒント

OpenID Connect 1.0 認証用に複数の  ClientRegistration が構成されている場合、JWS アルゴリズムリゾルバーは提供された  ClientRegistration を評価して、返すアルゴリズムを決定します。

OpenID Connect 1.0 ログアウト

OpenID Connect セッション管理 1.0 では、クライアントを使用してプロバイダーのエンドユーザーをログアウトできます。利用可能な戦略の 1 つは RP からのログアウト (英語) です。

OpenID プロバイダーがセッション管理とディスカバリ (英語) の両方をサポートしている場合、クライアントは OpenID プロバイダーのディスカバリメタデータ (英語) から  end_session_endpoint URL を取得できます。これは、次の例のように、 issuer-uri を使用して  ClientRegistration を構成することで実現できます。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            ...
        provider:
          okta:
            issuer-uri: https://dev-1234.oktapreview.com

…および RP 開始ログアウトを実装する  OidcClientInitiatedLogoutSuccessHandler は、次のように構成できます。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .anyRequest().authenticated()
            )
            .oauth2Login(withDefaults())
            .logout(logout ->
                logout
                    .logoutSuccessHandler(oidcLogoutSuccessHandler())
            );
    }

    private LogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
                new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);

        // Sets the `URI` that the End-User's User Agent will be redirected to
        // after the logout has been performed at the Provider
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("https://localhost:8080"));

        return oidcLogoutSuccessHandler;
    }
}

12.2 OAuth 2.0 クライアント

OAuth 2.0 クライアント機能は、OAuth 2.0 認証フレームワーク (英語) で定義されているクライアントロールのサポートを提供します。

大まかに言うと、利用可能なコア機能は次のとおりです。

HTTP クライアントのサポート

HttpSecurity.oauth2Client() DSL は、OAuth 2.0 クライアントが使用するコアコンポーネントをカスタマイズするための多くの設定オプションを提供します。さらに、 HttpSecurity.oauth2Client().authorizationCodeGrant() では、認可コードの付与をカスタマイズできます。

次のコードは、 HttpSecurity.oauth2Client() DSL によって提供される完全な構成オプションを示しています。

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2Client ->
                oauth2Client
                    .clientRegistrationRepository(this.clientRegistrationRepository())
                    .authorizedClientRepository(this.authorizedClientRepository())
                    .authorizedClientService(this.authorizedClientService())
                    .authorizationCodeGrant(authorizationCodeGrant ->
                        authorizationCodeGrant
                            .authorizationRequestRepository(this.authorizationRequestRepository())
                            .authorizationRequestResolver(this.authorizationRequestResolver())
                            .accessTokenResponseClient(this.accessTokenResponseClient())
                    )
            );
    }
}

OAuth2AuthorizedClientManager は、1 つ以上の  OAuth2AuthorizedClientProvider と協力して、OAuth 2.0 クライアントの認可(または再認可)を管理します。

次のコードは、 OAuth2AuthorizedClientManager @Bean を登録し、それを  authorization_code refresh_token client_credentials password 認可認可タイプのサポートを提供する  OAuth2AuthorizedClientProvider コンポジットに関連付ける方法の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .authorizationCode()
                    .refreshToken()
                    .clientCredentials()
                    .password()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

以下のセクションでは、OAuth 2.0 クライアントで使用されるコアコンポーネントと利用可能な設定オプションについて詳しく説明します。

12.2.1 コアインターフェース / クラス

ClientRegistration

ClientRegistration は、OAuth 2.0 または OpenID Connect 1.0 プロバイダーに登録されたクライアントの表現です。

クライアント登録には、クライアント ID、クライアントシークレット、認可付与タイプ、リダイレクト URI、スコープ、認可 URI、トークン URI、その他の詳細などの情報が保持されます。

ClientRegistration とそのプロパティは次のように定義されています。

public final class ClientRegistration {
    private String registrationId;  1
    private String clientId;    2
    private String clientSecret;    3
    private ClientAuthenticationMethod clientAuthenticationMethod;  4
    private AuthorizationGrantType authorizationGrantType;  5
    private String redirectUriTemplate; 6
    private Set<String> scopes; 7
    private ProviderDetails providerDetails;
    private String clientName;  8

    public class ProviderDetails {
        private String authorizationUri;    9
        private String tokenUri;    10
        private UserInfoEndpoint userInfoEndpoint;
        private String jwkSetUri;   11
        private Map<String, Object> configurationMetadata;  12

        public class UserInfoEndpoint {
            private String uri; 13
            private AuthenticationMethod authenticationMethod;  14
            private String userNameAttributeName;   15

        }
    }
}

1

registrationId ClientRegistration を一意に識別する ID。

2

clientId: クライアント識別子。

3

clientSecret: クライアントのシークレット。

4

clientAuthenticationMethod: プロバイダーでクライアントを認証するために使用される方法。サポートされている値は、 basic post、および none (パブリッククライアント) (英語) です。

5

authorizationGrantType: OAuth 2.0 認証フレームワークは、4 つの認可付与 (英語) タイプを定義します。サポートされている値は  authorization_code client_credentials password implicit です。

6

redirectUriTemplate: エンドユーザーがクライアントへのアクセスを認証および認可した後に、 認可サーバーがエンドユーザーのユーザーエージェントをリダイレクトするクライアントの登録済みリダイレクト URI。

7

scopes: 認可リクエストフロー中にクライアントがリクエストしたスコープ(openid、メール、プロファイルなど)。

8

clientName: クライアントに使用される説明的な名前。この名前は、自動生成されたログインページにクライアントの名前を表示するときなど、特定のシナリオで使用される場合があります。

9

authorizationUri: 認可サーバーの認可エンドポイント URI。

10

tokenUri: 認可サーバーのトークンエンドポイント URI。

11

jwkSetUri: 認可サーバーから JSON Web キー (JWK) (英語) セットを取得するために使用される URI。ID トークンの JSON Web 署名 (JWS) (英語) およびオプションで UserInfo レスポンスを検証するために使用される暗号化キーが含まれます。

12

configurationMetadata: OpenID プロバイダーの構成情報 (英語) 。この情報は、Spring Boot 2.x プロパティ  spring.security.oauth2.client.provider.[providerId].issuerUri が構成されている場合にのみ利用できます。

13

(userInfoEndpoint)uri: 認証されたエンドユーザーのクレーム / 属性にアクセスするために使用される UserInfo エンドポイント URI。

14

(userInfoEndpoint)authenticationMethod: アクセストークンを UserInfo エンドポイントに送信するときに使用される認証方法。サポートされる値は、 header form、および query です。

15

userNameAttributeName: エンドユーザーの名前または識別子を参照する UserInfo レスポンスで返される属性の名前。

ClientRegistration は、OpenID Connect プロバイダーの構成エンドポイント (英語) または認可サーバーのメタデータエンドポイント (英語) の検出を使用して最初に構成できます。

ClientRegistrations は、次の例に見られるように、このメソッドで  ClientRegistration を構成するための便利なメソッドを提供します。

ClientRegistration clientRegistration =
    ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();

上記のコードは、シリーズ  https://idp.example.com/issuer/.well-known/openid-configuration (英語) 、次に  https://idp.example.com/.well-known/openid-configuration/issuer (英語) 、最後に  https://idp.example.com/.well-known/oauth-authorization-server/issuer (英語) で照会し、最初に停止して 200 レスポンスを返します。

別の方法として、 ClientRegistrations.fromOidcIssuerLocation() を使用して、OpenID Connect プロバイダーの構成エンドポイントのみを照会できます。

ClientRegistrationRepository

ClientRegistrationRepository は、OAuth 2.0/OpenID Connect 1.0  ClientRegistration(s)のリポジトリとして機能します。

[Note] メモ

クライアント登録情報は最終的に保存され、関連する認可サーバーによって所有されます。このリポジトリは、認可サーバーに保存されているプライマリクライアント登録情報のサブセットを取得する機能を提供します。

Spring Boot 2.x 自動構成は、 spring.security.oauth2.client.registration. [registrationId] の各プロパティを  ClientRegistration のインスタンスにバインドし、 ClientRegistrationRepository 内の各  ClientRegistration インスタンスを構成します。

[Note] メモ

ClientRegistrationRepository のデフォルトの実装は  InMemoryClientRegistrationRepository です。

また、自動構成は、 ClientRegistrationRepository を  ApplicationContext の  @Bean として登録し、アプリケーションで必要な場合に依存関係の注入に使用できるようにします。

次のリストに例を示します。

@Controller
public class OAuth2ClientController {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @GetMapping("/")
    public String index() {
        ClientRegistration oktaRegistration =
            this.clientRegistrationRepository.findByRegistrationId("okta");

        ...

        return "index";
    }
}

OAuth2AuthorizedClient

OAuth2AuthorizedClient は、認可クライアントの表現です。エンドユーザー(リソース所有者)がクライアントに保護されたリソースにアクセスする認可を与えた場合、クライアントは認可されたと見なされます。

OAuth2AuthorizedClient は、 OAuth2AccessToken (およびオプションの  OAuth2RefreshToken)を、認可を認可した  Principal エンドユーザーである  ClientRegistration (クライアント)およびリソース所有者に関連付ける目的に役立ちます。

OAuth2AuthorizedClientRepository/OAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository は、Web リクエスト間で  OAuth2AuthorizedClient を保持するロールを果たします。一方、 OAuth2AuthorizedClientService の主なロールは、アプリケーションレベルで  OAuth2AuthorizedClient を管理することです。

開発者の観点から、 OAuth2AuthorizedClientRepository または  OAuth2AuthorizedClientService は、クライアントに関連付けられた  OAuth2AccessToken をルックアップする機能を提供し、保護されたリソースリクエストを開始するために使用できるようにします。

次のリストに例を示します。

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    @GetMapping("/")
    public String index(Authentication authentication) {
        OAuth2AuthorizedClient authorizedClient =
            this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName());

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}
[Note] メモ

Spring Boot 2.x 自動構成は、 OAuth2AuthorizedClientRepository および / または  OAuth2AuthorizedClientService @Bean を  ApplicationContext に登録します。ただし、アプリケーションは、カスタム  OAuth2AuthorizedClientRepository または  OAuth2AuthorizedClientService @Bean をオーバーライドして登録することを選択できます。

OAuth2AuthorizedClientManager/OAuth2AuthorizedClientProvider

OAuth2AuthorizedClientManager は、 OAuth2AuthorizedClient の全体的な管理を担当します。

主な責任は次のとおりです。

  • OAuth2AuthorizedClientProvider を使用して、OAuth 2.0 クライアントを認可(または再認可)します。
  • 通常  OAuth2AuthorizedClientService または  OAuth2AuthorizedClientRepository を使用して、 OAuth2AuthorizedClient の永続性を委譲します。

OAuth2AuthorizedClientProvider は、OAuth 2.0 クライアントを認可(または再認可)するための戦略を実装します。実装は通常、認可付与タイプを実装します。 authorization_code client_credentials など。

OAuth2AuthorizedClientManager のデフォルトの実装は  DefaultOAuth2AuthorizedClientManager です。これは、委譲ベースの複合を使用して複数の認可付与タイプをサポートする可能性のある  OAuth2AuthorizedClientProvider に関連付けられています。 OAuth2AuthorizedClientProviderBuilder を使用して、委譲ベースのコンポジットを構成および構築できます。

次のコードは、 authorization_code refresh_token client_credentials password 認可付与タイプのサポートを提供する  OAuth2AuthorizedClientProvider コンポジットを構成および構築する方法の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .authorizationCode()
                    .refreshToken()
                    .clientCredentials()
                    .password()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

DefaultOAuth2AuthorizedClientManager は、タイプ  Function<OAuth2AuthorizeRequest, Map<String, Object>> の  contextAttributesMapper にも関連付けられます。 Function<OAuth2AuthorizeRequest, Map<String, Object>> は、 OAuth2AuthorizeRequest から  OAuth2AuthorizationContext に関連付けられる属性の  Map への属性のマッピングを担当します。これは、 OAuth2AuthorizedClientProvider に必要な(サポートされている)属性を指定する必要がある場合に役立ちます。 PasswordOAuth2AuthorizedClientProvider では、リソース所有者の  username および  password が  OAuth2AuthorizationContext.getAttributes() で使用可能である必要があります。

次のコードは、 contextAttributesMapper の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .password()
                    .refreshToken()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
    // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
    authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

    return authorizedClientManager;
}

private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
    return authorizeRequest -> {
        Map<String, Object> contextAttributes = Collections.emptyMap();
        HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
        String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
        String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
        if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
            contextAttributes = new HashMap<>();

            // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
            contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
        }
        return contextAttributes;
    };
}

DefaultOAuth2AuthorizedClientManager は、 HttpServletRequest のコンテキストで 以内にを使用するように設計されています。 HttpServletRequest コンテキストの 外側を操作する場合は、代わりに  AuthorizedClientServiceOAuth2AuthorizedClientManager を使用してください。

サービスアプリケーションは、 AuthorizedClientServiceOAuth2AuthorizedClientManager を使用する場合の一般的な使用例です。多くの場合、サービスアプリケーションはユーザーの操作なしでバックグラウンドで実行され、通常はユーザーアカウントではなくシステムレベルのアカウントで実行されます。 client_credentials 付与タイプで構成された OAuth 2.0 クライアントは、サービスアプリケーションのタイプと見なすことができます。

次のコードは、 client_credentials 付与タイプのサポートを提供する  AuthorizedClientServiceOAuth2AuthorizedClientManager を構成する方法の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientService authorizedClientService) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
            new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientService);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

12.2.2 認可付与サポート

認証コード

[Note] メモ

認証コード (英語) 付与の詳細については、OAuth 2.0 認可フレームワークを参照してください。

認可の取得
[Note] メモ

認証コードの付与については、認可リクエスト / レスポンス (英語) プロトコルフローを参照してください。

認可リクエストの開始

OAuth2AuthorizationRequestRedirectFilter は  OAuth2AuthorizationRequestResolver を使用して  OAuth2AuthorizationRequest を解決し、エンドユーザーのユーザーエージェントを認可サーバーの認可エンドポイントにリダイレクトすることで認可コード付与フローを開始します。

OAuth2AuthorizationRequestResolver の主なロールは、提供された Web リクエストから  OAuth2AuthorizationRequest を解決することです。デフォルト実装  DefaultOAuth2AuthorizationRequestResolver は、 registrationId を抽出し、それを使用して関連  ClientRegistration の  OAuth2AuthorizationRequest を構築する(デフォルト)パス  /oauth2/authorization/{registrationId} で一致します。

OAuth 2.0 クライアント登録用の次の Spring Boot 2.x プロパティがあるとします。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/authorized/okta"
            scope: read, write
        provider:
          okta:
            authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize
            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token

ベースパス  /oauth2/authorization/okta を使用したリクエストは、 OAuth2AuthorizationRequestRedirectFilter による認可リクエストリダイレクトを開始し、最終的に認可コード認可フローを開始します。

[Note] メモ

AuthorizationCodeOAuth2AuthorizedClientProvider は、認可コード付与のための  OAuth2AuthorizedClientProvider の実装であり、 OAuth2AuthorizationRequestRedirectFilter による認可リクエストリダイレクトも開始します。

OAuth 2.0 クライアントがパブリッククライアント (英語) の場合、OAuth 2.0 クライアントの登録を次のように構成します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-authentication-method: none
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/authorized/okta"
            ...

コード交換用の証明キー (英語) (PKCE)を使用したパブリッククライアントがサポートされています。クライアントが信頼されていない環境(ネイティブアプリケーションまたは Web ブラウザーベースのアプリケーションなど)で実行されているため、その資格情報の機密性を維持できない場合、次の条件に該当する場合、PKCE が自動的に使用されます。

  1. client-secret は省略されます (または空)
  2. client-authentication-method は「なし」に設定されます ( ClientAuthenticationMethod.NONE)

DefaultOAuth2AuthorizationRequestResolver は、 UriComponentsBuilder を使用して  redirect-uri の  URI テンプレート変数もサポートします。

次の構成では、サポートされているすべての  URI テンプレート変数を使用します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            ...
            redirect-uri: "{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}"
            ...
[Note] メモ

{baseUrl} は  {baseScheme}://{baseHost}{basePort}{basePath} に解決されます

URI テンプレート変数を使用して  redirect-uri を構成することは、OAuth 2.0 クライアントがプロキシサーバーの背後で実行されている場合に特に役立ちます。これにより、 redirect-uri を展開するときに  X-Forwarded-* ヘッダーが使用されます。

認可リクエストのカスタマイズ

OAuth2AuthorizationRequestResolver が実現できる主な使用例の 1 つは、OAuth 2.0 認可フレームワークで定義された標準パラメーターを超える追加パラメーターで認可リクエストをカスタマイズする機能です。

例: OpenID Connect は、OAuth 2.0 認証フレームワーク (英語) で定義された標準パラメーターから拡張された、認証コード Flow (英語) の追加の OAuth 2.0 リクエストパラメーターを定義します。これらの拡張パラメーターの 1 つは  prompt パラメーターです。

[Note] メモ

オプション。認可サーバーがエンドユーザーに再認証と同意を求めるかどうかを指定する、ASCII 文字列値のスペース区切りの大文字と小文字を区別したリスト。定義されている値は、なし、ログイン、同意、select_account です。

次の例は、リクエストパラメーター  prompt=consent を含めることにより、 oauth2Login() の Authorization Request をカスタマイズする  OAuth2AuthorizationRequestResolver を実装する方法を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .anyRequest().authenticated()
            )
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .authorizationEndpoint(authorizationEndpoint ->
                        authorizationEndpoint
                            .authorizationRequestResolver(
                                new CustomAuthorizationRequestResolver(
                                        this.clientRegistrationRepository))    1
                    )
            );
    }
}

public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
    private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;

    public CustomAuthorizationRequestResolver(
            ClientRegistrationRepository clientRegistrationRepository) {

        this.defaultAuthorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest authorizationRequest =
                this.defaultAuthorizationRequestResolver.resolve(request);  2

        return authorizationRequest != null ?   3
                customAuthorizationRequest(authorizationRequest) :
                null;
    }

    @Override
    public OAuth2AuthorizationRequest resolve(
            HttpServletRequest request, String clientRegistrationId) {

        OAuth2AuthorizationRequest authorizationRequest =
                this.defaultAuthorizationRequestResolver.resolve(
                    request, clientRegistrationId);    4

        return authorizationRequest != null ?   5
                customAuthorizationRequest(authorizationRequest) :
                null;
    }

    private OAuth2AuthorizationRequest customAuthorizationRequest(
            OAuth2AuthorizationRequest authorizationRequest) {

        Map<String, Object> additionalParameters =
                new LinkedHashMap<>(authorizationRequest.getAdditionalParameters());
        additionalParameters.put("prompt", "consent");  6

        return OAuth2AuthorizationRequest.from(authorizationRequest)    7
                .additionalParameters(additionalParameters) 8
                .build();
    }
}

1

カスタム  OAuth2AuthorizationRequestResolver を構成する

2 4

DefaultOAuth2AuthorizationRequestResolver を使用して  OAuth2AuthorizationRequest を解決しようとする

3 5

OAuth2AuthorizationRequest が解決された場合、カスタマイズされたバージョンを返すか、 null を返す

6

既存の  OAuth2AuthorizationRequest.additionalParameters にカスタムパラメーターを追加する

7

さらなる変更のために  OAuth2AuthorizationRequest.Builder を返すデフォルト  OAuth2AuthorizationRequest のコピーを作成する

8

デフォルトの  additionalParameters をオーバーライドする

[Tip] ヒント

OAuth2AuthorizationRequest.Builder.build() は  OAuth2AuthorizationRequest.authorizationRequestUri を構築します。 OAuth2AuthorizationRequest.authorizationRequestUri は、 application/x-www-form-urlencoded 形式を使用するすべてのクエリパラメーターを含む完全な認証リクエスト URI を表します。

追加のリクエストパラメーターが特定のプロバイダに対して常に同じである単純なユースケースでは、 authorization-uri に直接追加できます。

例: リクエストパラメーター  prompt の値がプロバイダー  okta に対して常に  consent である場合、単に以下のように構成します。

spring:
  security:
    oauth2:
      client:
        provider:
          okta:
            authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize?prompt=consent

上記の例は、標準パラメーターの上にカスタムパラメーターを追加する一般的な使用例を示しています。または、要件がより高度な場合は、 OAuth2AuthorizationRequest.authorizationRequestUri プロパティをオーバーライドするだけで、認可リクエスト URI の作成を完全に制御できます。

次の例は、前の例の  customAuthorizationRequest() メソッドのバリエーションを示し、代わりに  OAuth2AuthorizationRequest.authorizationRequestUri プロパティをオーバーライドします。

private OAuth2AuthorizationRequest customAuthorizationRequest(
        OAuth2AuthorizationRequest authorizationRequest) {

    String customAuthorizationRequestUri = UriComponentsBuilder
            .fromUriString(authorizationRequest.getAuthorizationRequestUri())
            .queryParam("prompt", "consent")
            .build(true)
            .toUriString();

    return OAuth2AuthorizationRequest.from(authorizationRequest)
            .authorizationRequestUri(customAuthorizationRequestUri)
            .build();
}
認証リクエストの保存

AuthorizationRequestRepository は、認可リクエストが開始されてから認可レスポンスが受信されるまで(コールバック)、 OAuth2AuthorizationRequest の永続化を担当します。

[Tip] ヒント

OAuth2AuthorizationRequest は、認可レスポンスを関連付けて検証するために使用されます。

AuthorizationRequestRepository のデフォルトの実装は  HttpSessionOAuth2AuthorizationRequestRepository で、 HttpSession に  OAuth2AuthorizationRequest を格納します。

AuthorizationRequestRepository のカスタム実装がある場合、次の例に示すように構成できます。

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2Client ->
                oauth2Client
                    .authorizationCodeGrant(authorizationCodeGrant ->
                        authorizationCodeGrant
                            .authorizationRequestRepository(this.authorizationRequestRepository())
                            ...
                    )
            );
    }
}
アクセストークンのリクエスト
[Note] メモ

認証コードの付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

認可コード付与の  OAuth2AccessTokenResponseClient のデフォルト実装は  DefaultAuthorizationCodeTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンの認可コードを交換するために  RestOperations を使用します。

DefaultAuthorizationCodeTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、 DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter() にカスタム  Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装  OAuth2AuthorizationCodeGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の  RequestEntity 表現を構築します。ただし、カスタム  Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

[Important] 重要

カスタム  Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な  RequestEntity 表現を返す必要があります。

アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、 DefaultAuthorizationCodeTokenResponseClient.setRestOperations() にカスタム構成の  RestOperations を提供する必要があります。デフォルトの  RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
[Tip] ヒント

Spring MVC FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の  HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを  OAuth2AccessTokenResponse に変換するために使用されるカスタム  Converter<Map<String, String>, OAuth2AccessTokenResponse> を  OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる  ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを  OAuth2Error に変換するために  OAuth2ErrorHttpMessageConverter を使用します。

DefaultAuthorizationCodeTokenResponseClient をカスタマイズする場合でも、 OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2Client ->
                oauth2Client
                    .authorizationCodeGrant(authorizationCodeGrant ->
                        authorizationCodeGrant
                            .accessTokenResponseClient(this.accessTokenResponseClient())
                            ...
                     )
            );
    }
}

リフレッシュトークン

[Note] メモ

リフレッシュトークン (英語) の詳細については、OAuth 2.0 Authorization フレームワークを参照してください。

アクセストークンのリフレッシュ
[Note] メモ

リフレッシュトークンの付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

リフレッシュトークン付与の  OAuth2AccessTokenResponseClient のデフォルト実装は  DefaultRefreshTokenTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンをリフレッシュするときに  RestOperations を使用します。

DefaultRefreshTokenTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、 DefaultRefreshTokenTokenResponseClient.setRequestEntityConverter() にカスタム  Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装  OAuth2RefreshTokenGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の  RequestEntity 表現を構築します。ただし、カスタム  Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

[Important] 重要

カスタム  Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な  RequestEntity 表現を返す必要があります。

アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、 DefaultRefreshTokenTokenResponseClient.setRestOperations() にカスタム構成の  RestOperations を提供する必要があります。デフォルトの  RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
[Tip] ヒント

Spring MVC  FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の  HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを  OAuth2AccessTokenResponse に変換するために使用されるカスタム  Converter<Map<String, String>, OAuth2AccessTokenResponse> を  OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる  ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを  OAuth2Error に変換するために  OAuth2ErrorHttpMessageConverter を使用します。

DefaultRefreshTokenTokenResponseClient をカスタマイズする場合でも、 OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

// Customize
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient = ...

OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode()
                .refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenTokenResponseClient))
                .build();

...

authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
[Note] メモ

OAuth2AuthorizedClientProviderBuilder.builder().refreshToken() は  RefreshTokenOAuth2AuthorizedClientProvider を構成します。これは、リフレッシュトークン許可用の  OAuth2AuthorizedClientProvider の実装です。

OAuth2RefreshToken は、 authorization_code および  password 付与タイプのアクセストークンレスポンスでオプションで返されます。 OAuth2AuthorizedClient.getRefreshToken() が使用可能であり、 OAuth2AuthorizedClient.getAccessToken() の有効期限が切れている場合、 RefreshTokenOAuth2AuthorizedClientProvider によって自動的にリフレッシュされます。

クライアント資格情報

[Note] メモ

クライアント資格情報 (英語) 付与の詳細については、OAuth 2.0 認可フレームワークを参照してください。

アクセストークンのリクエスト
[Note] メモ

クライアント資格情報の付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

クライアント資格情報付与の  OAuth2AccessTokenResponseClient のデフォルト実装は  DefaultClientCredentialsTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンをリクエストするときに  RestOperations を使用します。

DefaultClientCredentialsTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、 DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter() にカスタム  Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装  OAuth2ClientCredentialsGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の  RequestEntity 表現を構築します。ただし、カスタム  Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

[Important] 重要

カスタム  Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な  RequestEntity 表現を返す必要があります。

アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、 DefaultClientCredentialsTokenResponseClient.setRestOperations() にカスタム構成の  RestOperations を提供する必要があります。デフォルトの  RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
[Tip] ヒント

Spring MVC  FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の  HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを  OAuth2AccessTokenResponse に変換するために使用されるカスタム  Converter<Map<String, String>, OAuth2AccessTokenResponse> を  OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる  ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを  OAuth2Error に変換するために  OAuth2ErrorHttpMessageConverter を使用します。

DefaultClientCredentialsTokenResponseClient をカスタマイズする場合でも、 OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

// Customize
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient = ...

OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
                .build();

...

authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
[Note] メモ

OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials() は、 ClientCredentialsOAuth2AuthorizedClientProvider を構成します。これは、Client Credentials 付与のための  OAuth2AuthorizedClientProvider の実装です。

アクセストークンの使用

OAuth 2.0 クライアント登録用の次の Spring Boot 2.x プロパティがあるとします。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            authorization-grant-type: client_credentials
            scope: read, write
        provider:
          okta:
            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token

…そして  OAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

OAuth2AccessToken は次のようにして入手できます。

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientManager authorizedClientManager;

    @GetMapping("/")
    public String index(Authentication authentication,
                        HttpServletRequest servletRequest,
                        HttpServletResponse servletResponse) {

        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
                .principal(authentication)
                .attributes(attrs -> {
                    attrs.put(HttpServletRequest.class.getName(), servletRequest);
                    attrs.put(HttpServletResponse.class.getName(), servletResponse);
                })
                .build();
        OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}
[Note] メモ

HttpServletRequest と  HttpServletResponse はどちらもオプション属性です。指定しない場合、 RequestContextHolder.getRequestAttributes() を使用して  ServletRequestAttributes がデフォルトになります。

リソース所有者のパスワード資格証明

[Note] メモ

リソース所有者のパスワード資格証明 (英語) 付与の詳細については、OAuth 2.0 認可フレームワークを参照してください。

アクセストークンのリクエスト
[Note] メモ

リソース所有者パスワード資格情報の付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

リソース所有者パスワード資格情報付与の  OAuth2AccessTokenResponseClient のデフォルト実装は  DefaultPasswordTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンをリクエストするときに  RestOperations を使用します。

DefaultPasswordTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、 DefaultPasswordTokenResponseClient.setRequestEntityConverter() にカスタム  Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装  OAuth2PasswordGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の  RequestEntity 表現を構築します。ただし、カスタム  Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

[Important] 重要

カスタム  Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な  RequestEntity 表現を返す必要があります。

アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、 DefaultPasswordTokenResponseClient.setRestOperations() にカスタム構成の  RestOperations を提供する必要があります。デフォルトの  RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
[Tip] ヒント

Spring MVC  FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の  HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを  OAuth2AccessTokenResponse に変換するために使用されるカスタム  Converter<Map<String, String>, OAuth2AccessTokenResponse> を  OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる  ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを  OAuth2Error に変換するために  OAuth2ErrorHttpMessageConverter を使用します。

DefaultPasswordTokenResponseClient をカスタマイズする場合でも、 OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

// Customize
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient = ...

OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
                .password(configurer -> configurer.accessTokenResponseClient(passwordTokenResponseClient))
                .refreshToken()
                .build();

...

authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
[Note] メモ

OAuth2AuthorizedClientProviderBuilder.builder().password() は  PasswordOAuth2AuthorizedClientProvider を構成します。これは、リソース所有者パスワード資格情報付与のための  OAuth2AuthorizedClientProvider の実装です。

アクセストークンの使用

OAuth 2.0 クライアント登録用の次の Spring Boot 2.x プロパティがあるとします。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            authorization-grant-type: password
            scope: read, write
        provider:
          okta:
            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token

…そして  OAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .password()
                    .refreshToken()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
    // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
    authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

    return authorizedClientManager;
}

private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
    return authorizeRequest -> {
        Map<String, Object> contextAttributes = Collections.emptyMap();
        HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
        String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
        String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
        if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
            contextAttributes = new HashMap<>();

            // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
            contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
        }
        return contextAttributes;
    };
}

OAuth2AccessToken は次のようにして入手できます。

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientManager authorizedClientManager;

    @GetMapping("/")
    public String index(Authentication authentication,
                        HttpServletRequest servletRequest,
                        HttpServletResponse servletResponse) {

        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
                .principal(authentication)
                .attributes(attrs -> {
                    attrs.put(HttpServletRequest.class.getName(), servletRequest);
                    attrs.put(HttpServletResponse.class.getName(), servletResponse);
                })
                .build();
        OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}
[Note] メモ

HttpServletRequest と  HttpServletResponse はどちらもオプション属性です。指定しない場合、 RequestContextHolder.getRequestAttributes() を使用して  ServletRequestAttributes がデフォルトになります。

12.2.3 追加機能

認可されたクライアントの解決

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

@Controller
public class OAuth2ClientController {

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

        ...

        return "index";
    }
}

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

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

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

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

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

    • authorization_code - フローを開始するために認可リクエストリダイレクトをトリガーする
    • client_credentials - アクセストークンはトークンエンドポイントから直接取得されます
    • password - アクセストークンはトークンエンドポイントから直接取得されます
  • OAuth2AccessToken の有効期限が切れている場合、 OAuth2AuthorizedClientProvider が認可を実行できる場合、リフレッシュ(またはリフレッシュ)されます。

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

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

認定クライアントの提供

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

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

@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";
}

1

oauth2AuthorizedClient() は  ServletOAuth2AuthorizedClientExchangeFilterFunction の  static メソッドです。

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

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

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

    ...

    return "index";
}

1

clientRegistrationId() は  ServletOAuth2AuthorizedClientExchangeFilterFunction の  static メソッドです。

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

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

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

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

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultOAuth2AuthorizedClient(true);
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}
[Warning] 警告

すべての HTTP リクエストはアクセストークンを受け取るため、この機能には注意が必要です。

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

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

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("okta");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}
[Warning] 警告

すべての HTTP リクエストはアクセストークンを受け取るため、この機能には注意が必要です。

12.3 OAuth 2.0 リソースサーバー

Spring Security は、2 つの形式の OAuth 2.0 ベアラートークン (英語) を使用したエンドポイントの保護をサポートしています。

これは、アプリケーションが権限管理を認可サーバー (英語) (Okta や Ping Identity など)に委譲している場合に便利です。この認可サーバーは、リソースサーバーがリクエストを認可するために調べることができます。

12.3.1 依存関係

ほとんどのリソースサーバーサポートは  spring-security-oauth2-resource-server に収集されます。ただし、JWT のデコードと検証のサポートは  spring-security-oauth2-jose にあります。つまり、JWT でエンコードされたベアラートークンをサポートする作業リソースサーバーを使用するには両方が必要です。

12.3.2 JWT の最小構成

Spring Boot を使用する場合、アプリケーションをリソースサーバーとして構成するには、2 つの基本的な手順が必要です。最初に、必要な依存関係を含め、2 番目に認可サーバーの場所を示します。

認可サーバーの指定

Spring Boot アプリケーションで、使用する認可サーバーを指定するには、次のようにします。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/issuer

ここで、 https://idp.example.com/issuer (英語)  は、認可サーバーが発行する JWT トークンの  iss クレームに含まれる値です。リソースサーバーは、このプロパティを使用して、さらに自己構成を行い、認可サーバーの公開キーを検出し、受信 JWT を検証します。

[Note] メモ

issuer-uri プロパティを使用するには、 https://idp.example.com/issuer/.well-known/openid-configuration (英語) https://idp.example.com/.well-known/openid-configuration/issuer (英語) 、または  https://idp.example.com/.well-known/oauth-authorization-server/issuer (英語)  のいずれかが認可サーバーでサポートされているエンドポイントであることも真である必要があります。このエンドポイントは、プロバイダー構成 (英語) エンドポイントまたは認可サーバーのメタデータ (英語) エンドポイントと呼ばれます。

以上です!

スタートアップの期待

このプロパティとこれらの依存関係を使用すると、Resource Server は自動的に JWT エンコードされたベアラートークンを検証するように自身を構成します。

これは、決定論的な起動プロセスを通じてこれを実現します。

  1. プロバイダー構成または認可サーバーメタデータエンドポイントをヒットし、 jwks_url プロパティのレスポンスを処理する
  2. 有効な公開鍵について  jwks_url を照会するための検証戦略を構成する
  3. https://idp.example.com (英語) に対して各 JWT  iss クレームを検証する検証戦略を構成します。

このプロセスの結果、リソースサーバーが正常に起動するには、認可サーバーが起動してリクエストを受信する必要があります。

[Note] メモ

リソースサーバーがクエリを実行したときに認可サーバーがダウンした場合(適切なタイムアウトが与えられた場合)、起動は失敗します。

ランタイムの期待

アプリケーションが起動すると、Resource Server は  Authorization: Bearer ヘッダーを含むリクエストの処理を試みます。

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

このスキームが示されている限り、Resource Server は Bearer Token 仕様に従ってリクエストの処理を試みます。

整形式の JWT が与えられると、リソースサーバーは次のことを行います。

  1. 起動時に  jwks_url エンドポイントから取得され、JWT と照合される公開鍵に対して署名を検証する
  2. JWT の  exp および  nbf タイムスタンプと、JWT の  iss クレームを検証します。
  3. 各スコープを接頭辞  SCOPE_ を持つ機関にマップします。
[Note] メモ

認可サーバーが新しい鍵を使用できるようになると、Spring Security は JWT の検証に使用される鍵を自動的にローテーションします。

デフォルトでは、結果の  Authentication#getPrincipal は Spring Security  Jwt オブジェクトであり、 Authentication#getName は JWT の  sub プロパティ(存在する場合)にマップします。

ここから、次へのジャンプを検討してください。

リソースサーバーの起動を認可サーバーの可用性に結び付けずに構成する方法

Spring Boot なしで構成する方法

12.3.3 認可サーバー JWK セット Uri を直接指定する

認可サーバーが構成エンドポイントをサポートしていない場合、またはリソースサーバーが認可サーバーから独立して起動できる必要がある場合は、 jwk-set-uri も提供できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          jwk-set-uri: https://idp.example.com/.well-known/jwks.json
[Note] メモ

JWK Set uri は標準化されていませんが、通常は認可サーバーのドキュメントに記載されています

リソースサーバーは起動時に認可サーバーに ping を実行しません。 issuer-uri を引き続き指定して、Resource Server が受信 JWT で  iss クレームを検証するようにします。

[Note] メモ

このプロパティは、DSL で直接指定することもできます。

12.3.4 Boot 自動構成のオーバーライドまたは置換

Spring Boot がリソースサーバーに代わって生成する 2 つの  @Bean があります。

1 つ目は、アプリをリソースサーバーとして構成する  WebSecurityConfigurerAdapter です。 spring-security-oauth2-jose を含めると、この  WebSecurityConfigurerAdapter は次のようになります。

protected void configure(HttpSecurity http) {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
}

アプリケーションが  WebSecurityConfigurerAdapter Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えることは、アプリケーション内で Bean を公開するのと同じくらい簡単です。

@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .jwtAuthenticationConverter(myConverter());
    }
}

上記では、 /messages/ で始まる URL の  message:read のスコープが必要です。

oauth2ResourceServer DSL のメソッドも自動構成をオーバーライドまたは置き換えます。

例: Spring Boot が 2 番目に作成する  @Bean は、 String トークンを  Jwt の検証済みインスタンスにデコードする  JwtDecoder です。

@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}
[Note] メモ

JwtDecoders#fromIssuerLocation (Javadoc)  を呼び出すと、JWK セット Uri を取得するためにプロバイダー構成または認可サーバーメタデータエンドポイントが呼び出されます。

アプリケーションが  JwtDecoder Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

そして、その構成は  jwkSetUri() を使用してオーバーライドするか、 decoder() を使用して置き換えることができます。

jwkSetUri() を使用する

認可サーバーの JWK Set Uri は 、構成プロパティとして構成することも、DSL で提供することもできます。

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .jwkSetUri("https://idp.example.com/.well-known/jwks.json");
    }
}

jwkSetUri() の使用は、構成プロパティよりも優先されます。

decoder() を使用する

jwkSetUri() よりも強力なのは  decoder() です。これは、 JwtDecoder の Boot 自動構成を完全に置き換えます。

@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .decoder(myCustomDecoder());
    }
}

これは、 validationmapping 、または request timeouts の ようなより詳細な構成が必要な場合に便利です。

JwtDecoder の公開 @Bean

または、 JwtDecoder を公開すると @Bean は  decoder() と同じ効果があります。

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}

12.3.5 信頼できるアルゴリズムの構成

デフォルトでは、 NimbusJwtDecoder、リソースサーバーは、 RS256 を使用したトークンのみを信頼および検証します。

これは、Spring BootNimbusJwtDecoder ビルダー、または JWK セットレスポンス からカスタマイズできます。

Spring Boot 経由

アルゴリズムを設定する最も簡単な方法は、プロパティとしてです:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithm: RS512
          jwk-set-uri: https://idp.example.org/.well-known/jwks.json

ビルダーを使用する

ただし、より強力にするには、 NimbusJwtDecoder に同梱されているビルダーを使用できます。

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).build();
}

jwsAlgorithm を複数回呼び出すと、 NimbusJwtDecoder は次のように複数のアルゴリズムを信頼するように構成されます。

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
}

または、 jwsAlgorithms を呼び出すことができます。

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithms(algorithms -> {
                    algorithms.add(RS512);
                    algorithms.add(EC512);
            }).build();
}

JWK Set レスポンスから

Spring Security の JWT サポートは Nimbus に基づいているため、その優れた機能もすべて使用できます。

例: Nimbus には、JWK Set URI レスポンスに基づいてアルゴリズムのセットを選択する  JWSKeySelector 実装があります。これを使用して、 NimbusJwtDecoder を次のように生成できます。

@Bean
public JwtDecoder jwtDecoder() {
    // makes a request to the JWK Set endpoint
    JWSKeySelector<SecurityContext> jwsKeySelector =
            JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);

    DefaultJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor<>();
    jwtProcessor.setJWSKeySelector(jwsKeySelector);

    return new NimbusJwtDecoder(jwtProcessor);
}

12.3.6 単一の非対称キーを信頼する

JWK Set エンドポイントでリソースサーバーをバッキングするよりも簡単なのは、RSA 公開キーをハードコードすることです。公開鍵は、Spring Boot またはビルダーを使用するを介して提供できます。

Spring Boot 経由

Spring Boot を介したキーの指定は非常に簡単です。キーの場所は次のように指定できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:my-key.pub

または、より洗練されたルックアップを可能にするために、 RsaKeyConversionServicePostProcessor を後処理できます。

@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
    return beanFactory ->
        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
                .setResourceLoader(new CustomResourceLoader());
}

キーの場所を指定します。

key.location: hfds://my-key.pub

そして、値をオートワイヤーします。

@Value("${key.location}")
RSAPublicKey key;

ビルダーを使用する

RSAPublicKey を直接接続するには、次のように適切な  NimbusJwtDecoder ビルダーを使用するだけです。

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.key).build();
}

12.3.7 単一の対称キーを信頼する

単一の対称キーの使用も簡単です。次のように、 SecretKey をロードして適切な  NimbusJwtDecoder ビルダーを使用するだけです。

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withSecretKey(this.key).build();
}

12.3.8 認可の構成

OAuth 2.0 認可サーバーから発行される JWT は通常、 scope または  scp 属性のいずれかを持ち、付与されたスコープ(または権限)を示します。例:

{ …​, "scope" : "messages contacts"}

この場合、Resource Server はこれらのスコープを付与された権限のリストに強制し、各スコープの前に文字列「SCOPE_」を付けようとします。

つまり、エンドポイントまたはメソッドを JWT から派生したスコープで保護するには、対応する式に次のプレフィックスを含める必要があります。

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorizeRequests -> authorizeRequests
                .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }
}

または、同様にメソッドセキュリティで:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}

権限の手動抽出

ただし、このデフォルトでは不十分な状況がいくつかあります。例: 一部の認可サーバーは  scope 属性を使用せず、独自のカスタム属性を持っています。または、リソースサーバーは、属性または属性の構成を内部化された機関に適合させる必要がある場合もあります。

このために、DSL は  jwtAuthenticationConverter() を公開します:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
    }
}

Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
    JwtAuthenticationConverter jwtAuthenticationConverter =
            new JwtAuthenticationConverter();

    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
            (new GrantedAuthoritiesExtractor());

    return jwtAuthenticationConverter;
}

Jwt を  Authentication に変換する責任があります。その構成の一部として、 Jwt から付与された権限の  Collection に移行する補助コンバーターを提供できます。

その最終的なコンバーターは、以下の  GrantedAuthoritiesExtractor のようなものです。

static class GrantedAuthoritiesExtractor
        implements Converter<Jwt, Collection<GrantedAuthority>> {

    public Collection<GrantedAuthority> convert(Jwt jwt) {
        Collection<String> authorities = (Collection<String>)
                jwt.getClaims().get("mycustomclaim");

        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

柔軟性を高めるため、DSL はコンバーターを  Converter<Jwt, AbstractAuthenticationToken> を実装するクラスに完全に置き換えることをサポートしています。

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return new CustomAuthenticationToken(jwt);
    }
}

12.3.9 検証の構成

リソースサーバーは、認可サーバーの発行者 URI を示す最小限の Spring Boot 構成を使用して、 iss クレームと  exp および  nbf タイムスタンプクレームをデフォルトで検証します。

検証をカスタマイズする必要がある状況では、Resource Server には 2 つの標準バリデーターが付属しており、カスタム  OAuth2TokenValidator インスタンスも受け入れます。

タイムスタンプ検証のカスタマイズ

通常、JWT には有効期間があり、ウィンドウの開始は  nbf クレームで示され、終了は  exp クレームで示されます。

ただし、すべてのサーバーでクロックドリフトが発生する可能性があります。これにより、あるサーバーではトークンが期限切れになり、別のサーバーでは期限切れになります。これにより、分散システムでコラボレーションサーバーの数が増えると、実装の胸焼けが発生する可能性があります。

リソースサーバーは  JwtTimestampValidator を使用してトークンの有効期間を検証し、 clockSkew で構成して上記の問題を軽減できます。

@Bean
JwtDecoder jwtDecoder() {
     NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
             JwtDecoders.fromIssuerLocation(issuerUri);

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new IssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}
[Note] メモ

デフォルトでは、Resource Server は 30 秒のクロックスキューを設定します。

カスタム検証ツールの構成

aud クレームのチェックの追加は、 OAuth2TokenValidator API を使用すると簡単です。

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

次に、リソースサーバーに追加するには、 JwtDecoder インスタンスを指定するだけです。

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}

12.3.10 クレームセットマッピングの構成

Spring Security は、Nimbus (英語) ライブラリを使用して、JWT の構文解析と署名の検証を行います。Spring Security は、各フィールド値の Nimbus の解釈と、それぞれを Java 型に強制する方法の対象となります。

例: Nimbus は Java 7 と互換性があるため、タイムスタンプフィールドを表すために  Instant を使用しません。

また、別のライブラリを使用したり、JWT 処理に使用したりすることもできます。これにより、調整が必要な独自の強制決定を行うことができます。

または、非常に単純に、リソースサーバーはドメイン固有の理由で JWT にクレームを追加または削除したい場合があります。

これらの目的のために、Resource Server は  MappedJwtClaimSetConverter を使用した JWT クレームセットのマッピングをサポートしています。

単一クレームの変換のカスタマイズ

デフォルトでは、 MappedJwtClaimSetConverter はクレームを次のタイプに強制しようとします。

請求

Java タイプ

aud

Collection<String>

exp

Instant

iat

Instant

iss

String

jti

String

nbf

Instant

sub

String

MappedJwtClaimSetConverter.withDefaults を使用して、個々の申し立ての変換戦略を構成できます。

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();

    MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
            .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
    jwtDecoder.setClaimSetConverter(converter);

    return jwtDecoder;
}

これにより、 sub のデフォルトクレームコンバーターがオーバーライドされることを除き、すべてのデフォルトが保持されます。

クレームを追加する

MappedJwtClaimSetConverter は、たとえば既存のシステムに適応するために、カスタムクレームを追加するためにも使用できます。

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));

申し立ての削除

また、同じ API を使用して、クレームを削除することも簡単です。

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));

クレームの名前を変更する

一度に複数のクレームを参照したり、クレームの名前を変更したりするような、より洗練されたシナリオでは、Resource Server は  Converter<Map<String, Object>, Map<String,Object>> を実装するクラスを受け入れます。

public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);

        String username = (String) convertedClaims.get("user_name");
        convertedClaims.put("sub", username);

        return convertedClaims;
    }
}

そして、インスタンスは通常のように提供できます:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}

12.3.11 タイムアウトの構成

デフォルトでは、Resource Server は認可サーバーとの調整にそれぞれ 30 秒の接続およびソケットタイムアウトを使用します。

これはいくつかのシナリオでは短すぎるかもしれません。さらに、バックオフや発見などのより高度なパターンは考慮されません。

Resource Server が認可サーバーに接続する方法を調整するために、 NimbusJwtDecoder は  RestOperations のインスタンスを受け入れます。

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .setConnectionTimeout(60000)
            .setReadTimeout(60000)
            .build();

    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
    return jwtDecoder;
}

12.3.12 イントロスペクションの最小構成

通常、Opaque トークンは、認可サーバーによってホストされる OAuth 2.0 イントロスペクションエンドポイント (英語) を介して検証できます。これは、失効が必要な場合に便利です。

Spring Boot を使用する場合、イントロスペクションを使用するリソースサーバーとしてアプリケーションを構成するには、2 つの基本的な手順が必要です。まず、必要な依存関係を含め、次に、イントロスペクションエンドポイントの詳細を示します。

認可サーバーの指定

イントロスペクションエンドポイントの場所を指定するには、次のようにします。

security:
  oauth2:
    resourceserver:
      opaque-token:
        introspection-uri: https://idp.example.com/introspect
        client-id: client
        client-secret: secret

ここで、 https://idp.example.com/introspect (英語)  は認証サーバーによってホストされるイントロスペクションエンドポイントであり、 client-id および  client-secret はそのエンドポイントをヒットするために必要な資格情報です。

リソースサーバーはこれらのプロパティを使用して、さらに自己構成し、受信 JWT を検証します。

[Note] メモ

イントロスペクションを使用する場合、認可サーバーの言葉は法律です。認可サーバーがトークンが有効であるとレスポンスした場合、有効です。

以上です!

スタートアップの期待

このプロパティとこれらの依存関係が使用されると、リソースサーバーは自動的に不透明なベアラートークンを検証するように構成します。

この起動プロセスは、エンドポイントを検出する必要がなく、追加の検証ルールが追加されないため、JWT よりもかなり単純です。

ランタイムの期待

アプリケーションが起動すると、Resource Server は  Authorization: Bearer ヘッダーを含むリクエストの処理を試みます。

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

このスキームが示されている限り、Resource Server は Bearer Token 仕様に従ってリクエストの処理を試みます。

Opaque トークンを指定すると、リソースサーバーは

  1. 指定された資格情報とトークンを使用して、指定されたイントロスペクションエンドポイントを照会する
  2. { 'active' : true } 属性のレスポンスをインスペクションする
  3. 各スコープをプレフィックス  SCOPE_ を持つ機関にマップする

結果として得られる  Authentication#getPrincipal は、デフォルトでは Spring Security  OAuth2AuthenticatedPrincipal (Javadoc)  オブジェクトであり、 Authentication#getName はトークンの  sub プロパティ(存在する場合)にマップします。

ここから、次の場所にジャンプできます。

12.3.13 認証後の属性の検索

トークンが認証されると、 BearerTokenAuthentication のインスタンスが  SecurityContext に設定されます。

つまり、構成で  @EnableWebMvc を使用する場合、 @Controller メソッドで使用できます。

@GetMapping("/foo")
public String foo(BearerTokenAuthentication authentication) {
    return authentication.getTokenAttributes().get("sub") + " is the subject";
}

BearerTokenAuthentication は  OAuth2AuthenticatedPrincipal を保持するため、コントローラーメソッドでも使用できることも意味します。

@GetMapping("/foo")
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
    return principal.getAttribute("sub") + " is the subject";
}

SpEL を介した属性の検索

もちろん、これは、SpEL を介して属性にアクセスできることも意味します。

例:  @PreAuthorize アノテーションを使用できるように  @EnableGlobalMethodSecurity を使用する場合、次のことができます。

@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public String forFoosEyesOnly() {
    return "foo";
}

12.3.14 Boot 自動構成のオーバーライドまたは置換

Spring Boot がリソースサーバーに代わって生成する 2 つの  @Bean があります。

1 つ目は、アプリをリソースサーバーとして構成する  WebSecurityConfigurerAdapter です。Opaque トークンを使用する場合、この  WebSecurityConfigurerAdapter は次のようになります。

protected void configure(HttpSecurity http) {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken)
}

アプリケーションが  WebSecurityConfigurerAdapter Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えることは、アプリケーション内で Bean を公開するのと同じくらい簡単です。

@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .opaqueToken()
                    .introspector(myIntrospector());
    }
}

上記では、 /messages/ で始まる URL の  message:read のスコープが必要です。

oauth2ResourceServer DSL のメソッドも自動構成をオーバーライドまたは置き換えます。

例: 2 番目の  @Bean Spring Boot が作成する  OpaqueTokenIntrospector は、 String トークンを  OAuth2AuthenticatedPrincipal の検証済みインスタンスにデコードします。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

アプリケーションが  OpaqueTokenIntrospector Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

そして、その構成は、 introspectionUri() および  introspectionClientCredentials() を使用してオーバーライドするか、 introspector() を使用して置き換えることができます。

introspectionUri() を使用する

認可サーバーの Introspection Uri は 、構成プロパティとして構成するか、DSL で提供できます。

@EnableWebSecurity
public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .opaqueToken()
                    .introspectionUri("https://idp.example.com/introspect")
                    .introspectionClientCredentials("client", "secret");
    }
}

introspectionUri() の使用は、構成プロパティよりも優先されます。

introspector() を使用する

introspectionUri() よりも強力なのは  introspector() です。これは、 OpaqueTokenIntrospector の Boot 自動構成を完全に置き換えます。

@EnableWebSecurity
public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .opaqueToken()
                    .introspector(myCustomIntrospector());
    }
}

これは、権限マッピングJWT の取り消し、またはリクエストタイムアウトなどのより深い構成が必要な場合に便利です。

OpaqueTokenIntrospector の公開 @Bean

または、 OpaqueTokenIntrospector を公開すると @Bean は  introspector() と同じ効果があります。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

12.3.15 認可の構成

OAuth 2.0 Introspection エンドポイントは、通常、 scope 属性を返し、付与されたスコープ(または権限)を示します。例:

{ …​, "scope" : "messages contacts"}

この場合、Resource Server はこれらのスコープを付与された権限のリストに強制し、各スコープの前に文字列「SCOPE_」を付けようとします。

つまり、Opaque トークンから派生したスコープを持つエンドポイントまたはメソッドを保護するには、対応する式に次のプレフィックスを含める必要があります。

@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorizeRequests -> authorizeRequests
                .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
    }
}

または、同様にメソッドセキュリティで:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}

権限の手動抽出

デフォルトでは、Opaque トークンサポートは、イントロスペクションレスポンスからスコープ要求を抽出し、個々の  GrantedAuthority インスタンスに解析します。

例: イントロスペクションのレスポンスが次の場合:

{
    "active" : true,
    "scope" : "message:read message:write"
}

次に、Resource Server は、 message:read 用と  message:write 用の 2 つの権限を持つ  Authentication を生成します。

これはもちろん、属性セットを見て独自の方法で変換するカスタム  OpaqueTokenIntrospector を使用してカスタマイズできます。

public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        return new DefaultOAuth2AuthenticatedPrincipal(
                principal.getName(), principal.getAttributes(), extractAuthorities(principal));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
        return scopes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

その後、このカスタムイントロスペクターは、 @Bean として公開するだけで構成できます。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new CustomAuthoritiesOpaqueTokenIntrospector();
}

12.3.16 タイムアウトの構成

デフォルトでは、Resource Server は認可サーバーとの調整にそれぞれ 30 秒の接続およびソケットタイムアウトを使用します。

これはいくつかのシナリオでは短すぎるかもしれません。さらに、バックオフや発見などのより高度なパターンは考慮されません。

Resource Server が認可サーバーに接続する方法を調整するために、 NimbusOpaqueTokenIntrospector は  RestOperations のインスタンスを受け入れます。

@Bean
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .basicAuthentication(clientId, clientSecret)
            .setConnectionTimeout(60000)
            .setReadTimeout(60000)
            .build();

    return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}

12.3.17 JWT でのイントロスペクションの使用

よくある質問は、イントロスペクションが JWT と互換性があるかどうかです。Spring Security の Opaque トークンサポートは、トークンの形式を気にしないように設計されています。提供されたイントロスペクションエンドポイントにトークンを喜んで渡します。

JWT が取り消された場合に備えて、リクエストごとに認可サーバーで確認する必要がある要件があるとしましょう。

トークンに JWT 形式を使用している場合でも、検証方法はイントロスペクションです。つまり、次のようにします。

spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://idp.example.org/introspection
          client-id: client
          client-secret: secret

この場合、結果の  Authentication は  BearerTokenAuthentication になります。対応する  OAuth2AuthenticatedPrincipal の属性は、イントロスペクションエンドポイントによって返されたものです。

しかし、奇妙なことに、イントロスペクションエンドポイントは、トークンがアクティブであるかどうかのみを返します。それで?

この場合、エンドポイントにヒットするカスタム  OpaqueTokenIntrospector を作成できますが、返されたプリンシパルを更新して、属性として JWT クレームを取得します。

public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor());

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        try {
            Jwt jwt = this.jwtDecoder.decode(token);
            return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES);
        } catch (JwtException e) {
            throw new OAuth2IntrospectionException(e);
        }
    }

    private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor<SecurityContext> {
        JWTClaimsSet process(SignedJWT jwt, SecurityContext context)
                throws JOSEException {
            return jwt.getJWTClaimSet();
        }
    }
}

その後、このカスタムイントロスペクターは、 @Bean として公開するだけで構成できます。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new JwtOpaqueTokenIntropsector();
}

12.3.18 /userinfo エンドポイントの呼び出し

一般的に、リソースサーバーは、基になるユーザーを気にするのではなく、付与された権限を気にします。

ただし、認証ステートメントをユーザーに結び付けることが重要な場合があります。

適切な  ClientRegistrationRepository をセットアップして、アプリケーションが  spring-security-oauth2-client も使用している場合、これはカスタム  OpaqueTokenIntrospector で非常に簡単です。以下のこの実装は、3 つのことを行います。

  • トークンの有効性を確認するために、イントロスペクションエンドポイントに委譲する
  • /userinfo エンドポイントに関連付けられた適切なクライアント登録を検索する
  • /userinfo エンドポイントからレスポンスを呼び出して返する
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService();

    private final ClientRegistrationRepository repository;

    // ... constructor

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
        Instant issuedAt = authorized.getAttribute(ISSUED_AT);
        Instant expiresAt = authorized.getAttribute(EXPIRES_AT);
        ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id");
        OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
        OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token);
        return this.oauth2UserService.loadUser(oauth2UserRequest);
    }
}

spring-security-oauth2-client を使用していない場合でも、非常に簡単です。 WebClient の独自のインスタンスで  /userinfo を呼び出すだけです。

public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final WebClient rest = WebClient.create();

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
        return makeUserInfoRequest(authorized);
    }
}

いずれにしても、 OpaqueTokenIntrospector を作成したら、それを  @Bean として公開してデフォルトをオーバーライドする必要があります。

@Bean
OpaqueTokenIntrospector introspector() {
    return new UserInfoOpaqueTokenIntrospector(...);
}

12.3.19 JWT と Opaque トークンの両方をサポート

場合によっては、両方の種類のトークンにアクセスする必要があります。例: 1 つのテナントが JWT を発行し、他のテナントが Opaque トークンを発行する複数のテナントをサポートできます。

リクエスト時にこの決定を行う必要がある場合は、 AuthenticationManagerResolver を使用して次のように実現できます。

@Bean
AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver() {
    BearerTokenResolver bearerToken = new DefaultBearerTokenResolver();
    JwtAuthenticationProvider jwt = jwt();
    OpaqueTokenAuthenticationProvider opaqueToken = opaqueToken();

    return request -> {
        String token = bearerToken.resolve(request);
        if (isAJwt(token)) {
            return jwt::authenticate;
        } else {
            return opaqueToken::authenticate;
        }
    }
}

そして、DSL でこの  AuthenticationManagerResolver を指定します。

http
    .authorizeRequests()
        .anyRequest().authenticated()
        .and()
    .oauth2ResourceServer()
        .authenticationManagerResolver(this.tokenAuthenticationManagerResolver);

12.3.20 マルチテナンシー

リソースサーバーは、何らかのテナント ID をキーとするベアラートークンを検証するための複数の戦略がある場合、マルチテナントと見なされます。

例: リソースサーバーは、2 つの異なる認可サーバーからベアラートークンを受け入れる場合があります。または、認可サーバーが複数の発行者を表している場合があります。

いずれの場合も、実行する必要がある 2 つの事柄と、それらの選択方法に関連するトレードオフがあります。

  1. テナントを解決する
  2. テナントを伝播する

リクエスト材料によるテナントの解決

リクエストマテリアルによるテナントの解決は、次のように、実行時に  AuthenticationManager を決定する  AuthenticationManagerResolver を実装することで実行できます。

@Component
public class TenantAuthenticationManagerResolver
        implements AuthenticationManagerResolver<HttpServletRequest> {
    private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
    private final TenantRepository tenants; 1

    private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); 2

    public TenantAuthenticationManagerResolver(TenantRepository tenants) {
        this.tenants = tenants;
    }

    @Override
    public AuthenticationManager resolve(HttpServletRequest request) {
        return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant);
    }

    private String toTenant(HttpServletRequest request) {
        String[] pathParts = request.getRequestURI().split("/");
        return pathParts.length > 0 ? pathParts[1] : null;
    }

    private AuthenticationManager fromTenant(String tenant) {
        return Optional.ofNullable(this.tenants.get(tenant)) 3
                .map(JwtDecoders::fromIssuerLocation) 4
                .map(JwtAuthenticationProvider::new)
                .orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
    }
}

1

テナント情報の仮想ソース

2

テナント識別子をキーとする「AuthenticationManager」のキャッシュ

3

テナントの検索は、単に発行者の場所をその場で計算するよりも安全です - ルックアップはテナントのホワイトリストとして機能する

4

ディスカバリエンドポイントを介して  JwtDecoder を作成します。ここでの遅延検索は、起動時にすべてのテナントを構成する必要がないことを意味します

そして、DSL でこの  AuthenticationManagerResolver を指定します。

http
    .authorizeRequests()
        .anyRequest().authenticated()
        .and()
    .oauth2ResourceServer()
        .authenticationManagerResolver(this.tenantAuthenticationManagerResolver);

クレームによるテナントの解決

クレームによるテナントの解決は、リクエスト資料による解決と同様です。唯一の本当の違いは、 toTenant メソッドの実装です。

@Component
public class TenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
    private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
    private final TenantRepository tenants; 1

    private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); 2

    public TenantAuthenticationManagerResolver(TenantRepository tenants) {
        this.tenants = tenants;
    }

    @Override
    public AuthenticationManager resolve(HttpServletRequest request) {
        return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant); 3
    }

    private String toTenant(HttpServletRequest request) {
        try {
            String token = this.resolver.resolve(request);
            return (String) JWTParser.parse(token).getJWTClaimsSet().getIssuer();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    private AuthenticationManager fromTenant(String tenant) {
        return Optional.ofNullable(this.tenants.get(tenant)) 4
                .map(JwtDecoders::fromIssuerLocation) 5
                .map(JwtAuthenticationProvider::new)
                .orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
    }
}

1

テナント情報の仮想ソース

2

テナント識別子をキーとする「AuthenticationManager」のキャッシュ

3 4

テナントの検索は、単に発行者の場所をその場で計算するよりも安全です - ルックアップはテナントのホワイトリストとして機能する

5

ディスカバリエンドポイントを介して  JwtDecoder を作成します。ここでの遅延検索は、起動時にすべてのテナントを構成する必要がないことを意味します

http
    .authorizeRequests()
        .anyRequest().authenticated()
        .and()
    .oauth2ResourceServer()
        .authenticationManagerResolver(this.tenantAuthenticationManagerResolver);

クレームの解析は 1 回のみ

この戦略は単純ですが、JWT が  AuthenticationManagerResolver によって一度解析され、次に  JwtDecoder によって再び解析されるというトレードオフが伴うことに気づいたかもしれません。

Nimbus の  JWTClaimSetAwareJWSKeySelector を使用して  JwtDecoder を直接設定することにより、この余分な解析を軽減できます。

@Component
public class TenantJWSKeySelector
    implements JWTClaimSetAwareJWSKeySelector<SecurityContext> {

    private final TenantRepository tenants; 1
    private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); 2

    public TenantJWSKeySelector(TenantRepository tenants) {
        this.tenants = tenants;
    }

    @Override
    public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
            throws KeySourceException {
        return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
                .selectJWSKeys(jwsHeader, securityContext);
    }

    private String toTenant(JWTClaimsSet claimSet) {
        return (String) claimSet.getClaim("iss");
    }

    private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
        return Optional.ofNullable(this.tenantRepository.findById(tenant)) 3
                .map(t -> t.getAttrbute("jwks_uri"))
                .map(this::fromUri)
                .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
    }

    private JWSKeySelector<SecurityContext> fromUri(String uri) {
        try {
            return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); 4
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

1

テナント情報の仮想ソース

2

テナント識別子をキーとする「JWKKeySelector」のキャッシュ

3

テナントのルックアップは、単に JWK Set エンドポイントをオンザフライで計算するよりも安全です - ルックアップはテナントホワイトリストとして機能する

4

JWK Set エンドポイントから返されるキーの種類を介して  JWSKeySelector を作成します。ここでの遅延検索は、起動時にすべてのテナントを構成する必要がないことを意味します

上記のキーセレクターは、多くのキーセレクターの構成です。JWT の  iss クレームに基づいて、使用するキーセレクターを選択します。

[Note] メモ

このアプローチを使用するには、トークンの署名の一部としてクレームセットを含めるように認可サーバーが構成されていることを確認してください。これがなければ、発行者が悪役によって改ざんされていないという保証はありません。

次に、 JWTProcessor を作成できます。

@Bean
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
    ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor();
    jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
    return jwtProcessor;
}

すでに見たように、テナント認識をこのレベルに下げるためのトレードオフは、より多くの構成です。もう少しあります。

次に、発行者を検証していることを確認します。ただし、発行者は JWT ごとに異なる可能性があるため、テナント対応バリデーターも必要になります。

@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
    private final TenantRepository tenants;
    private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();

    public TenantJwtIssuerValidator(TenantRepository tenants) {
        this.tenants = tenants;
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
                .validate(token);
    }

    private String toTenant(Jwt jwt) {
        return jwt.getIssuer();
    }

    private JwtIssuerValidator fromTenant(String tenant) {
        return Optional.ofNullable(this.tenants.findById(tenant))
                .map(t -> t.getAttribute("issuer"))
                .map(JwtIssuerValidator::new)
                .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
    }
}

テナント対応プロセッサーとテナント対応バリデーターができたため、 JwtDecoder の作成に進むことができます:

@Bean
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
    NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
    OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
            (JwtValidators.createDefault(), this.jwtValidator);
    decoder.setJwtValidator(validator);
    return decoder;
}

テナントの解決についてお話ししました。

リクエストマテリアルによってテナントを解決することを選択した場合は、同じメソッドでダウンストリームリソースサーバーに対処する必要があります。例: サブドメインで解決する場合、同じサブドメインを使用してダウンストリームリソースサーバーをアドレス指定する必要があります。

ただし、ベアラートークンの要求によって解決する場合は、Spring Security のベアラートークン伝播のサポートについて学習してください。

12.3.21 ベアラートークンの解決

デフォルトでは、Resource Server は  Authorization ヘッダーでベアラートークンを探します。ただし、これはいくつかの方法でカスタマイズできます。

カスタムヘッダーからベアラートークンを読み取る

例: カスタムヘッダーからベアラートークンを読み取る必要がある場合があります。これを実現するために、次の例に示すように、 HeaderBearerTokenResolver インスタンスを DSL に接続できます。

http
    .oauth2ResourceServer()
        .bearerTokenResolver(new HeaderBearerTokenResolver("x-goog-iap-jwt-assertion"));

フォームパラメーターからベアラートークンを読み取る

または、以下に示すように、 DefaultBearerTokenResolver を構成することで実行できるフォームパラメーターからトークンを読み取ることもできます。

DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
http
    .oauth2ResourceServer()
        .bearerTokenResolver(resolver);

12.3.22 ベアラートークンの伝播

これでベアラートークンを所有しているため、それをダウンストリームサービスに渡すと便利かもしれません。 ServletBearerExchangeFilterFunction (Javadoc) では、これは非常に簡単です。次の例で確認できます。

@Bean
public WebClient rest() {
    return WebClient.builder()
            .filter(new ServletBearerExchangeFilterFunction())
            .build();
}

上記の  WebClient を使用してリクエストを実行すると、Spring Security は現在の  Authentication を検索し、 AbstractOAuth2Token (Javadoc)  資格情報を抽出します。次に、 Authorization ヘッダーでそのトークンを伝搬します。

例:

this.rest.get()
        .uri("https://other-service.example.com/endpoint")
        .retrieve()
        .bodyToMono(String.class)
        .block()

https://other-service.example.com/endpoint (英語) を呼び出して、ベアラートークン  Authorization ヘッダーを追加します。

この動作をオーバーライドする必要がある場所では、次のようにヘッダーを自分で指定するだけです。

this.rest.get()
        .uri("https://other-service.example.com/endpoint")
        .headers(headers -> headers.setBearerAuth(overridingToken))
        .retrieve()
        .bodyToMono(String.class)
        .block()

この場合、フィルターはフォールバックし、リクエストを Web フィルターチェーンの残りの部分に単純に転送します。

[Note] メモ

OAuth 2.0 クライアントフィルター機能 (Javadoc) とは異なり、このフィルター関数は、トークンが期限切れになった場合、トークンを更新しようとしません。このレベルのサポートを取得するには、OAuth 2.0 クライアントフィルターを使用してください。

RestTemplate サポート

現時点では  RestTemplate の専用サポートはありませんが、独自のインターセプターを使用して非常に簡単に伝播を実現できます。

@Bean
RestTemplate rest() {
    RestTemplate rest = new RestTemplate();
    rest.getInterceptors().add((request, body, execution) -> {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return execution.execute(request, body);
        }

        if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
            return execution.execute(request, body);
        }

        AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
        request.getHeaders().setBearerAuth(token.getTokenValue());
        return execution.execute(request, body);
    });
    return rest;
}
現行バージョンへ切り替える