前回「Spring Security 5でサポートされるOAuth 2.0 Loginの大まかな処理の流れを理解する」で大まかな処理の流れを紹介したので、それらをさらにブレークダウンした処理の流れなどをみていきたいと思います。
前提バージョン
- Spring Boot 2.0.0.M7
- Spring Security 5.0.0.RELEASE
認可要求
クライアントAPからプロバイダの認可画面を表示(=正確にはクライアントからプロバイダへリダイレクト)する時の処理フローをみていきます。前回紹介した図だと、赤線で囲んだところがここでの説明対象になります。
認可要求(認可画面表示要求)が行われると、OAuth2AuthorizationRequestRedirectFilter
を起点として、ClientRegistrationRepository
、AuthorizationRequestRepository
、RedirectStrategy
などと連携して処理を行います。
処理の流れ |
---|
リソースオーナは、ログイン画面に表示されているリンクを押下して、「認可画面表示要求(GET /oauth2/authorization/{registrationId} )」を行う。デモアプリケーションではプロバイダにGitHubを利用しているので、registrationId はgithub になる。 |
OAuth2AuthorizationRequestRedirectFilter は、ClientRegistrationRepository インタフェースのfindByRegistrationId メソッドを呼び出して、引数に渡したregistrationId に対応するクライアント登録情報(ClientRegistration )を参照する。 |
OAuth2AuthorizationRequestRedirectFilter は、クライアント登録情報から「認可要求用のリクエスト情報(OAuth2AuthorizationRequest )」を生成し、AuthorizationRequestRepository のsaveAuthorizationRequest メソッドを呼び出すことで、「認可要求用のリクエスト情報」をリクエストを跨いで共有できる領域(セッションなど)に保存する。(=後述の「認可後の認証」の中で参照する) |
OAuth2AuthorizationRequestRedirectFilter は、クライアント登録情報からプロバイダ提供の「認可エンドポイント」に移動するためのURLを生成し、RedirectStrategy を介してプロバイダ提供のページにリダイレクトする。 |
認可後の認証
プロバイダの認可画面で認可を行った後に認証する時の処理フローをみていきます。前回紹介した図だと、赤線で囲んだところがここでの説明対象になります。
認可が行われると、OAuth2LoginAuthenticationFilter
を起点として、ClientRegistrationRepository
、AuthorizationRequestRepository
、AuthenticationManager
、AuthenticationProvider
などと連携して処理を行います。
ちょっと処理フローが長いので、「認証前処理」「認証処理」「認証後処理」の3つのフェーズに分けて説明したいと思います。
認証前処理
まず、認証処理に必要な情報を取集し、OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken
)を生成します。
処理の流れ |
---|
リソースオーナは、認可画面に表示されている「認可ボタン」を押下してプロバイダのユーザ情報を使用してデモアプリケーションにログインすることを許可する。プロバイダ側での処理が完了すると、デモアプリケーションの「認可後の認証(GET /login/oauth2/code/{registrationId} )」処理が呼び出される(=実際にはリダイレクトされる)。デモアプリケーションではプロバイダにGitHubを利用しているので、registrationId はgithub になる。 |
OAuth2LoginAuthenticationFilter は、リクエストよりプロバイダからの認可応答を解析して「認可要求のレスポンス情報(OAuth2AuthorizationResponse )」を生成する。 |
OAuth2LoginAuthenticationFilter は、AuthorizationRequestRepository インタフェースのloadAuthorizationRequest メソッドを呼び出して、「認可要求のリクエスト情報(OAuth2AuthorizationRequest )」を復元する。(復元した情報はAuthorizationRequestRepository インタフェースのremoveAuthorizationRequest メソッドを呼び出して削除する) |
OAuth2LoginAuthenticationFilter は、ClientRegistrationRepository インタフェースのfindByRegistrationId メソッドを呼び出して、引数に渡したregistrationId に対応する「クライアント登録情報(ClientRegistration )」を参照する。 |
OAuth2LoginAuthenticationFilter は、「認可要求のリクエスト情報(OAuth2AuthorizationRequest )」「認可要求のレスポンス情報(OAuth2AuthorizationResponse )」「クライアント登録情報(ClientRegistration )」を保持する「OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken )」を生成する。 |
認証処理
つぎに、AuthenticationManager
に「OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken
)」を渡して認証処理をします実行(authenticate
メソッドを呼び出す)。
処理の流れ |
---|
OAuth2LoginAuthenticationFilter は、AuthenticationManager インタフェースのauthenticate メソッドに「OAuth2ログイン用の認証トークン(OAuth2LoginAuthenticationToken )」を渡して、リソースオーナの認証を行う。 |
AuthenticationManager (実体はProviderManager クラス)は、認証処理をAuthenticationProvider のauthenticate メソッドへ委譲する。 |
OAuth2 Login用のAuthenticationProvider インターフェースの実装クラスは、「トークンエンドポイント」「ユーザ情報エンドポイント」「JWKsエンドポイント(OIDC 1.0のみ)」にアクセスしてアクセストークンとユーザ情報を取得し、認証結果を返却する。(AuthenticationProvider の実装クラスは、OAuth 2.0用とOIDC 1.0用の2つのクラスが提供されています→詳細は後述します) |
認証後処理
さいごに、「認証結果トークン(OAuth2LoginAuthenticationToken
)」をもとにリソースオーナをSpring Securiryの世界で認証済み(ログイン済み)の状態にします。
処理の流れ |
---|
OAuth2LoginAuthenticationFilter は、認証結果から認証情報(OAuth2AnthenticationToken )を生成し、SecurityContext に設定する。SecurityContextに認証情報を設定することで、Spring Securiryの世界で認証済み(ログイン済み)の状態となる。 |
OAuth2LoginAuthenticationFilter は、認証結果から「認証済みクライアント情報(OAuth2AuthorizedClient )」を生成し、OAuth2AuthorizedClientService のsaveAuthorizedClinet メソッドを呼び出すことで、「認証済みクライアント情報」を任意のクラスからアクセスできる領域に保存する。(デモアプリケーションでは、DemoController の中から「認証済みクライアント情報」へアクセスしている) |
AuthenticationProvider
Spring Securityは、ひとつのアプリケーション内で(同時に)複数の認証方式(例えば、フォームログイン、Basic認証、OAuth2.0 Login認証など)をサポートするためのAuthenticationManager
の実装クラスとしてProviderManager
を提供しています。ProviderManager
は、複数のAuthenticationProvider
の実装クラスを保持し具体的な認証処理をAuthenticationProvider
に委譲するスタイルを採用しており、OAuth 2.0/OIDC 1.0 Login用の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
で処理が行われる)
処理の流れ |
---|
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
が含まれる
処理の流れ |
---|
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:
モデルクラスっていう表現が正しいのかは全く自身なし・・・よい表現があったらコメントください!!
全体像
全体像の構成はこんな感じですが、ぱっと見だとよくわからないですね・・・ ということで・・・私の独断でこれらのクラスをいくつか分類してクラスの役割などを紹介します。
クライアント登録情報
まずは・・・クライアント登録情報を起点としたクラス構成を紹介します。
クラス名 | 説明 |
---|---|
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
メソッドの返り値として返却する情報)に分けてみていきます。
認証依頼
クラス名 | 説明 |
---|---|
OAuth2LoginAuthenticationToken |
OAuth 2.0認証処理で必要となる情報を「未認証の状態」で保持する。(=他のオブジェクトへの参照を保持するだけのコンテナクラス) |
OAuth2AuthorizationExchange |
認可要求と認可応答の情報を保持する。(=他のオブジェクトへの参照を保持するだけのコンテナクラス) |
OAuth2AuthorizationRequest |
認可要求の情報(「アクセスする認可エンドポイントのURI」「使用するグラントタイプ(認可コード vs インプリシット)」「使用する認可応答の種類(認可コード vs アクセストークン)」「認可後のリダイレクト先URI」「使用するスコープ一覧」「CSRF対策用に生成したステート値」「使用するクライアントID」など)を保持する。 |
OAuth2AuthorizationResponse |
認可応答の情報(「リクエストURI=リダイレクト先URI」「プロバイダから戻されたステート値」「プロバイダが発行した認可コード」)を保持する。 |
OAuth2Error |
認可処理のエラー情報(「エラーコード」「エラー内容の説明」「エラー詳細にアクセスするためのURI」)を保持する。 |
認証結果
クラス名 | 説明 |
---|---|
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フロー用のクラスで定義している情報も保持しています。
クラス名 | 説明 |
---|---|
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ユーザ情報(
OAuth2User
やOidcUser
)は、以下の方法でアクセスすることができます。ここでは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認証情報」を起点としたクラス構成を紹介します。
クラス名 | 説明 |
---|---|
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認証済みクライアント情報」を起点としたクラス構成を紹介します。
クラス名 | 説明 |
---|---|
OAuth2AuthorizedClient |
OAuth 2.0 Login機能で認証した際のクライアント情報(「使用したクライアント登録情報」「ユーザ名(ユーザの識別子)」「プロバイダから取得したアクセストークン」)を保持する。 |
NOTE:
OAuth 2.0認証済みクライアント情報は、
OAuth2AuthorizedClientService
のloadAuthorizedClient
メソッドを呼び出すことで取得することができます。@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定義(自動コンフィギュレーション)やデフォルト動作のカスタマイズ方法などを紹介できればな〜と思っていますが、いつになるかわかりません
参考サイト
- https://github.com/spring-projects/spring-security/tree/master/samples/boot/oauth2login
- https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-oauth2login
- https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced
- https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal