9.9. OAuth 2.0¶
9.9.1. Overview¶
本節では、OAuth 2.0の概要とSpringプロジェクトの一つであるSpring Securityを使用してOAuth 2.0の仕様に沿った認可制御機能を実装する方法について説明する。
Tip
Spring Security が提供するOAuth 2.0のリファレンス
Spring Security が提供するOAuth 2.0は、本ガイドラインで紹介していない機能も提供している。Spring Security が提供するOAuth 2.0について詳しく知りたい場合は、OAuth2を参照されたい。
9.9.1.1. OAuth 2.0とは¶
OAuth 2.0とは、サードパーティ製アプリケーションがHTTPサービスを利用する際に、サーバ上の保護されたリソースに対するアクセス範囲の指定を可能にするための認可フレームワークのことである。
OAuth 2.0はRFCとして仕様化されており、関連する複数の技術仕様から構成されている。
以下にOAuth 2.0の主要な仕様を示す。
RFC |
概要 |
説明 |
---|---|---|
RFC 6749
|
用語や認可方式などの、OAuth 2.0としてのもっとも基本的な内容が記載されている技術仕様。
|
|
RFC 6750
|
RFC 6749に記載されている認可制御を実現する場合に利用する、「署名なしアクセストークン」(以降、アクセストークンと表す)のサーバ間の受け渡し方法に関する技術仕様。
アクセストークンについては後述する。
|
|
RFC 6819
|
OAuth 2.0を使用するうえで考慮が必要となるセキュリティ要件に関する技術仕様。
本ガイドラインでは検討項目の具体的な説明は割愛する。
|
|
RFC 7519
|
署名が可能なJSONを含んだトークンであるJSON Web Token (JWT)に関する技術仕様。
|
|
RFC 7523
|
RFC 6749に記載されている認可制御の実現する場合に利用するアクセストークンとして、RFC 6819で定められているJWTを利用する方法に関する技術仕様。
|
|
RFC 7009
|
トークンの無効化を行う追加エンドポイントに関する技術仕様。
|
従来のクライアントサーバ型の認証モデルでは、サードパーティ製アプリケーションはHTTPサービスの保護されたリソースにアクセスするために、ユーザの認証情報(ユーザ名とパスワードなど)を利用して認証を行う。
つまり、ユーザは、サードパーティ製アプリケーションにリソースへのアクセス権を与えるために自身の認証情報をサードパーティと共有する必要があるが、これはサードパーティ製アプリケーションに不具合や悪意のある操作などが存在した場合に、ユーザの意図しないアクセスや情報漏洩等のリスクにつながる。
これに対し、OAuth 2.0ではHTTPサービスとの認証はユーザが直接行い、サードパーティ製アプリケーションには「アクセストークン」と呼ばれる認証済みリクエストを行うための情報を払い出すことで、サードパーティに認証情報を共有することなくリソースへアクセスすることが可能となる。
また、アクセストークン発行時にリソースに対するアクセス範囲(スコープ)を指定可能とすることで従来のクライアントサーバ型の認証モデルと比較してより柔軟なアクセス制御を実現している。
9.9.1.2. OAuth 2.0のアーキテクチャ¶
9.9.1.2.1. ロール¶
OAuth 2.0ではロールとして以下の4つを定義している。
ロール名 |
説明 |
---|---|
リソースオーナ
|
保護されたリソースへのアクセスを許可するロール。人(エンドユーザ)など。
|
リソースサーバ
|
保護されたリソースを提供するサーバ。
|
認可サーバ
|
リソースオーナの認証と、アクセストークン(クライアントがリソースサーバにアクセスするときに必要な情報)の発行を行うサーバ。
|
クライアント
|
リソースオーナの認可を得て、リソースオーナの代理として保護されたリソースに対してリクエストを行うロール。Webアプリケーションなど。クライアント情報は事前に認可サーバに登録され、認可サーバ内で一意な情報であるクライアントIDにより管理される。
OAuth 2.0ではクライアントクレデンシャル(クライアントの認証情報)の機密性を維持できる能力に基づき、クライアントタイプとして以下の2つを定義している。
(1) コンフィデンシャル
クライアントクレデンシャルの機密性を維持することができるクライアント。
(2) パブリック
リソースオーナのデバイス上で実行されるクライアントのように、クライアントクレデンシャルの機密性を維持することができず、かつ他の手段を用いたセキュアなクライアント認証が行えないクライアント。
また、OAuth 2.0ではクライアントとして以下のような例を考慮して設計されている。
(1) コンフィデンシャル
(2) パブリック
|
Note
ユーザエージェントは、リソースオーナが使用するWebブラウザ等を指す。本ガイドラインでは、エンドユーザの操作が発生する箇所を明確にするため、リソースオーナ(エンドユーザ)とユーザエージェントを別のものとして解説する。
ガイドラインでリソースオーナと明示している場合に、エンドユーザの操作が発生する。
9.9.1.2.2. スコープ¶
OAuth 2.0では保護されたリソースに対するアクセスを制御する方法としてスコープという概念を使用している。
認可サーバはクライアントからの要求に対し、認可サーバのポリシーまたはリソースオーナの指示に基づいてアクセストークンにスコープを含め、保護されたリソースに対するアクセス権(読み込み権限、書き込み権限など)を指定することが出来る。
9.9.1.2.3. プロトコルフロー¶
OAuth 2.0では、以下のような流れでリソースへのアクセスを行う。
項番 |
説明 |
---|---|
(1)
|
リソースオーナに対して認可を要求する。上の図ではクライアントがリソースオーナに直接要求を行っているが、認可サーバを経由して行うほうが望ましい。
後述するグラントタイプの中では認可コードグラントとインプリシットグラントが認可サーバを経由してリソースオーナに要求を行うフローになっている。
|
(2)
|
クライアントはリソースオーナからの認可を表すクレデンシャルとして認可グラント(後述)を受け取る。
|
(3)
|
クライアントは、認可サーバに対して自身の認証情報とリソースオーナが与えた認可グラントを提示することで、アクセストークンを要求する。
|
(4)
|
認可サーバはクライアントを認証し、認可グラントの正当性を確認する。認可グラントが正当な場合、アクセストークンを発行する。
|
(5)
|
クライアントはリソースサーバの保護されたリソースへリクエストを行い、発行されたアクセストークンにより認証する。
|
(6)
|
リソースサーバはアクセストークンの正当性を確認し、正当な場合、リクエストを受け入れリソースを応答する。
|
Note
OAuth 1.0で不評だった署名とトークン交換の複雑な仕組みを簡略化するために、OAuth 2.0ではアクセストークンを扱うリクエストはHTTPS通信で行うことを必須としている。(HTTPS通信を使用することでアクセストークンの盗聴を防止する)
9.9.1.2.5. アクセストークンのライフサイクル¶
アクセストークンはクライアントが提示する認可グラントの正当性を認可サーバが確認することで発行される。発行されたアクセストークンは、認可サーバのポリシーまたはリソースオーナの指示に基づいたスコープが与えられ、保護されたリソースに対するアクセス権を保持する。アクセストークンは発行時に有効期限が設定され、有効期限切れとなると保護されたリソースに対するアクセス権を失効される。
アクセストークンの発行から失効までの流れは以下のようになる。
項番 |
説明 |
---|---|
(1)
|
クライアントが認可グラントを提示し、アクセストークンを要求する。
|
(2)
|
認可サーバはクライアントが提示した認可グラントを確認し、アクセストークンを発行する。
|
(3)
|
クライアントはアクセストークンを提示し、リソースサーバの保護されたリソースを要求する。
|
(4)
|
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、正当であればリソースサーバの保護されたリソースに対して処理を行う。
|
(5)
|
クライアントはアクセストークン(有効期限切れ)を提示し、リソースサーバの保護されたリソースを要求する。
|
(6)
|
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、アクセストークンの有効期限が切れている場合はエラーを返却する。
|
リフレッシュトークンによるアクセストークンの再発行の流れは以下のようになる。
項番 |
説明 |
---|---|
(1)
|
クライアントが認可グラントを提示し、アクセストークンを要求する。
|
(2)
|
認可サーバはクライアントが提示した認可グラントを確認し、アクセストークンとリフレッシュトークンを発行する。
|
(3)
|
クライアントはアクセストークンを提示し、リソースサーバの保護されたリソースを要求する。
|
(4)
|
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、正当であればリソースサーバの保護されたリソースに対して処理を行う。
|
(5)
|
クライアントはアクセストークン(有効期限切れ)を提示し、リソースサーバの保護されたリソースを要求する。
|
(6)
|
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、アクセストークンの有効期限が切れている場合はエラーを返却する。
|
(7)
|
リソースサーバよりアクセストークンの有効期限切れエラーが返却された場合、クライアントはリフレッシュトークンを提示することで新しいアクセストークンを要求する。
|
(8)
|
認可サーバはクライアントが提示したリフレッシュトークンの正当性を検証し、正当であればアクセストークンとオプションでリフレッシュトークンを発行する。
|
リフレッシュトークンの有効期限が期限切れとなった場合は認可サーバへ認可グラントの再提示を行う。
項番 |
説明 |
---|---|
(1)
|
クライアントが有効期限切れのアクセストークンを提示し、リソースサーバよりアクセストークンの有効期限切れエラーが返却された場合、クライアントはリフレッシュトークン(有効期限切れ)を提示することで新しいアクセストークンを要求する。
|
(2)
|
認可サーバはクライアントが提示したリフレッシュトークンの正当性を検証し、リフレッシュトークンの有効期限が切れている場合はエラーを返却する。
|
(3)
|
認可サーバよりリフレッシュトークンの有効期限切れエラーが返却された場合、クライアントは認可グラントを再提示し、アクセストークンを要求する。
|
(4)
|
認可サーバはクライアントが提示した認可グラントを確認し、アクセストークンとリフレッシュトークンを発行する。
|
9.9.1.3. Spring Securityより提供されるOAuth2のアーキテクチャ¶
Spring Security を使用してリソースサーバ、クライアントを構築した場合、以下のような流れで処理が行われる。
項番 |
説明 |
---|---|
(1)
|
リソースオーナはユーザエージェントを介してクライアントへアクセスする。
|
(2)
|
クライアントは
OAuth2AuthorizedClientManager を介してOAuth2AuthorizedClient を要求する。 |
(3)
|
要求された
OAuth2AuthorizedClient が認可サーバで許可されていない場合、ユーザエージェントへ認可サーバの認可エンドポイントへリダイレクトさせるよう指示する。 |
(4)
|
ユーザエージェントは認可サーバの認可エンドポイントへリダイレクトする。
|
(5)
|
認可エンドポイントはリソースオーナへ認可を問い合わせる画面を表示した後に、リソースオーナからの認可リクエストを受け取り認可コードを発行する。
発行した認可コードは、リダイレクトURIのリクエストパラメータとしてユーザエージェント経由でクライアントに渡される。
|
(6)
|
クライアントは受け取った認可コードを内部のコンテキストに保持し、リクエストを再度処理する。
|
(7)
|
認可コードを取得している場合、認可コードを使用してトークンエンドポイントからアクセストークンを取得する。
取得したアクセストークン、リフレッシュトークンをもとに
OAuth2AuthorizedClient を生成し、Spring Securityが管理するリポジトリに格納する。格納後にSpring Security Filterは元の処理へリダイレクトを行う。
|
(8)
|
クライアントは
OAuth2AuthorizedClientManager を介して(7)で生成したOAuth2AuthorizedClient を要求する。この時、
OAuth2AuthorizedClient が認可された状態となっているため、認可サーバへのリダイレクトは実施しない。 |
(9)
|
OAuth2AuthorizedClient に設定されているアクセストークンの有効期限が切れている場合は、トークンエンドポイントに対しアクセストークンの再払い出しを要求する。 |
(10)
|
クライアントは
RestTemplate のヘッダに(9)で取得したアクセストークンを設定し、リソースサーバにアクセスする。 |
(11)
|
リソースサーバはアクセストークンを受け取ると、アクセストークンの検証を行う。
|
(12)
|
リソースサーバはアクセストークンの検証に成功した場合、クライアントからのリクエストに応じたリソースを返却する。
|
9.9.1.3.2. リソースサーバ¶
リソースサーバでは、アクセストークン自体の妥当性とアクセストークンが保持するスコープ内のリソースへのアクセスであることを検証する機能を提供する。
Spring Securityでは、アクセストークンが付与されていないリクエストを受信した際にWWW-Authenticate
ヘッダーをクライアントへ送信し、アクセストークンでの認証が必要なことを伝える。アクセストークンが送信されると、Spring Securityはアクセストークンの検証を行い、問題がない場合にアプリケーションロジックを実行する。
9.9.1.3.2.1. アクセストークン受信前¶
項番 |
説明 |
---|---|
(1)
|
クライアントからアクセストークンを付与していないリクエストを受信する。
|
(2)
|
リソースサーバはアクセストークンが付与されていないリクエストを受信すると
FilterSecurityInterceptor でAccessDeniedException をスローしアクセスを拒否する。 |
(3)
|
ExceptionTranslationFilter でアクセスを拒否したことを検知し、クライアントへWWW-Authenticate ヘッダーを送信する。クライアントは
WWW-Authenticate が返却されることでアクセストークンでの認証が必要である旨を検知する。 |
9.9.1.3.2.2. アクセストークン受信後¶
アクセストークン受信後のフローを以下に示す。
項番 |
説明 |
---|---|
(1)
|
クライアントからアクセストークンを付与したリクエストを受信する。
|
(2)
|
HttpServletRequest からBearerTokenAuthenticationToken を抽出し、AuthenticationManager へ値を渡す。 |
(3)
|
AuthenticationManager は受け取ったBearerTokenAuthenticationToken の検証を行う。Note
それぞれの認証方法の詳細はJWT及びOpaqueトークンを参照されたい。 |
(4)
|
(3)の検証結果より、クライアントから受け取ったアクセストークンでの認証が成功した場合、認証情報を
SecurityContextHolder に保存する。 |
(4)’
|
(3)の検証結果より、クライアントから受け取ったアクセストークンでの認証が失敗した場合、
SecurityContextHolder の情報を削除し、クライアントへWWW-Authenticate ヘッダーを送信する。 |
(5)
|
クライアントからリクエストされたスコープに基づいたリソースに対する処理を行う。
|
9.9.1.3.3. クライアント¶
クライアントでは、リソースオーナからの認可を取得するために認可サーバへリダイレクトさせる機能と、認可サーバからアクセストークンを取得してリソースサーバへアクセスする機能を提供する。
Spring Securityは、アクセストークンを取得してリソースサーバへアクセスするためにOAuth2AuthorizedClient
、OAuth2AuthorizationRequestRedirectFilter
、OAuth2AuthorizedClientRepository
、OAuth2AuthorizedClientManager
、OAuth2AuthorizedClientProvider
を提供している。
OAuth2AuthorizedClientManager
でOAuth2AuthorizedClient
を管理し、OAuth2AuthorizedClient
が認可前の場合にClientAuthorizationRequiredException
を発生させ、OAuth2AuthorizationRequestRedirectFilter
で例外をハンドリングして認可サーバへリダイレクトさせることが可能となる。
また、OAuth2AuthorizedClientManager
では認可されたOAuth2AuthorizedClient
をOAuth2AuthorizedClientRepository
で管理する機能を提供する。これにより、複数のリクエスト間でアクセストークンを共有することが可能となる。
9.9.1.3.3.1. クライアント認可処理¶
クライアントの認可処理のフローを以下に示す。
項番 |
説明 |
---|---|
(1)
|
ユーザエージェントがクライアントのServiceの呼び出しが行われるよう、Security Filterの処理を実施後にControllerへアクセスする。
|
(2)
|
Serviceより
OAuth2AuthorizedClientManager を呼び出し、OAuth2AuthorizedClient を要求する。 |
(3)
|
OAuth2AuthorizedClient が認可前のため、OAuth2AuthorizedClientProvider はClientAuthorizationRequiredException を発生させ、OAuth2AuthorizationRequestRedirectFilter でハンドリングさせることで、ユーザエージェントに対し認可サーバへのリダイレクトを促す。この時、(1)で受け付けた
HttpServletRequest はHTTPセッションに保存される。 |
(4)
|
ユーザエージェントから認可サーバの認可エンドポイントに対しリダイレクトする。
|
(5)
|
認可サーバで、リソースオーナーの認証、クライアントの認可を実施後、ユーザエージェントに対しクライアントへのリダイレクト処理を促す。その際に、認可コードをパラメータとして設定する。
|
(6)
|
ユーザエージェントは認可処理後のリダイレクトURIに対しリダイレクトを行う。
|
(7)
|
認可サーバから払いだされた認可コードを使用し、認可サーバのトークンエンドポイントに対しアクセストークンを要求する。取得したアクセストークン及びリフレッシュトークンをもとに
OAuth2AuthorizedClient を生成する。生成した
OAuth2AuthorizedClient はOAuth2AuthorizedClientRepository に格納される。格納後、(3)でHTTPセッションに保存した
HttpServletRequest を取り出し再処理を行う。 |
(8)
|
Serviceより
OAuth2AuthorizedClientManager を呼び出し、(7)で生成したOAuth2AuthorizedClient を要求する。 |
(9)
|
(8)で取得した
OAuth2AuthorizedClientManager に設定されているアクセストークンをヘッダに設定し、リソースサーバへアクセスする。 |
9.9.1.3.3.2. クライアント認可後¶
クライアントが認可後のフローを以下に示す。
項番 |
説明 |
---|---|
(1)
|
ユーザエージェントがクライアントのServiceの呼び出しが行われるよう、Security Filterの処理を実施後にControllerへアクセスする。
|
(2)
|
Serviceより
OAuth2AuthorizedClientManager を呼び出し、OAuth2AuthorizedClient を要求する。 |
(3)
|
認可済みの
OAuth2AuthorizedClient に格納されたアクセストークンを取得する。アクセストークンの有効期限が切れている場合、リフレッシュトークンを使用し、認可サーバのトークンエンドポイントに対しアクセストークンのリフレッシュを要求する。リフレッシュされたアクセストークン及びリフレッシュトークンをOAuth2AuthorizedClient に設定する。リフレッシュトークンの有効期限も切れている場合は
OAuth2AuthorizationException がスローされる。例外がスローされた場合、OAuth2AuthorizedClientRepository に保存しているOAuth2AuthorizedClient は削除される。 |
(4)
|
(3)で取得したアクセストークンをヘッダに設定し、リソースサーバへアクセスする。
|
9.9.2. How to use¶
Spring Security が提供するOAuth2を使用するために必要となるBean定義例や実装方法について説明する。
9.9.2.1. How to Useの構成¶
認可グラントで記載した通り、OAuth 2.0ではグラントタイプにより認可サーバ、クライアント間のフローが異なる。そのため、アプリケーションがサポートするグラントタイプに沿った実装を行う必要がある。
本ガイドラインでは、認可コードグラントについてリソースサーバ、クライアントの実装方法の解説を行う。
9.9.2.2. 認可コードグラントの実装¶
認可コードグラントを利用したリソースサーバ、クライアントの実装方法について説明する。
9.9.2.2.1. リソースサーバの実装¶
リソースサーバの実装方法について説明する。
リソースサーバでは、アクセストークンの検証とリソースに対しての認可制御をSpring Securityの機能を使用して提供する。
ここではTODOリソースのREST APIに対して認可制御を実現する方法を説明する。
9.9.2.2.1.1. 依存ライブラリ設定¶
アクセストークンによる認証を行えるようにするため、pom.xml
にライブラリを追加する。マルチプロジェクト構成の場合は、domainプロジェクトのpom.xml
に追加する。
pom.xml
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>
Note
上記設定例の
spring-security-oauth2-resource-server
とspring-security-oauth2-jose
は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xml
でのバージョンの指定は不要である。
9.9.2.2.1.2. 設定ファイルの作成(リソースサーバ)¶
リソースサーバを実装する際には新たにOAuth 2.0用のBean定義ファイルを作成する。
ここでは oauth2-resource.xml
とする。
oauth2-resource.xml
には以下の設定を追加する。
oauth2-resource.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://www.springframework.org/schema/security" xsi:schemaLocation=" http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd "> <sec:http pattern="/api/v1/todos/**" once-per-request="false"> <!-- (1) --> <!-- omitted --> <sec:oauth2-resource-server> <sec:jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json" /> <!-- (2) --> </sec:oauth2-resource-server> </sec:http> </beans>
項番
説明
(1)pattern
属性には認可制御の対象とするパスのパターンを指定する。(2)jwk-set-uri
を指定することでリソースサーバーは自動的にJWTエンコードされたアクセストークンを検証するように構成される。設定するJWK Set uriは使用する認可サーバのアーキテクチャ仕様を確認されたい。
作成したoauth2-resource.xml
を読み込むようにweb.xml
に設定を追加する。
web.xml
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:META-INF/spring/applicationContext.xml classpath*:META-INF/spring/oauth2-resource.xml <!-- (1) --> classpath*:META-INF/spring/spring-security.xml </param-value> </context-param>
項番
説明
(1)oauth2-resource.xml
で設定したパスのパターンを内包するようなパスがspring-security.xml
にアクセス制御対象として設定されている場合を考慮し、先にoauth2-resource.xml
を読み込むようにする。
9.9.2.2.1.3. リソースにアクセス可能なスコープの設定¶
リソースごとにアクセス可能なスコープを定義するために、OAuth 2.0用のBean定義ファイルにスコープを追加する。
実装例は以下の通りである。
oauth2-resource.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://www.springframework.org/schema/security" xsi:schemaLocation=" http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd "> <sec:http pattern="/api/v1/todos/**" once-per-request="false"> <!-- omitted --> <sec:intercept-url pattern="/api/v1/todos/**" method="GET" access="hasAuthority('SCOPE_READ')" /> <!-- (1) --> <sec:intercept-url pattern="/api/v1/todos/**" method="POST" access="hasAuthority('SCOPE_CREATE')" /> <!-- (1) --> <sec:intercept-url pattern="/api/v1/todos/**" method="PUT" access="hasAuthority('SCOPE_UPDATE')" /> <!-- (1) --> <sec:intercept-url pattern="/api/v1/todos/**" method="DELETE" access="hasAuthority('SCOPE_DELETE')" /> <!-- (1) --> <sec:oauth2-resource-server> <sec:jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json" /> </sec:oauth2-resource-server> </sec:http> </beans>
項番
説明
(1)intercept-url
を使用してリソースに対してスコープによるアクセスポリシーを定義する。pattern
属性には保護したいリソースのパスのパターンを指定する。本実装例では/api/v1/todos/
配下のリソースが保護される。method
属性にはリソースのHTTPメソッドを指定する。access
属性にはリソースへのアクセスを認可するscopeを指定する。設定値は大文字、小文字を区別し、スコープの前にSCOPE_
を付ける。
Note
OAuth 2.0認可サーバーから発行されるJWTは
scope
属性またはscp
属性のいずれかを持ち、付与されたスコープや権限を示す。リソースサーバは、これらの属性で受け取った各スコープの前に自動的に
SCOPE_
を付けるため、スコープによる制御を行うためには、SCOPE_
を付ける必要がある。
@GetMapping @PreAuthorize("hasAuthority('SCOPE_READ')") // (1) @ResponseStatus(HttpStatus.OK) public List<TodoResource> getTodosWithPreAuthorize() { return getTodos(); }
項番
説明
(1)PreAuthorize
を使用してリソースに対してスコープによるアクセスポリシーを定義する。
9.9.2.2.2. クライアントの実装¶
クライアントの実装方法について説明する。
クライアントでは、グラントタイプやスコープなどのアプリケーション用件に沿ったパラメータを定義することで、OAuth 2.0機能を使用したリソースへのアクセスが可能となる。
9.9.2.2.2.1. 依存ライブラリ設定¶
Spring Security のOAuth2.0クライアント機能を使用するため、pom.xml
にライブラリを追加する。マルチプロジェクト構成の場合は、domainプロジェクトのpom.xml
に追加する。
pom.xml
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <scope>provided</scope> </dependency>
Note
上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、
pom.xml
でのバージョンの指定は不要である。
9.9.2.2.2.2. 設定ファイルの作成(クライアント)¶
oauth2-client.xml
とする。oauth2-client.xml
には以下の設定を追加する。oauth2-client.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <sec:http pattern="/api/v1/todos/**" once-per-request="false"> <!-- omitted --> <sec:oauth2-client /> <!-- (1) --> </sec:http> </beans>
項番
説明
(1)<sec:http>要素の子要素として<sec:oauth2-client>要素を指定する。<sec:oauth2-client>要素を指定すると、OAuth 2.0 クライアント機能が適用される。Note
本ガイドラインではOAuth 2.0 クライアント機能をデフォルトの状態で使用している。
OAuth2AuthorizedClient
を管理するためのClientRegistrationRepository
及びOAuth2AuthorizedClientRepository
が自動的にBean定義されるが、要件に応じ適切にカスタマイズされたい。カスタマイズ可能な構成オプションについてはOAuth 2.0 Clientを参照されたい。
作成したoauth2-client.xml
を読み込むようにweb.xml
に設定を追加する。
web.xml
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:META-INF/spring/applicationContext.xml classpath*:META-INF/spring/oauth2-client.xml <!-- (1) --> classpath*:META-INF/spring/spring-security.xml </param-value> </context-param>
項番
説明
(1)oauth2-client.xml
で設定したパスのパターンを内包するようなパスがspring-security.xml
にアクセス制御対象として設定されている場合を考慮し、先にoauth2-client.xml
を読み込むようにする。
9.9.2.2.2.3. 例外ハンドリングの除外設定¶
ClientAuthorizationRequiredException
を発生させ、OAuth2AuthorizationRequestRedirectFilter
でハンドリングすることで認可サーバが提供するリソースオーナの認可を取得するためのページへリダイレクトさせている。OAuth2AuthorizationRequestRedirectFilter
はSecurity Filterとして提供されており、ブランクプロジェクトで予め設定しているSystemExceptionResolver
が先にClientAuthorizationRequiredException
をハンドリングしてしまうと期待した動作にならない。そのため、spring-mvc.xml
の設定を変更し、SystemExceptionResolver
がClientAuthorizationRequiredException
をハンドリングしないようにする必要がある。SystemExceptionResolver
の詳しい解説については例外ハンドリングを参照されたい。spring-mvc.xml
<bean class="org.terasoluna.gfw.web.exception.SystemExceptionResolver"> <!-- omitted --> <property name="excludedExceptions"> <array> <!-- (1) --> <value>org.springframework.security.oauth2.client.ClientAuthorizationRequiredException</value> </array> </property> </bean>
項番
説明
(1)ClientAuthorizationRequiredException
をSystemExceptionResolver
のハンドリング対象から除外する。
9.9.2.2.2.4. クライアントの登録¶
ClientRegistration
を使用し、認可サーバに登録されたクライアントを定義する。ClientRegistrationRepository
に保持される。oauth2-client.xml
<sec:client-registrations> <sec:client-registration registration-id="okta-client-id" client-id="readClient" client-secret="okta-client-secret" authorization-grant-type="authorization_code" redirect-uri="{baseUrl}/authorized/okta" scope="READ,CREATE" provider-id="okta" /> <!-- (1)~(7) --> <sec:provider provider-id="okta" authorization-uri="https://dev-1234.oktapreview.com/oauth2/v1/authorize" token-uri="https://dev-1234.oktapreview.com/oauth2/v1/token" /> <!-- (8)~(9) --> </sec:client-registrations>
項番
説明
(1)registration-id
属性に、ClientRegistrationを一意に識別するBean IDを設定する。(2)client-id
属性に、認可サーバに登録されたクライアントIDを設定する。(3)client-secret
属性に、認可サーバに登録されたクライアントの認可に用いるパスワードを設定する。(4)authorization-grant-type
属性に、グラントタイプを設定する。認可コードグラントの場合authorization_code
を指定する。(5)redirect-uri
属性に、認可サーバが認可コード発行後にユーザエージェントからクライアントへリダイレクトさせるためのリダイレクトURIを設定する。このURIへリダイレクト後、Spring SecurityはClientAuthorizationRequiredException
を発生させたリクエストの再処理を行う。リダイレクトURIは認可サーバに登録されているリダイレクトURIと一致させる必要がある。詳しくはRFC6749#section-3.1.2を参照されたい。Note
{baseUrl}
はURIテンプレート変数であり、{baseScheme}://{baseHost}{basePort}{basePath}
のことを指す。URIテンプレートを使用することで、展開時に
X-Forwarded-*
ヘッダが使用される。これにより、User agentとクライアントがProxy Serverを経由して通信している場合であっても、クライアント側のURIを取得することが可能となる。詳しくは、Initiating the Authorization Requestを参照されたい。
Note
元のリクエストが再処理されるのは、CSRF対策が無効またはGETメソッドで処理された場合であり、CSRF対策が有効(ブランクのデフォルト設定)かつGETメソッド以外で処理された場合は、リダイレクトURIにリダイレクトされた後元のリクエストが再処理されずにリダイレクトURIでそのまま処理される。
リダイレクトURIに対応したControllerを作成する場合、そのままでは元のリクエスト情報は取得できない。必要に応じて元のリクエストをセッションに保存する等の対処が必要があるため、注意すること。本ガイドラインでは、具体的な対処方法の説明については割愛する。
CSRF対策を無効化する場合、URLパターンを設けるなどセキュリティリスクが小さくなるように実装すること。なお、元のリクエストパスにはGETメソッドでリダイレクトするため、ControllerはGETメソッドを許容するようにする必要がある。
CSRF対策についてはCSRF対策を参照されたい。
(6)scope
属性には、認可リクエストとしてクライアントにリクエストするスコープをカンマ区切りで設定する。Note
問い合わせの対象となるスコープは、認可サーバに事前に登録されているスコープと、クライアントが認可リクエスト時にリクエストパラメータで指定したスコープの積となる。
例として、認可サーバでREADとCREATEとDELETEのスコープが割り当てられているクライアントに対してREADとCREATEのスコープをリクエストパラメータで指定した場合は、(READ,CREATE,DELETE)と(READ,CREATE)の積であるスコープ(READ,CREATE)が割り当てられる。
認可サーバでクライアントに割り当てられていないスコープをリクエストパラメータで指定した場合はエラーとなり、アクセスが拒否される。
(7)provider-id
属性には、エンドポイントを設定したプロバイダのIDを設定する。(8)authorization-uri
属性には、認可サーバーの認可エンドポイントURIを設定する。クライアントが認可前の場合は当認可エンドポイントURIに対しリクエストを送信する。認可エンドポイントURIは使用する認可サーバにより異なるため、認可サーバのアーキテクチャ仕様を確認されたい。(9)token-uri
属性には、認可サーバーのトークンエンドポイントURIを設定する。アクセストークンの取得やアクセストークンのリフレッシュを行う際に当トークンエンドポイントURIに対しリクエストを送信する。トークンエンドポイントURIは使用する認可サーバにより異なるため、認可サーバのアーキテクチャ仕様を確認されたい。
9.9.2.2.2.6. アクセストークンの取得¶
認可サーバへのアクセスはOAuth2AuthorizedClientManager
及びSecurity Filterにより隠蔽されるため、認可サーバへのアクセスについて意識する必要はない。アクセストークンを取得するにはOAuth2AuthorizedClientManager
で管理しているOAuth2AuthorizedClient
を取得し、OAuth2AuthorizedClient
からアクセストークンを取得するだけでよい。
以下にServiceクラスの実装例を示す。
OAuth2TokenServiceImpl.java
@Service public class OAuth2TokenServiceImpl implements OAuth2TokenService { @Inject OAuth2AuthorizedClientManager authorizedClientManager; // omitted @Override public OAuth2AccessToken getToken(String registrationId) { Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); // (1) OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest .withClientRegistrationId(registrationId).principal( authentication).build(); OAuth2AuthorizedClient authorizedClient = null; try { // (2) authorizedClient = this.authorizedClientManager.authorize( authorizeRequest); } catch (OAuth2AuthorizationException e) { if ("invalid_grant".equals(e.getError().getErrorCode())) { // (3) LOGGER.warn("refresh token expired. registrationId = {}", registrationId); throw new ClientAuthorizationRequiredException(registrationId); } throw e; } // (4) OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); return accessToken; } }
項番
説明
(1)registrationId
によって識別されたOAuth2AuthorizedClient
を認可するためのリクエストを作成する。(2)registrationId
によって識別されたOAuth2AuthorizedClient
の認可(または再認可)を行う。OAuth2AuthorizedClient
が認可されていない場合は、AuthorizationCodeOAuth2AuthorizedClientProvider
がClientAuthorizationRequiredException
をスローし、Security Filterで捕捉することで認可処理を実行する。また、OAuth2AuthorizedClient
が認可済みの状態でアクセストークンの有効期限が切れている場合は、リフレッシュトークンによりアクセストークンがリフレッシュされる。Note
アクセストークンのリフレッシュが必要と判断される時間は、デフォルトの設定ではアクセストークンの有効期限切れの60秒前となっている。
(3)有効期限切れのリフレッシュトークンをリクエストした場合は、OAuth2AuthorizationException
がスローされ例外コードにinvalid_grant
が設定される。ここでは、リフレッシュトークンの有効期限が切れていた場合に処理を継続させるため、(2)と同様にClientAuthorizationRequiredException
をスローしOAuth2AuthorizedClient
の認可処理を行っている。当処理は一例であるため、有効期限が切れた際にそのままエラー画面に遷移させたい場合など、要件に合わせ適切にエラーハンドリングを実施されたい。Note
invalid_grant
は、RFC6749#section-5.2に記載されている通りリフレッシュトークンの有効期限切れ以外の場合にもスローされる。そのため、短時間で該当箇所のログが大量に出力されるような場合は不正なアクティビティの可能性がある点に注意されたい。(4)認可済みのOAuth2AuthorizedClient
からアクセストークンを取得する。
9.9.2.2.2.7. アクセストークンの設定¶
Authorization: Bearer
ヘッダにアクセストークンを設定する必要がある。RestTemplateAccessTokenInterceptor
public class RestTemplateAccessTokenInterceptor implements ClientHttpRequestInterceptor { // (1) private String registrationId; @Inject OAuth2TokenService oAuth2TokenService; @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { OAuth2AccessToken accessToken = this.oAuth2TokenService.getToken( this.registrationId); // (2) request.getHeaders().setBearerAuth(accessToken.getTokenValue()); // (3) return execution.execute(request, body); } public void setRegistrationId(String registrationId) { this.registrationId = registrationId; } }
項番
説明
(1)ClientHttpRequestInterceptor
を実装し、ClientHttpRequestに対するInterceptorを作成する。(2)アクセストークンを取得する。アクセストークンの取得方法はアクセストークンの取得を参照されたい。(3)アクセストークンでの認証を行うために、Authorization: Bearer
ヘッダに(2)で取得したアクセストークンを設定する。Authorization: Bearer
ヘッダについてはRFC6750を参照されたい。
applicationContext.xml
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate"> <!-- omitted --> <property name="interceptors"> <!-- (1) --> <bean class="com.example.todo.app.interceptor.RestTemplateAccessTokenInterceptor"> <property name="registrationId" value="${registrationId}" /> </bean> </property> </bean>
項番
説明
(1)RestTemplateAccessTokenInterceptor
をInterceptorとして設定する。
9.9.2.2.2.8. リソースサーバへのアクセス¶
RestTemplateを用いてリソースサーバへアクセスする方法を説明する。
認可サーバへのアクセスはOAuth2AuthorizedClientManager
及びSecurity Filterにより隠蔽されるため、開発の際には認可サーバを意識する必要がなく、リソースサーバが提供するREST APIに対して行う処理を、通常のREST APIへのアクセスと同様に記述する。
以下にServiceクラスの実装例を示す。
TodoServiceImpl.java
@Service @Transactional public class TodoServiceImpl implements TodoService { @Value("${resource.serverUrl}/api/v1/todos/") private String resourceServerUri; @Inject RestTemplate restTemplate; // (1) @Override @Transactional(readOnly = true) public Todo findOne(@NotEmpty String todoId) { // @formatter:off RequestEntity<Void> requestEntity = RequestEntity .get(this.resourceServerUri + todoId) // (2) .build(); // @formatter:on ResponseEntity<Todo> responseEntity = getRestTemplate(registrationId) .exchange(requestEntity, Todo.class); // (3) Todo todo = responseEntity.getBody(); return todo; } // omitted }
項番
説明
(1)RestTemplate
をインジェクションする。(2)ここではGET
メソッドを指定している。リソースサーバが提供するREST APIに合わせ適切に設定されたい。なお、アクセストークンの設定については、アクセストークンの設定を参照されたい。(3)URLで指定したリソースサーバに対しRESTでアクセスし、結果をTodo.class
として受け取る。