Java
OAuth
spring-security
spring-boot

第3回:Spring Security 5でサポートされたOAuth 2.0 Loginの処理の流れを(深く)理解する

前回「Spring Security 5でサポートされるOAuth 2.0 Loginの大まかな処理の流れを理解する」で大まかな処理の流れを紹介したので、それらをさらにブレークダウンした処理の流れなどをみていきたいと思います。

前提バージョン

  • Spring Boot 2.0.0.M7
  • Spring Security 5.0.0.RELEASE

認可要求

クライアントAPからプロバイダの認可画面を表示(=正確にはクライアントからプロバイダへリダイレクト)する時の処理フローをみていきます。前回紹介した図だと、赤線で囲んだところがここでの説明対象になります。

image.png

認可要求(認可画面表示要求)が行われると、OAuth2AuthorizationRequestRedirectFilterを起点として、ClientRegistrationRepositoryAuthorizationRequestRepositoryRedirectStrategyなどと連携して処理を行います。

image.png

処理の流れ
リソースオーナは、ログイン画面に表示されているリンクを押下して、「認可画面表示要求(GET /oauth2/authorization/{registrationId})」を行う。デモアプリケーションではプロバイダにGitHubを利用しているので、registrationIdgithubになる。
OAuth2AuthorizationRequestRedirectFilterは、ClientRegistrationRepositoryインタフェースのfindByRegistrationIdメソッドを呼び出して、引数に渡したregistrationIdに対応するクライアント登録情報(ClientRegistration)を参照する。
OAuth2AuthorizationRequestRedirectFilterは、クライアント登録情報から「認可要求用のリクエスト情報(OAuth2AuthorizationRequest)」を生成し、AuthorizationRequestRepositorysaveAuthorizationRequestメソッドを呼び出すことで、「認可要求用のリクエスト情報」をリクエストを跨いで共有できる領域(セッションなど)に保存する。(=後述の「認可後の認証」の中で参照する)
OAuth2AuthorizationRequestRedirectFilterは、クライアント登録情報からプロバイダ提供の「認可エンドポイント」に移動するためのURLを生成し、RedirectStrategyを介してプロバイダ提供のページにリダイレクトする。

認可後の認証

プロバイダの認可画面で認可を行った後に認証する時の処理フローをみていきます。前回紹介した図だと、赤線で囲んだところがここでの説明対象になります。

image.png

認可が行われると、OAuth2LoginAuthenticationFitlerを起点として、ClientRegistrationRepositoryAuthorizationRequestRepositoryAuthenticationManagerAuthenticationProviderなどと連携して処理を行います。

image.png

ちょっと処理フローが長いので、「認証前処理」「認証処理」「認証後処理」の3つのフェーズに分けて説明したいと思います。

認証前処理

まず、認証処理に必要な情報を取集し、OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken)を生成します。

image.png

処理の流れ
リソースオーナは、認可画面に表示されている「認可ボタン」を押下してプロバイダのユーザ情報を使用してデモアプリケーションにログインすることを許可する。プロバイダ側での処理が完了すると、デモアプリケーションの「認可後の認証(GET /login/oauth2/code/{registrationId})」処理が呼び出される(=実際にはリダイレクトされる)。デモアプリケーションではプロバイダにGitHubを利用しているので、registrationIdgithubになる。
OAuth2LoginAuthenticationFilterは、リクエストよりプロバイダからの認可応答を解析して「認可要求のレスポンス情報(OAuth2AuthorizationResponse)」を生成する。
OAuth2LoginAuthenticationFilterは、AuthorizationRequestRepositoryインタフェースのloadAuthorizationRequestメソッドを呼び出して、「認可要求のリクエスト情報(OAuth2AuthorizationRequest)」を復元する。(復元した情報はAuthorizationRequestRepositoryインタフェースのremoveAuthorizationRequestメソッドを呼び出して削除する)
OAuth2LoginAuthenticationFilterは、ClientRegistrationRepositoryインタフェースのfindByRegistrationIdメソッドを呼び出して、引数に渡したregistrationIdに対応する「クライアント登録情報(ClientRegistration)」を参照する。
OAuth2LoginAuthenticationFilterは、「認可要求のリクエスト情報(OAuth2AuthorizationRequest)」「認可要求のレスポンス情報(OAuth2AuthorizationResponse)」「クライアント登録情報(ClientRegistration)」を保持する「OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken)」を生成する。

認証処理

つぎに、AuthenticationManagerに「OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken)」を渡して認証処理をします実行(authenticateメソッドを呼び出す)。

image.png

処理の流れ
OAuth2LoginAuthenticationFilterは、AuthenticationManagerインタフェースのauthenticateメソッドに「OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken)」を渡して、リソースオーナの認証を行う。
AuthenticationManager(実体はProviderManagerクラス)は、認証処理をAuthenticationProviderauthenticateメソッドへ委譲する。
OAuth2 Login用のAuthenticationProviderインターフェースの実装クラスは、「トークンエンドポイント」「ユーザ情報エンドポイント」「JWKsエンドポイント(OIDC 1.0のみ)」にアクセスしてアクセストークンとユーザ情報を取得し、認証結果を返却する。(AuthenticationProviderの実装クラスは、OAuth 2.0用とOIDC 1.0用の2つのクラスが提供されています→詳細は後述します)

認証後処理

さいごに、「認証結果トークン(OAuth2LoginAuthenticationToken)」をもとにリソースオーナをSpring Securiryの世界で認証済み(ログイン済み)の状態にします。

image.png

処理の流れ
OAuth2LoginAuthenticationFilterは、認証結果から認証情報(OAuth2AnthenticationToken)を生成し、SecurityContextに設定する。SecurityContextに認証情報を設定することで、Spring Securiryの世界で認証済み(ログイン済み)の状態となる。
OAuth2LoginAuthenticationFilterは、認証結果から「認証済みクライアント情報(OAuth2AuthorizedClient)」を生成し、OAuth2AuthorizedClientServicesaveAuthorizedClinetメソッドを呼び出すことで、「認証済みクライアント情報」を任意のクラスからアクセスできる領域に保存する。(デモアプリケーションでは、DemoControllerの中から「認証済みクライアント情報」へアクセスしている)

AuthenticationProvider

Spring Securityは、ひとつのアプリケーション内で(同時に)複数の認証方式(例えば、フォームログイン、Basic認証、OAuth2.0 Login認証など)をサポートするためのAuthenticationManagerの実装クラスとしてProviderManagerを提供しています。ProviderManagerは、複数のAuthenticationProviderの実装クラスを保持し具体的な認証処理をAuthenticationProviderに委譲するスタイルを採用しており、OAuth 2.0/OIDC 1.0 Login用のAuthenticationProviderが提供されています。

参考)AuthenticationProviderのメソッド定義
public interface AuthenticationProvider {

    // 認証処理を行うメソッド
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    // authenticateメソッドの引数に渡されるAuthenticationの型が
    // このクラスでサポートする認証方式か否かを判定するメソッド
    boolean supports(Class<?> authentication);

}

OAuth 2.0用のAuthenticationProvider

Spring Securityは、OAuth 2.0をサポートするプロバイダで管理しているユーザでログインするためのAuthenticationProviderの実装クラスとしてOAuth2LoginAuthenticationProviderを提供しており、以下の条件をすべてみたす際にこのクラスで認証処理が行われます。

  • authenticateメソッドの引数に渡されるAuthenticationの型がOAuth2LoginAuthenticationTokenに割り当てることができる
  • 認可要求時のスコープにopenidが含まれていない(→スコープにopenidが含まれている場合はOIDC 1.0用のAuthenticationProviderで処理が行われる)

image.png

処理の流れ
OAuth2LoginAuthenticationProviderは、OAuth2AccessTokenResponseClientインタフェースのgetTokenResponseメソッドを呼び出すことで、「トークンエンドポイント」からアクセストークンを取得する。Spring Seuciryは、Nimbus OAuth 2.0 SDKのAPIを利用してアクセストークンを取得する実装クラス(NimbusAuthorizationCodeTokenResponseClient)を提供しています。
OAuth2LoginAuthenticationProviderは、OAuth2UserServiceインタフェースのloadUserメソッドを呼び出すことで、「ユーザ情報ポイント(アクセストークンにひもづくユーザ情報を取得するAPI)」からユーザ情報を取得する。Spring Seuciryは、Nimbus OAuth 2.0 SDKのAPIを利用してアクセストークンを取得する実装クラス(DefaultOAuth2UserService)を提供しています。
OAuth2LoginAuthenticationProviderは、認証結果を返却するためにOAuth2LoginAuthenticationTokenを生成する。

OIDC 1.0用のAuthenticationProvider

Spring Securityは、OIDC 1.0をサポートするプロバイダで管理しているユーザでログインするためのAuthenticationProviderの実装クラスとしてOidcAuthorizationCodeAuthenticationProviderを提供しており、以下の条件をすべてみたす際にこのクラスで認証処理が行われます。

  • authenticateメソッドの引数に渡されるAuthenticationの型がOAuth2LoginAuthenticationTokenに割り当てることができる
  • 認可要求時のスコープにopenidが含まれる

image.png

処理の流れ
OidcAuthorizationCodeAuthenticationProviderは、OAuth2AccessTokenResponseClientインタフェースのgetTokenResponseメソッドを呼び出すことで、「トークンエンドポイント」からアクセストークン(+IDトークン)を取得する。Spring Seuciryは、Nimbus OAuth 2.0 SDKのAPIを利用してアクセストークン(+IDトークン)を取得する実装クラス(NimbusAuthorizationCodeTokenResponseClient)を提供しています。
OidcAuthorizationCodeAuthenticationProviderは、JwtDecorderインタフェースのdecodeメソッドを呼び出すことで、IDトークンに含まれるクレーム(リソースオーナの識別子や認証情報など)を取得する。Spring Seuciryは、Nimbus OAuth 2.0 SDKのAPIを利用して「JWKsエンドポイント」からJWKを取得してIDトークンの検証+デコードを行う実装クラス(NimbusJwtDecoderJwkSupport)を提供しています。
OidcAuthorizationCodeAuthenticationProviderは、OAuth2UserServiceインタフェースのloadUserメソッドを呼び出すことで、「ユーザ情報ポイント」からユーザ情報(クレーム)を取得する。Spring Seuciryは、Nimbus OAuth 2.0 SDKのAPIを利用してアクセストークンを取得する実装クラス(OidcUserService)を提供しています。
OAuth2LoginAuthenticationProviderは、認証結果を返却するためにOAuth2LoginAuthenticationTokenを生成する。

参考: OAuth 2.0/OIDC 1.0 Login機能で利用するモデルクラス

参考までに、本エントリーで紹介した処理フローの中で登場したモデルクラス(一部紹介してないクラスもあるけど・・)の構成を載せておきます。

NOTE:

モデルクラスっていう表現が正しいのかは全く自身なし・・・よい表現があったらコメントください!!

全体像

全体像の構成はこんな感じですが、ぱっと見だとよくわからないですね・・・ :sweat_smile: ということで・・・私の独断でこれらのクラスをいくつか分類してクラスの役割などを紹介します。

image.png

クライアント登録情報

まずは・・・クライアント登録情報を起点としたクラス構成を紹介します。

image.png

クラス名 説明
ClientRegistration プロバイダの各種エンドポイントにアクセスする際に必要となる「クライアント情報(クライアントID、クライアントシークレット、要求スコープ、認可後のリダイレクト先URIテンプレート)」「使用するグラントタイプ(認可コード vs インプリシット)」「トークンエンドポイントアクセス時の認証方式(Basic認証 vs Form認証)」などを保持する。
ProviderDetails プロバイダの各種エンドポイントのURIを保持する。
UserInfoEndpoint プロバイダ提供のユーザ情報を取得するエンドポイントに関する情報(URI、ユーザ識別子の属性名)を保持する。

NOTE:

Spring Securityは、「Google」「GitHub」「Facebook」「Okta」向けのクライアント登録情報を生成するためのサポートクラスとしてCommonOAuth2Providerを提供しています。

OAuth 2.0認証処理用の認証トークン

つぎに・・・AuthenticationManager(AuthenticationProvider)のauthenticateメソッドの引数と返り値として使用する「OAuth 2.0認証処理用の認証トークン」を起点としたクラス構成を紹介します。「OAuth 2.0認証処理用の認証トークン」は、認証依頼(authenticateメソッドの引数に渡す情報)と認証結果(authenticateメソッドの返り値として返却する情報)に分けてみていきます。

認証依頼

image.png

クラス名 説明
OAuth2LoginAuthenticationToken OAuth 2.0認証処理で必要となる情報を「未認証の状態」で保持する。(=他のオブジェクトへの参照を保持するだけのコンテナクラス)
OAuth2AuthorizationExchange 認可要求と認可応答の情報を保持する。(=他のオブジェクトへの参照を保持するだけのコンテナクラス)
OAuth2AuthorizationRequest 認可要求の情報(「アクセスする認可エンドポイントのURI」「使用するグラントタイプ(認可コード vs インプリシット)」「使用する認可応答の種類(認可コード vs アクセストークン)」「認可後のリダイレクト先URI」「使用するスコープ一覧」「CSRF対策用に生成したステート値」「使用するクライアントID」など)を保持する。
OAuth2AuthorizationResponse 認可応答の情報(「リクエストURI=リダイレクト先URI」「プロバイダから戻されたステート値」「プロバイダが発行した認可コード」)を保持する。
OAuth2Error 認可処理のエラー情報(「エラーコード」「エラー内容の説明」「エラー詳細にアクセスするためのURI」)を保持する。

認証結果

image.png

クラス名 説明
OAuth2LoginAuthenticationToken OAuth 2.0認証処理の処理結果を「認証済みの状態」で保持する。(=他のオブジェクトへの参照を保持するだけのコンテナクラス)
OAuth2AccessToken OAuth 2.0のアクセストークン情報(「トークン値」「トークン生成日時」「トークン有効期限日時」「トークンタイプ(Bearerのみサポート)」「スコープ一覧」)を保持する。
OAuth2User OAuth 2.0 Login機能を使用してOAuth 2.0のフローで認証したユーザであることを示す。
DefaultOAuth2User OAuth 2.0 Login機能を使用してOAuth 2.0のフローで認証したユーザのユーザ情報(「権限一覧(デフォルトだと「ROLE_USER」のみ)」「ユーザの属性情報」「ユーザ名(ユーザ識別子)を保持する属性名」)を保持する。
OAuth2UserAuthority OAuth 2.0 Login機能を使用してOAuth 2.0のフローで認証したユーザの権限情報(「権限値」「ユーザの属性情報」)を保持する。

ちなみに・・・OIDC 1.0の仕組みで認証した場合は、OAuth2UserではなくOidcUserを実装したクラスでユーザ情報を保持します。下の図をみてわかるとおり、OIDC 1.0フロー用はクラスはOAuth 2.0フロー用のクラスを継承しているため、OAuth 2.0フロー用のクラスで定義している情報も保持しています。

image.png

クラス名 説明
OidcUser OAuth 2.0 Login機能を使用してOIDC 1.0のフローで認証したユーザであることを示す。
DefaultOidcUser OAuth 2.0 Login機能を使用してOIDC 1.0のフローで認証したユーザのクレーム(「IDトークンの情報」「ユーザ情報(クレーム)」)を保持する。
OidcIdToken IDトークンにひもづく情報(「トークン値」「トークン生成日時」「トークン有効期限日時」「クレーム(ユーザ識別子、認証情報など)」)を保持する。
OidcUserInfo ユーザ情報エンドポイントから取得したクレーム(ユーザのプロフィールなど)を保持する。
OidcUserAuthority OAuth 2.0 Login機能を使用してOIDC 1.0のフローで認証したユーザのクレーム(「IDトークンの情報」「ユーザ情報(クレーム)」)を保持する。

NOTE:

OAuth 2.0ユーザ情報(OAuth2UserOidcUser)は、以下の方法でアクセスすることができます。ここではOAuth2Userにアクセスする方法を紹介しますが、基本的にはOidcUserも同じ方法でアクセスすることができます。

  • Controllerの引数にOAuth2AuthenticationTokenを宣言して取得する

    @GetMapping("/")
    public String index(OAuth2AuthenticationToken authentication, Model model) {
        OAuth2User user = authentication.getPrincipal();
        // ...
    }
    
  • Controllerの引数に宣言(+@AuthenticationPrincipalを付与)して取得する
    Spring Bootの自動コンフィギュレーションを使えば特に何もしなくても@AuthenticationPrincipalが利用できますが、非Spring Boot環境の場合は、@AuthenticationPrincipalを利用できるようにBean定義を行う必要があります。詳しくは、Spring Seucirtyのリファレンスをみてください。

    @GetMapping("/attributes")
    public String userAttributeAtLogin(@AuthenticationPrincipal OAuth2User oauth2User, Model model) {
        // ...
    }
    
  • SecurityContextHolder(SecurityContext)のメソッドを呼び出して取得する

    OAuth2User user =
        OAuth2AuthenticationToken.class.cast(SecurityContextHolder.getContext().getAuthentication())
        .getPrincipal();
    

OAuth 2.0認証情報

つぎに・・・AuthenticationManager(AuthenticationProvider)のauthenticateメソッドを呼び出して認証が成功した後に生成する「OAuth 2.0認証情報」を起点としたクラス構成を紹介します。

image.png

クラス名 説明
OAuth2AuthenticationToken OAuth 2.0 Login機能で認証した際の認証情報(「権限一覧」「ユーザ情報」「使用したクライアント登録情報のID」)を保持する。

NOTE:

OAuth 2.0認証情報は、以下の方法でアクセスすることができます。

  • Controllerの引数に宣言して受け取る

    @GetMapping("/")
    public String index(OAuth2AuthenticationToken authentication, Model model) {
        // ...
    }
    
  • SecurityContextHolder(SecurityContext)のメソッドを呼び出して取得する

    OAuth2AuthenticationToken authentication =
        OAuth2AuthenticationToken.class.cast(SecurityContextHolder.getContext().getAuthentication());
    

OAuth 2.0認証済みクライアント情報

最後に・・・AuthenticationManager(AuthenticationProvider)のauthenticateメソッドを呼び出して認証が成功した後に生成する「OAuth 2.0認証済みクライアント情報」を起点としたクラス構成を紹介します。

image.png

クラス名 説明
OAuth2AuthorizedClient OAuth 2.0 Login機能で認証した際のクライアント情報(「使用したクライアント登録情報」「ユーザ名(ユーザの識別子)」「プロバイダから取得したアクセストークン」)を保持する。

NOTE:

OAuth 2.0認証済みクライアント情報は、OAuth2AuthorizedClientServiceloadAuthorizedClientメソッドを呼び出すことで取得することができます。

@Controller
public class DemoController {

    private final RestOperations restOperations = new RestTemplate();
    private final OAuth2AuthorizedClientService authorizedClientService;

    public DemoController(OAuth2AuthorizedClientService authorizedClientService) {
        this.authorizedClientService = authorizedClientService; // OAuth2AuthorizedClientServiceをインジェクション
    }

    @GetMapping("/attributes/latest")
    public String userLatestAttribute(OAuth2AuthenticationToken authentication, Model model) {
        // loadAuthorizedClientの引数にOAuth 2.0認証情報で保持している「クライアント登録情報ID」と「ユーザ名」を渡す
        OAuth2AuthorizedClient authorizedClient =
               this.authorizedClientService.loadAuthorizedClient(
                       authentication.getAuthorizedClientRegistrationId(),
                       authentication.getName());

        // RestTemplateを使用し、アクセストークンを付与してユーザ情報エンドポイントへアクセスする
        String userInfoUri = authorizedClient.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUri();
        RequestEntity<Void> requestEntity = RequestEntity.get(URI.create(userInfoUri))
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue())
                .build();

        // ...
    }

}

ちなみに・・・ここではRestTemplateを使用してユーザ情報エンドポイントへアクセスする例になっていますが、Spring Framework 5で追加された「WebClient(リアクティブ対応のHTTPクライアント)」を使うこともできます。

Map userAttributes = WebClient.builder()
        .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue())
        .build()
        .get()
        .uri(userInfoUri)
        .retrieve()
        .bodyToMono(Map.class)
        .block();
model.addAttribute("attributes", userAttributes);

まとめ

デモアプリケーションを例に、本エントリも含めて2回にわけてSpring Security 5で追加されたOAuth 2.0/OIDC 1.0 Login機能の仕組み(処理フローなど)をある程度細かくみてきました。本エントリーでは説明を省いた部分もあるので、もっと細かく(実装レベルの)処理フローを知りたい方は、本エントリーを足がかりに?Spring Securityのソースコードを読んでいただければと思います!
次回は・・・Bean定義(自動コンフィギュレーション)やデフォルト動作のカスタマイズ方法などを紹介できればな〜と思っていますが、いつになるかわかりません :smirk:

参考サイト