9.9. OAuth

Caution

本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。

目次

9.9.1. Overview

本節では、OAuth 2.0の概要とSpringプロジェクトの一つである Spring Security OAuthを使用してOAuth 2.0の仕様に沿った認可制御機能を実装する方法について説明する。

Tip

Spring Security OAuth のリファレンス

Spring Security OAuthは、本ガイドラインで紹介していない機能も提供している。 Spring Security OAuthについて詳しく知りたい場合は、OAuth 2 Developers Guideを参照されたい。


9.9.1.1. OAuth 2.0とは

OAuth 2.0とは、サードパーティー製アプリケーションがHTTPサービスを利用する際に、 サーバ上の保護されたリソースに対するアクセス範囲の指定を可能にするための認可フレームワークのことである。

OAuth 2.0はRFCとして仕様化されており、関連する複数の技術仕様から構成されている。

以下にOAuth 2.0の主要な仕様を示す。

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
トークンの無効化を行う追加エンドポイントに関する技術仕様。

従来のクライアントサーバ型の認証モデルでは、サードパーティー製アプリケーションはサーバ上の 保護されたリソースにアクセスするために、ユーザーの認証情報(ユーザー名とパスワードなど)を利用して認証を行う。

つまり、ユーザーは、サードパーティー製アプリケーションにリソースへのアクセス権を与えるために 自身の認証情報をサードパーティと共有する必要があるが、 これはサードパーティー製アプリケーションに不具合や悪意のある操作などが存在した場合に、 ユーザの意図しないアクセスや情報漏洩等のリスクにつながる。

../_images/OAuth_TraditionalAuthenticationModel.png

これに対し、OAuth 2.0では認証はユーザが直接行い、サードパーティー製アプリケーションには 「アクセストークン」と呼ばれる認証済みリクエストを行うための情報を払い出すことで、 サードパーティーに認証情報を共有することなくリソースへアクセスすることが可能となる。

また、アクセストークン発行時にリソースに対するアクセス範囲(スコープ)を指定可能とすることで 従来のクライアントサーバ型の認証モデルと比較してより柔軟なアクセス制御を実現している。

../_images/OAuth_OAuthAuthenticationModel.png

9.9.1.2. OAuth 2.0のアーキテクチャ

ここではOAuth 2.0が定義するロール、スコープ、認可グラント、及びプロトコルフローについて説明する。 OAuth 2.0ではスコープや認可グラントという概念を定義しており、これらの概念を使用して認可の仕様を定めている。


9.9.1.2.1. ロール

OAuth 2.0ではロールとして以下の4つを定義している。

OAuth 2.0におけるロール
ロール名 説明
リソースオーナ
保護されたリソースへのアクセスを許可するロール。人(エンドユーザ)など。
リソースサーバ
保護されたリソースを提供するロール。Webサーバ。
認可サーバ
リソースオーナの認証と、アクセストークン(クライアントがリソースサーバにアクセスするときに必要な情報)の発行を行うロール。Webサーバ。
クライアント
リソースオーナの認可を得て、リソースオーナの代理として保護されたリソースに対してリクエストを行うロール。Webアプリケーションなど。クライアントの情報は事前に認可サーバに登録され、認可サーバ内で一意な情報であるクライアントIDにより管理される。
OAuth 2.0ではクライアントクレデンシャル(クライアントの認証情報)の機密性を維持できる能力に基づき、クライアントタイプとして以下の2つを定義している。
(1) コンフィデンシャル
クライアントクレデンシャルの機密性を維持することができるクライアント。
(2) パブリック
リソースオーナのデバイス上で実行されるクライアントのように、クライアントクレデンシャルの機密性を維持することができず、かつ他の手段を用いたセキュアなクライアント認証が行えないクライアント。

また、OAuth 2.0ではクライアントとして以下のような例を考慮して設計されている
(1) Webアプリケーション(web application)
Webサーバー上で実行されるクライアント(コンフィデンシャル)。
(2) ユーザエージェントベースアプリケーション(user-agent-based application)
クライアントコードがWebサーバーからダウンロードされリソースオーナのユーザエージェント内で実行されるクライアント(パブリック)。Javascriptアプリケーションなど。
(3) ネイティブアプリケーション(native application)
リソースオーナのデバイス上にインストールされ実行されるクライアント(パブリック)。

9.9.1.2.2. スコープ

OAuth 2.0では保護されたリソースに対するアクセスを制御する方法としてスコープという概念を使用している。

認可サーバはクライアントからの要求に対し、認可サーバのポリシーまたはリソースオーナの 指示に基づいてアクセストークンにスコープを含め、保護されたリソースに対する アクセス権(読み込み権限、書き込み権限など)を指定することが出来る。


9.9.1.2.3. プロトコルフロー

OAuth 2.0では、以下のような流れでリソースへのアクセスを行う。

../_images/OAuth_ProtocolFlow.png
OAuth 2.0のプロトコルフロー
項番 説明
(1)
クライアントはリソースオーナに対して認可を要求する。上の図ではリソースオーナに
直接要求を行ってるが、認可サーバを経由して行うほうが望ましい。
後述するグラントタイプの中では認可コードグラントとインプリシットグラントが
認可サーバを経由してリソースオーナに要求を行うフローになっている。
(2)
クライアントはリソースオーナからの認可を表すクレデンシャルとして認可グラント(後述)を受け取る。
(3)
クライアントは、認可サーバーに対して自身の認証情報とリソースオーナが与えた認可グラントを提示することで、アクセス
トークンを要求する。
(4)
認可サーバはクライアントを認証し、認可グラントの正当性を確認する。認可グラントが正当な場合、
アクセストークンを発行する。
(5)
クライアントはリソースサーバの保護されたリソースへリクエストを行い、発行されたアクセス
トークンにより認証する。
(6)
リソースサーバはアクセストークンの正当性を確認し、正当な場合、リクエストを受け入れる。

Note

OAuth 1.0で不評だった署名とトークン交換の複雑な仕組みを簡略化するために、OAuth 2.0ではアクセストークンを扱うリクエストはHTTPS通信で行うことを必須としている。 (HTTPS通信を使用することでアクセストークンの盗聴を防止する)


9.9.1.2.4. 認可グラント

認可グラントは、リソースオーナからの認可を表し、クライアントがアクセストークンを取得する際に用いられる。 OAuth 2.0では、グラントタイプとして以下の4つを定義しているが、クレデンシャル項目を追加するなどの独自拡張を行うこともできる。

クライアントは4つのグラントタイプのいずれかにより、認可サーバへアクセストークンを要求し、取得したアクセストークンでリソースサーバにアクセスする。 認可サーバはサポートするグラントタイプを必ず1つ以上定義しており、その中から使用するグラントタイプをクライアントからの認可リクエストによって決定する。

OAuth 2.0における認可グラント
グラントタイプ 説明
認可コードグラント
認可コードグラントのフローでは、認可サーバがクライアントとリソースオーナの仲介となって認可コードをクライアントへ発行し、クライアントが認可コードを認可サーバに渡すことでアクセストークンを発行する。
認可サーバが発行した認可コードを使用してアクセストークンを発行するため、クライアントへリソースオーナのクレデンシャルを共有する必要がない。
認可コードグラントはWebアプリケーションのように、コンフィデンシャルなクライアントがOAuth 2.0を利用する際に使用する。
インプリシットグラント
インプリシットグラントのフローでは、認可コードグラントと同様に認可サーバが仲介するが、認可コードの代わりに直接アクセストークンを発行する。
アクセストークンはURL中にエンコードされるため、リソースオーナや同一デバイス上の他のアプリケーションに漏えいする可能性があるほか、 クライアントの認証を行わないことから、他のクライアントに対して発行されたアクセストークンを不正に用いた成りすまし攻撃のリスクがある。
インプリシットグラントはJavascriptで実装されたクライアントなどの、クライアントタイプがパブリックである場合のみ使用すること。
リソースオーナパスワードクレデンシャルグラント
リソースオーナパスワードクレデンシャルグラントのフローでは、クライアントがリソースオーナの認証情報を認可グラントとして使用して、直接アクセストークンを発行する。
クライアントへリソースオーナのクレデンシャルを共有する必要があるため、クライアントの信頼性が低い場合、クレデンシャルの不正利用や漏洩のリスクがある。
リソースオーナパスワードクレデンシャルグラントはリソースオーナとクライアントの間で高い信頼があり、かつ他のグラントタイプが利用できない場合にのみ使用すること。
クライアントクレデンシャルグラント
クライアントクレデンシャルグラントのフローでは、クライアントの認証情報を認可グラントとして使用して、直接アクセストークンを発行する。
クライアントがリソースオーナであるような場合に使用する。

認可コードグラントのフローを以下に示す。

../_images/OAuth_AuthorizationCodeGrant.png
認可コードグラントフロー
項番 説明
(1)
リソースオーナは、ユーザエージェントを介してクライアントが提供するリソースサーバの保護されたリソースが必要なページにアクセスする。
クライアントはリソースオーナから認可の取得を行うために、リソースオーナのユーザエージェントを認可サーバの認可エンドポイントにアクセスさせる。
このとき、クライアントは自身を識別するためのクライアントIDと、オプションとしてリソースに要求するスコープ、認可サーバが認可処理後にユーザエージェントを戻すリダイレクトURL、stateをリクエストパラメータに含める。
stateはユーザエージェントに紐付くランダムな値であり、一連のフローが同じユーザエージェントで実行されたことを保証するために利用される(CSRF対策)。
(2)
ユーザエージェントは、クライアントに指示された認可サーバの認可エンドポイントにアクセスする。
認可サーバはユーザエージェント経由でリソースオーナを認証し、リクエストパラメータのクライアントID、スコープ、リダイレクトURLを元に、自身に登録済みのクライアント情報と比較しパラメータの正当性確認を行う。
確認完了後、アクセス要求の許可/拒否をリソースオーナにたずねる。
(3)
リソースオーナはアクセス要求の許可/拒否を認可サーバに送信する。
リソースオーナがアクセスを許可した場合、認可サーバの認可エンドポイントは認可コードを発行し、リクエストパラメータのリダイレクトURLを用いてユーザエージェントをクライアントにリダイレクトさせる指示を出す。
その際、リダイレクトURLのリクエストパラメータとして、認可コードをリダイレクトURLに付与する。
(4)
ユーザエージェントは認可コードが付与されたリダイレクトURLにアクセスする。
クライアントの処理が完了するとリソースオーナにレスポンスを返却する。
(5)
クライアントはアクセストークンを要求するために、認可コードを認可サーバのトークンエンドポイントに送信する。
認可サーバのトークンエンドポイントはクライアントの認証と認可コードの正当性の検証を行い、正当である場合アクセストークンと任意でリフレッシュトークンを発行する。
リフレッシュトークンはアクセストークンが無効化された、または期限切れの際に新しいアクセストークンを発行するために使用される。
(6)
クライアントは取得したアクセストークンでリソースサーバにアクセスする。
リソースサーバはアクセストークンの正当性を確認し、正当な場合、リクエストを処理してレスポンスをクライアントに返却する。

インプリシットグラントのフローを以下に示す。

../_images/OAuth_ImplicitGrant.png
インプリシットグラントフロー
項番 説明
(1)
リソースオーナは、ユーザエージェントを介してクライアントが提供するリソースサーバの保護されたリソースが必要なページにアクセスする。
クライアントはリソースオーナから認可の取得とアクセストークンの発行を行うために、リソースオーナのユーザエージェントを認可サーバの認可エンドポイントにアクセスさせる。
このとき、クライアントは自身を識別するためのクライアントIDと、オプションとしてリソースに要求するスコープ、認可サーバが認可処理後にユーザエージェントを戻すリダイレクトURL、stateをリクエストパラメータに含める。
stateはユーザエージェントに紐付くランダムな値であり、一連のフローが同じユーザエージェントで実行されたことを保証するために利用される(CSRF対策)。
(2)
ユーザエージェントは、クライアントに指示された認可サーバの認可エンドポイントにアクセスする。
認可サーバはユーザエージェント経由でリソースオーナを認証し、リクエストパラメータのクライアントID、スコープ、リダイレクトURLを元に、自身に登録済みのクライアント情報と比較しパラメータの正当性確認を行う。
確認完了後、アクセス要求の許可/拒否をリソースオーナにたずねる。
(3)
リソースオーナはアクセス要求の許可/拒否を認可サーバに送信する。
リソースオーナがアクセスを許可した場合、認可サーバの認可エンドポイントはリクエストパラメータのリダイレクトURLを用いてユーザエージェントをクライアントリソースにリダイレクトさせる指示を出す。その際、アクセストークンをリダイレクトURLのURLフラグメントに付与する。
(4)
ユーザエージェントはリダイレクトの指示に従い、クライアントリソースにリクエストを送信する。このとき、URLフラグメントの情報をローカルで保持し、リダイレクトの際にはURLフラグメントを送信しない。
クライアントリソースにアクセスすると、Webページ(通常は埋め込みスクリプトを含むHTMLドキュメント)が返却される。
ユーザエージェントはWebページに含まれるスクリプトを実行し、ローカルで保持していたURLフラグメントからアクセストークンを抽出する。
(5)
ユーザエージェントはアクセストークンをクライアントに渡す。
クライアントの処理が完了するとリソースオーナにレスポンスを返却する。
(6)
クライアントは取得したアクセストークンでリソースサーバにアクセスする。
リソースサーバはアクセストークンの正当性を確認し、正当な場合、リクエストを処理してレスポンスをクライアントに返却する。

リソースオーナパスワードクレデンシャルグラントのフローを以下に示す。

../_images/OAuth_ResourceOwnerPasswordCredentialsGrant.png
リソースオーナパスワードクレデンシャルグラントフロー
項番 説明
(1)
リソースオーナがクライアントにクレデンシャル(ユーザー名、パスワード)を提供する。
(2)
クライアントはアクセストークンを要求するために、認可サーバのトークンエンドポイントにアクセスする。
このとき、クライアントはリソースオーナから指定されたクレデンシャルとリソースに要求するスコープをリクエストパラメータに含める。
(3)
認可サーバのトークンエンドポイントはクライアントを認証し、リソースオーナのクレデンシャルを検証する。正当である場合アクセストークンを発行する。

クライアントクレデンシャルグラントのフローを以下に示す。

../_images/OAuth_ClientCredentialsGrant.png
クライアントクレデンシャルグラントフロー
項番 説明
(1)
クライアントはアクセストークンを要求するために、認可サーバのトークンエンドポイントにアクセスする。
このとき、クライアントはクライアント自身のクレデンシャルを含めてアクセストークンを要求する。
(2)
認可サーバのトークンエンドポイントはクライアントを認証し、認証に成功した場合アクセストークンを発行する。

9.9.1.2.5. アクセストークンのライフサイクル

アクセストークンはクライアントが提示する認可グラントの正当性を認可サーバが確認することで発行される。 発行されたアクセストークンは、認可サーバのポリシーまたはリソースオーナの指示に基づいたスコープが与えられ、保護されたリソースに対するアクセス権を得る。 アクセストークンは発行時に有効期限が設定され、有効期限切れとなると保護されたリソースに対するアクセス権を失効される。

アクセストークンの発行から失効までの流れは以下のようになる。

../_images/OAuth_LifeCycleOfAccessToken.png
アクセストークンの発行から失効までのフロー
項番 説明
(1)
クライアントが認可グラントを提示し、アクセストークンを要求する。
(2)
認可サーバはクライアントが提示した認可グラントを確認し、アクセストークンを発行する。
(3)
クライアントはアクセストークンを提示し、リソースサーバの保護されたリソースを要求する。
(4)
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、正当であればリソースサーバの保護されたリソースに対して処理を行う。
(5)
クライアントはアクセストークン(有効期限切れ)を提示し、リソースサーバの保護されたリソースを要求する。
(6)
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、アクセストークンの有効期限が切れている場合はエラーを返却する。
アクセストークンが有効期限切れとなると保護されたリソースに対するアクセス権を失効されるが、アクセストークンが有効期限切れとなる前にアクセストークンを無効化し保護されたリソースに対するアクセス権を失効させることも可能である。
アクセストークンが有効期限切れとなる前に無効化する場合、クライアントより認可サーバにトークンの取り消し依頼を行う。無効化されたアクセストークンは保護されたリソースに対するアクセス権を失効される。

アクセストークンが有効期限切れとなった場合、クライアントがアクセストークンを再取得するためには認可サーバへ認可グラントの再提示を行い、認可サーバによる正当性の再確認が必要になる。 そのため、アクセストークンの有効期限を短く設定した場合はユーザビリティが下がってしまう。一方で、アクセストークンの有効期限を長く設定した場合はアクセストークンの漏洩、漏洩時に悪用されるリスクが高まってしまう。
ユーザビリティを下げずに漏洩、漏洩時のリスクを下げるためにはリフレッシュトークンが用いられる。 リフレッシュトークンはアクセストークンが無効化されたあるいは期限切れの際、認可グラントの再提示を行うことなく新しいアクセストークンを取得するために利用される。 リフレッシュトークンも発行時に有効期限が設定され、リフレッシュトークンが有効期限切れとなった場合はアクセストークンの再発行ができなくなる。
アクセストークンの有効期限に短い期間を設定し、リフレッシュトークンの有効期限に長い期間を設定することで、短いサイクルでアクセストークンが再発行されユーザビリティを保ちつつアクセストークン漏洩及び漏洩時の悪用のリスクも抑えることができる。
リフレッシュトークンの発行はオプションであり、認可サーバーの判断に委ねられる。

リフレッシュトークンによるアクセストークンの再発行の流れは以下のようになる。

../_images/OAuth_LifeCycleOfAccessTokenWithRefreshToken.png
アクセストークンの発行から再発行までのフロー
項番 説明
(1)
クライアントが認可グラントを提示し、アクセストークンを要求する。
(2)
認可サーバはクライアントが提示した認可グラントを確認し、アクセストークンとリフレッシュトークンを発行する。
(3)
クライアントはアクセストークンを提示し、リソースサーバの保護されたリソースを要求する。
(4)
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、正当であればリソースサーバの保護されたリソースに対して処理を行う。
(5)
クライアントはアクセストークン(有効期限切れ)を提示し、リソースサーバの保護されたリソースを要求する。
(6)
リソースサーバはクライアントが提示したアクセストークンの正当性を検証し、アクセストークンの有効期限が切れている場合はエラーを返却する。
(7)
リソースサーバよりアクセストークンの有効期限切れエラーが返却された場合、クライアントはリフレッシュトークンを提示することで新しいアクセストークンを要求する。
(8)
認可サーバはクライアントが提示しリフレッシュトークンの正当性を検証し、正当であればアクセストークンとオプションでリフレッシュトークンを発行する。

9.9.1.3. Spring Security OAuthのアーキテクチャ

Spring Security OAuthは、OAuth 2.0で定義されているロールのうち、認可サーバ、リソースサーバ、クライアントの3つのロールをSpringアプリケーションとして構築する際に必要となる機能を提供するライブラリである。 Spring Security OAuthは、Spring Framework(Spring MVC)やSpring Securityが提供する機能と連携して動作する仕組みになっており、Spring Security OAuthが提供するデフォルト実装を適切にコンフィギュレーション(Bean定義)するだけで、認可サーバ、リソースサーバ、クライアントを構築することができる。 また、Spring FrameworkやSpring Securityと同様に数多くの拡張ポイントが用意されており、Spring Security OAuthが提供するするデフォルト実装で実現できない要件を組み込むことができるようになっている。

なお、各ロール間のリクエストに対する認証・認可にはSpring Securityが提供する機能を利用するため、そちらの詳細は認証及び 認可を参照されたい。

Spring Security OAuthを使用して認可サーバ、リソースサーバ、クライアントを構築した場合、以下のような流れで処理が行われる。

../_images/OAuth_OAuth2Architecture.png
Spring Security OAuthのフロー
項番 説明
(1)
リソースオーナはユーザエージェントを介してクライアントへアクセスする。
クライアントはサービスよりOAuth2RestTemplateの呼び出しを行う。
認可エンドポイントにアクセスする認可グラントの場合、ユーザエージェントへ認可サーバの認可エンドポイントへリダイレクトさせるよう指示する。
(2)
ユーザエージェントは認可サーバの認可エンドポイントであるAuthorizationEndpointへアクセスする。
AuthorizationEndpointはリソースオーナへ認可を問い合わせる画面を表示させる。
リソースオーナはクライアントにスコープに対して認可を行い、AuthorizationEndpointへアクセスする。
AuthorizationEndpointは、認可グラントが認可コードグラントである場合は認可コードを、インプリシットグラントである場合はアクセストークンを発行する。
発行した認可コードまたはアクセストークンは、リダイレクトを使用してユーザエージェント経由でクライアントに渡される。
(3)
クライアントはOAuth2RestTemplateより認可サーバのトークンエンドポイントであるTokenEndpointへアクセスする。
TokenEndpointAuthorizationServerTokenServiceを呼び出しアクセストークンを発行する。発行したアクセストークンはクライアントへの応答として渡される。
認可グラントが認可コードグラントである場合は認可コードを認可サーバに提示する。アクセストークン発行前にTokenEndpointで認可コードの正当性を検証される。
(4)
クライアントは(2)または(3)で取得したアクセストークンを指定して OAuth2RestTemplateよりリソースサーバにアクセスする。
リソースサーバはOAuth2AuthenticationManagerを呼び出し、ResourceServerTokenServicesを介してアクセストークンに紐づく認証情報を取得する。また、認証情報の取得時にアクセストークンを検証する。
アクセストークンの検証に成功した場合、クライアントからのリクエストに応じたリソースを返却する。

Note

前述のとおり、OAuth 2.0では各エンドポイントにおいてHTTPS通信の使用を前提としているが、HTTPS通信を使用するのがSSLアクセラレータやWebサーバまでの場合や、 ロードバランサを使用して複数のAPサーバに分散させる場合がある。 リソースオーナによって認可された後にクライアントに認可コードまたは、アクセストークンを連携するためのリダイレクトURLを組み立てる際に、 SSLアクセラレータやWebサーバ、ロードバランサを指し示すリダイレクトURLを組み立てる必要がある。

Spring(Spring Security OAuth)では以下のいずれかのヘッダを使用してリダイレクト用のURLを組み立てる仕組みが提供されている。

  • Forwardedヘッダ
  • X-Forwarded-Hostヘッダ、X-Forwarded-Portヘッダ、X-Forwarded-Protoヘッダ

SSLアクセラレータやWebサーバ、ロードバランサ側で上記ヘッダを付与するように設定し、Spring(Spring Security OAuth)が正しいリダイレクトURLを生成できるようにする必要がある。 これを行わない場合、認可コードグラントやインプリシットグラントにおいて行うリクエストパラメータ(リダイレクトURL)の検証に失敗する可能性がある。

Tip

Spring Security OAuthが提供するエンドポイントはSpring MVCの機能を拡張して実現している。Spring Security OAuthが提供するエンドポイントには@FrameworkEndpointアノテーションがクラスに設定されている。 これは@Controllerアノテーションで開発者がコンポーネントとして登録したクラスと競合させないためである。 また、@FrameworkEndpointアノテーションでコンポーネントとして登録されたエンドポイントは、RequestMappingHandlerMappingの拡張クラスであるFrameworkEndpointHandlerMappingがエンドポイントの@RequestMappingアノテーションを読み取り、URLと合致する@FrameworkEndpointのメソッドを、Handlerクラスとして扱っている。


9.9.1.3.1. 認可サーバ

認可サーバは、リソースオーナの認証と、認証後のリソースオーナからの認可の取得、およびアクセストークンの発行を行う。

OAuth 2.0ではアクセス範囲を指定する表現としてスコープという概念をサポートしている。 クライアントは認可リクエスト送信時にスコープを指定し、リソースオーナが指定されたスコープを認可するか、認可サーバに事前に登録されたスコープと一致する場合に、認可サーバでクライアントに対してそのスコープを認可する。 クライアントに対してのスコープの認可と 認可の節で説明しているSpring Securiyのロールによる認可は併用可能である。

Spring Security OAuthでは、AuthorizationEndpointにおいてリソースオーナからの認可の取得機能を提供し、 AuthorizationEndpointまたはTokenEndpointにおいてクライアントに対してのアクセストークンの発行機能を提供している。 AuthorizationEndpointTokenEndpointはアクセストークンの発行をAuthorizationServerTokenServiceの デフォルト実装であるDefaultTokenServicesによって行っている。 アクセストークンの発行の際には、ClientDetailsServiceを介して認可サーバに登録済みのクライアント情報を取得し、アクセストークン発行の可否の検証に使用している。

なお、OAuth 2.0の仕様では認可サーバを利用するクライアントの登録手順については定められておらず、Spring Security OAuthにおいても クライアント登録手続きはサポートされていない。 そのため、アプリケーションでクライアント登録のインターフェイスを提供したい場合には、独自に実装する必要がある。

リソースオーナの認証にはSpring Securityの認証機能を利用する。 詳細については 認証を参照のこと。

以下に認可サーバの構成を示す。

../_images/OAuth_AutohrizationServerAuthArchitecture.png
認可サーバの動き(認可エンドポイントアクセス時)
項番 説明
(1)
ユーザエージェントが認可サーバの認可エンドポイント(/oauth/authorize)にアクセスすることでAuthorizationEndpointの処理が実行される。
(2)
ClientDetailsServiceのメソッドを呼び出し、事前に登録されているクライアント情報を取得後、リクエストパラメータを検証する。
(3)
UserApprovalHandlerのメソッドを呼び出し、クライアントへスコープに対する認可が登録されているかチェックする。
認可が未登録である場合、ユーザエージェント経由でリソースオーナへ認可を問い合わせる画面(/oauth/confirm_access)を表示させる。
このとき、問い合わせの対象となるスコープはリクエストパラメータと事前に登録されているクライント情報の積をとり、Spring MVCの@SessionAttributesを利用して連携される。
(4)
UserApprovalHandlerの実装であるApprovalStoreUserApprovalHandlerではApprovalStoreにより認可の状態を管理する。
リソースオーナにより認可が行われた場合、ApprovalStoreのメソッドを呼び出し、指定された情報を登録する。

Note

問い合わせの対象となるスコープは前述のとおり、認可サーバに事前に登録されているスコープと、クライアントが認可リクエスト時にリクエストパラメータで指定したスコープの積となる。 例として、認可サーバでREADとCREATEとDELETEのスコープが割り当てられているクライアントに対して、READとCREATEのスコープをリクエストパラメータで指定した場合は(READ,CREATE,DELETE)と(READ,CREATE)の積である、スコープREAD,CREATEが割り当てられる。 認可サーバでクライアントに割り当てられていないスコープをリクエストパラメータで指定した場合はエラーとなり、アクセスが拒否される。


../_images/OAuth_AutohrizationServerTokenArchitecture.png
認可サーバの動き(トークンエンドポイントアクセス時)
項番 説明
(1)
クライアントが認可サーバのトークンエンドポイント(/oauth/token)にアクセスすることでTokenEndpointの処理が実行される。
(2)
ClientDetailsServiceのメソッドを呼び出し、事前に登録されているクライアント情報を取得後、リクエストパラメータのスコープがクライアントに登録済みのものかチェックする。
(3)
スコープが登録済みのものであった場合、TokenGranterのメソッドを呼び出し、アクセストークンを発行する。
(4)
TokenGranterの実装であるAbstractTokenGranterではAuthorizationServerTokenServicesのメソッドを呼び出し、アクセストークンを発行する。
AbstractTokenGranterはグラントタイプ別に実装されているTokenGranterの基底クラスであり、実際の処理は各クラスに委譲される。
(5)
AuthorizationServerTokenServicesの実装であるDefaultTokenServicesではTokenStoreのメソッドを呼び出し、アクセストークンの状態を管理する。

9.9.1.3.2. リソースサーバ

リソースサーバは、クライアントからの保護されたリソースに対するアクセス要求を処理し、レスポンスを返す。 リソースサーバは、クライアントからのリクエストに付加されるアクセストークンについて、有効期限内であることを検証し、アクセストークンに紐づく認証情報を取得する。 認証情報の取得後は、要求されたリソースが当該アクセストークンのスコープ範囲内であることを検証する。 アクセストークン検証後の処理は、通常のアプリケーションと同様に実装を行うことができる。

Spring Security OAuthでは、アクセストークンの検証機能を、Spring Securityの認証・認可の枠組みを利用して実現している。 具体的には、ServletFilterにSpring Security OAuthが提供するOAuth2AuthenticationProcessingFilterを使用し、 AuthenticationEntryPointインタフェース としてOAuth2AuthenticationEntryPointを、AuthenticationManagerとしてOAuth2AuthenticationManagerをそれぞれ使用している。 Spring Securityの詳細については 認証を参照のこと。

以下にリソースサーバの構成を示す

../_images/OAuth_ResourceServerArchitecture.png
リソースサーバの動き
項番 説明
(1)
初めにクライアントがリソースサーバにアクセスするとOAuth2AuthenticationProcessingFilter
呼び出しが行われる。
OAuth2AuthenticationProcessingFilterではアクセストークンの抽出を行う。
(2)
アクセストークンを抽出後、AuthenticationManagerの実装であるOAuth2AuthenticationManagerにおいてResourceServerTokenServices
介してアクセストークンに紐づく認証情報を取得する。また、認証情報の取得時にアクセストークンを検証する。
アクセストークンに紐づく認証情報の取得方法には、認可サーバに対してHTTPによる問い合わせを行うほか、認可サーバと
TokenStoreを共用して取得を行うなどの方法がある。
どのようにして認証情報の取得を行うかについてはResourceServerTokenServicesの実装に依存する。
(3)
アクセストークンの検証に成功した場合、クライアントからのリクエストに応じたリソースを返却する。
(4)
認証エラー時に発生する例外は、OAuth2AuthenticationEntryPointを使用してハンドリングし、エラー応答を行う。
(5)
認可エラー時に発生する例外は、OAuth2AccessDeniedHandlerを使用してハンドリングし、エラー応答を行う。

9.9.1.3.3. クライアント

クライアントは、リソースオーナの認可とアクセストークンを取得し、リソースオーナの代理としてリソースサーバの保護されたリソースに対してアクセスを行う。 その際、リソースへのリクエストには認可サーバから発行されたアクセストークンを付加する。

Spring Security OAuthでは、クライアントの基本的な機能の実現方法としてRestOperationsのOAuth 2.0向けの実装であるOAuth2RestTemplateを提供している。

OAuth2RestTemplateでは、アクセストークンの発行やリフレッシュトークンを使用したアクセストークンの再発行、また、アクセストークンを使用したリソースサーバへのアクセスといった機能のほか、 サーブレットフィルタとしてOAuth2ClientContextFilterを利用することで、認可コードグラントなどで必要となる認可の機能を実現している。

また、OAuth2RestTemplateではOAuth2ProtectedResourceDetailsにて指定されたグラントタイプに沿って認可サーバより取得したアクセストークンをOAuth2ClientContextの実装であるDefaultOAuth2ClientContextに保持する。 DefaultOAuth2ClientContextはデフォルトではセッションスコープのBeanとして定義され、複数のリクエスト間でアクセストークンを共有をすることが可能となる。

リソースサーバへのアクセス機能の開発をする際は、RestOperationsの実装としてSpring Webが提供するRestTemplateの代わりにSpring Security OAuthが提供するOAuth2RestTemplateを使用する以外は、通常のRESTクライアントの開発と同様の実装をする。

以下に、クライアント機能としてOAuth2RestTemplateを使用した場合の構成を示す。

../_images/OAuth_ClientArchitecture.png
クライアントの動き
項番 説明
(1)
ユーザエージェントがクライアントのServiceの呼び出しが行われるようControllerへアクセスを行う。
リソースサーバへのアクセスを伴うアクセスに対しては、(5)で発生する可能性があるUserRedirectRequiredExceptionを捕捉するためのサーブレットフィルタ(OAuth2ClientContextFilter)を適用する。
このサーブレットフィルタを適用することで、UserRedirectRequiredExceptionが発生した際に、ユーザエージェントを認可サーバの認可エンドポイントへアクセスさせることができる。
(2)
ServiceよりOAuth2RestTemplateの呼び出しを行う。
(3)
リソースサーバへアクセスする前に、メンバとして保持しているDefaultOAuth2ClientContextよりアクセストークンを保持しているか確認を行う。
アクセストークンを保持しており、かつ有効期限内である場合、DefaultOAuth2ClientContextより取得したアクセストークンを指定してリソースサーバへのアクセスを行う。
(4)
初回アクセス時などでアクセストークンを保持していなかった場合、または有効期限が超過していた場合、AccessTokenProviderを呼び出しアクセストークンの取得を行う。
(5)
AccessTokenProviderでは、リソースの詳細情報としてOAuth2ProtectedResourceDetailsに定義しているグラントタイプに応じてアクセストークンの取得を行う。
認可コードグラント向けの実装であるAuthorizationCodeAccessTokenProviderでは、認可コードの取得が完了していない場合、UserRedirectRequiredExceptionをthrowする。
(6)
(3)または(5)で取得したアクセストークンを指定して、リソースサーバへのアクセスを行う。
アクセス時にアクセストークンの有効期限切れ(AccessTokenRequiredException)などのエラーが発生した場合、保持しているアクセストークンを初期化した後、再度(4)以降の処理を行う。

9.9.2. How to use

Spring Security OAuthを使用するために必要となるBean定義例や実装方法について説明する。


9.9.2.1. Spring Security OAuthのセットアップ

Spring Security OAuthが提供しているクラスを使用するために、Spring Security OAuthを依存ライブラリとして追加する。

<!-- (1) -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
項番 説明
(1)
Spring Security OAuthを使用するプロジェクトの pom.xml に、Spring Security OAuthを依存ライブラリとして追加する。
リソースサーバ、認可サーバ、クライアントを別プロジェクトとして作成する場合、それぞれについて記述を追加すること。

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。 上記の依存ライブラリはterasoluna-gfw-parentが利用しているSpring IO Platformで定義済みである。


9.9.2.2. アプリケーションの設定

Spring Security OAuthを使用するアプリケーションの設定について説明する。

認可グラント」にて示したとおり、OAuth 2.0ではグラントタイプにより認可サーバ、クライアント間のフローが異なる。 そのため、Spring Security OAuthを使用するアプリケーションでは、アプリケーションがサポートするグラントタイプに沿った設定を行う必要がある。 グラントタイプ別の設定内容については各ロールの実装を参照。


9.9.2.3. 認可サーバの実装

認可サーバの実装方法について説明する。

認可サーバでは、「リソースオーナからの認可の取得」「アクセストークンの発行」を行うためのエンドポイントをSpring Security OAuthの機能を使用して提供する。 なお、上記のエンドポイントにアクセスする場合はリソースオーナまたはクライアントの認証が必要であり、本ガイドラインではSpring Securityの認証・認可の仕組みを使用して実現する。

9.9.2.3.1. 設定ファイルの作成(認可サーバ)

認可サーバに関する定義を行うための設定ファイルとして oauth2-auth.xmlを作成する。
oauth2-auth.xmlでは、認可サーバの機能を提供するためのエンドポイントのBean定義およびそれらのエンドポイントに対するセキュリティ設定、認可サーバのサポートするグラントタイプの設定を行う。
  • oauth2-auth.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:oauth2="http://www.springframework.org/schema/security/oauth2"
       xsi:schemaLocation="http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security.xsd
           http://www.springframework.org/schema/security/oauth2
           http://www.springframework.org/schema/security/spring-security-oauth2.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">


</beans>

次に、作成したoauth2-auth.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-auth.xml  <!-- (1) -->
        classpath*:META-INF/spring/spring-security.xml
    </param-value>
</context-param>
項番 説明
(1)
OAuth 2.0用のBean定義ファイルの指定を行う。oauth2-auth.xmlで設定したアクセス制御の対象のURLがspring-security.xmlで設定したアクセス制御の対象のURLに含まれる場合を考慮し、spring-security.xmlより先に記述すること。

9.9.2.3.2. 認可サーバの定義

次に、認可サーバの定義を追加する。

  • oauth2-auth.xml
<oauth2:authorization-server
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize" >  <!-- (1) -->
    <oauth2:authorization-code />  <!-- (2) -->
    <oauth2:implicit />  <!-- (3) -->
    <oauth2:refresh-token />  <!-- (4) -->
    <oauth2:client-credentials />  <!-- (5) -->
    <oauth2:password />  <!-- (6) -->
</oauth2:authorization-server>
項番 説明
(1)
<oauth2:authorization-server>タグを使用し、認可サーバの設定を定義を行う。
<oauth2:authorization-server>タグを使用することで、認可を行うための認可エンドポイントと、アクセストークンを発行するためのトークンエンドポイントがコンポーネントとして登録される。
token-endpoint-url属性にトークンエンドポイントのURLを指定する。指定しない場合はデフォルト値である “/oauth/token” が指定される。
authorization-endpoint-url属性に認可エンドポイントのURLを指定する。指定しない場合はデフォルト値である “/oauth/authorize” が指定される。
(2)
<oauth2:authorization-code>タグを使用して、認可コードグラントをサポートする。
(3)
<oauth2:implicit>タグを使用して、インプリシットグラントをサポートする。
(4)
<oauth2:refresh-token>タグを使用して、リフレッシュトークンをサポートする。
(5)
<oauth2:client-credentials>タグを使用して、クライアントクレデンシャルグラントをサポートする。
(6)
<oauth2:password>タグを使用して、リソースオーナパスワードクレデンシャルグラントをサポートする。

Note

サポートするグラントタイプを複数指定する場合は上記の順番で指定する必要がある。

Note

認可コードは、認可コードが発行されてからアクセストークンの発行までの短い期間しか使われないため、デフォルトではインメモリで管理される。 認可サーバが複数台構成の場合は、複数サーバ間で認可コードを共有するためにDBで管理する必要がある。 認可コードをDBで管理する場合は、主キーとなる認可コードを保持するカラムと、認証情報を保持するカラムによって構成された以下のようなテーブルを作成する。以下の例ではPostgreSQLを使用した場合のDB定義を説明する。

../_images/OAuth_ERDiagramCode.png

認可サーバの設定ファイルには、<oauth2:authorization-code>タグのauthorization-code-services-refに、認可コードをDB管理するorg.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServicesのBean IDを指定する。 JdbcAuthorizationCodeServicesのコンストラクタには、認可コード格納用のテーブルに接続するためのデータソースを指定する。 認可コードをDBにて永続管理する場合の注意点についてはトランザクション制御必ず 参照のこと。

  • oauth2-auth.xml
<oauth2:authorization-server
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize" >
    <oauth2:authorization-code authorization-code-services-ref="authorizationCodeServices"/>
    <!-- omitted -->
</oauth2:authorization-server>

<bean id="authorizationCodeServices"
      class="org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices">
    <constructor-arg ref="codeDataSource"/>
</bean>

<!-- omitted -->

9.9.2.3.3. クライアントの認証

エンドポイントに対してアクセスしてきたクライアントについては、登録済みのクライアントか確認するために認証を行う必要がある。 クライアントの認証は、クライアントよりパラメータで渡されたクライアントIDとパスワードを、認可サーバで保持しているクライアント情報をもとに検証することで行う。認証にはBasic認証を用いて行う。

Spring Security OAuthではクライアント情報を取得するためのインタフェースであるoorg.springframework.security.oauth2.provider.ClientDetailsServiceの実装クラスを提供している。 また、クライアントの情報を保持するためのクラスとしてClientDetailsインタフェースの実装クラスであるorg.springframework.security.oauth2.provider.client.BaseClientDetailsクラスを提供している。 BaseClientDetailsではクライアントIDやサポートするグラントタイプなどのOAuth 2.0を利用する上での基本的なパラメータを提供しており、BaseClientDetailsを拡張することで独自のパラメータを追加することも可能である。 ここではBaseClientDetailsの拡張とClientDetailsServiceの実装クラス作成を行い、独自パラメータとして クライアント名 を追加したクライアント情報をDBを用いて管理、および認証を行う場合の実装方法について説明する。

まず、以下のようなDBを作成する。

../_images/OAuth_ERDiagram.png
項番 説明
(1)
クライアント情報を保持するテーブル。client_idを主キーとする。
各カラムの役割は以下のとおりである。
・client_id:クライアントを識別するIDであるクライアントIDを保持するカラム。
・client_secret:クライアントの認証に使用するパスワードを保持するカラム。
・client_name:クライアント名を保持する独自カラム。独自カラムであるため必須ではない。
・access_token_validity:アクセストークンの有効期間[秒]を保持するカラム。
・refresh_token_validity:リフレッシュトークンの有効期間[秒]を保持するカラム。
(2)
スコープ情報を保持するテーブル。client_idを外部キーとし、クライアント情報と対応付けする。
scopeカラムに、クライアント認可に使用するスコープを保持する。クライアントがもつスコープの数だけレコードを登録する。
(3)
リソース情報を保持するテーブル。client_idを外部キーとし、クライアント情報と対応付けする。
resource_idカラムに、クライアントのアクセス可能なリソースかどうかを、リソースサーバが識別するために使用するリソースIDを保持する。
リソースサーバが保持するリソースに対して定義しているリソースIDがここで登録されているリソースIDに含まれる場合のみ、リソースへのアクセスを許可される。
クライアントがアクセス可能なリソースIDの数だけレコードを登録する。
リソースIDを一件も登録しなかった場合は、全てのリソースに対してアクセス可能となるため、登録しない場合は注意が必要である。
(4)
グラント情報を保持するテーブル。client_idを外部キーとし、クライアント情報と対応付けする。
authorized_grant_typeカラムに、クライアントの使用するグラントを保持する。
クライアントが利用するグラントの数だけレコードを登録する。
(5)
リダイレクトURL情報を保持するテーブル。client_idを外部キーとし、クライアント情報と対応付けする。
web_server_redirect_uriカラムに、リソースオーナによる認可後にユーザエージェントをリダイレクトさせるURLを保持する。
リダイレクトURLは認可コードグラント、インプリシットグラントの場合のみ使用される。
認可コードグラント、インプリシットグラント以外のグラントタイプを使用する場合はテーブル自体が不要となる。
クライアントが認可リクエスト時に申告するURLと、ホストとルートパスが一致するリダイレクトURLがない場合はエラーとなる。
クライアントが申告する可能性のあるURLの数だけレコードを登録する。

クライアント情報を保持するモデルを作成する。

  • Client.java
public class Client implements Serializable{
    private String clientId; // (1)
    private String clientSecret; // (2)
    private String clientName; // (3)
    private Integer accessTokenValidity; // (4)
    private Integer refreshTokenValidity; // (5)
    // Getters and Setters are omitted
}
項番 説明
(1)
クライアントを識別するクライアントIDを保持するフィールド。
(2)
クライアントの認証に使用するパスワードを保持するフィールド。
(3)
Spring Security OAuthでは提供されていない、クライアント名を保持する拡張フィールド。
拡張フィールドであるため必須ではない。
(4)
アクセストークンの有効期間[秒]を保持するフィールド。
(5)
リフレッシュトークンの有効期間[秒]を保持するフィールド。

BaseClientDetailsクラスを継承させたクラスを作成することで、クライアントの詳細情報を簡単に拡張することができる。

  • OAuthClientDetails.java
public class OAuthClientDetails extends BaseClientDetails{
    private Client client;
    // Getter and Setter are omitted
}

org.springframework.security.oauth2.provider.ClientDetailsServiceは、認可処理で必要となるクライアント詳細情報をデータストアから取得するためのインタフェースである。 以下では、ClientDetailsServiceの実装クラスの作成について説明する。

  • OAuthClientDetailsService.java
@Service("clientDetailsService") // (1)
@Transactional
public class OAuthClientDetailsService implements ClientDetailsService {

    @Inject
    ClientRepository clientRepository;

    @Override
    public ClientDetails loadClientByClientId(String clientId)
            throws ClientRegistrationException {

        Client client = clientRepository.findClientByClientId(clientId); // (2)

        if (client == null) { // (3)
              throw new NoSuchClientException("No client with requested id: " + clientId);
        }

        // (4)
        Set<String> clientScopes = clientRepository.findClientScopesByClientId(clientId);
        Set<String> clientResources = clientRepository.findClientResourcesByClientId(clientId);
        Set<String> clientGrants = clientRepository.findClientGrantsByClientId(clientId);
        Set<String> clientRedirectUris = clientRepository.findClientRedirectUrisByClientId(clientId);


         // (5)
        OAuthClientDetails clientDetails = new OAuthClientDetails();
        clientDetails.setClientId(client.getClientId());
        clientDetails.setClientSecret(client.getClientSecret());
        clientDetails.setAccessTokenValiditySeconds(client.getAccessTokenValidity());
        clientDetails.setRefreshTokenValiditySeconds(client.getRefreshTokenValidity());
        clientDetails.setResourceIds(clientResources);
        clientDetails.setScope(clientScopes);
        clientDetails.setAuthorizedGrantTypes(clientGrants);
        clientDetails.setRegisteredRedirectUri(clientRedirectUris);
        clientDetails.setClient(client);

        return clientDetails;
    }

}
項番 説明
(1)
Serviceとしてcomponent-scanの対象とするため、クラスレベルに@Serviceアノテーションをつける。
Bean名をclientDetailsServiceとして指定する。
(2)
データベースから取得したクライアント情報をClientモデルに保持させる。
(3)
クライアント情報が見つからない場合は、Spring Security OAuthの例外であるNoSuchClientExceptionを発生させる。
(4)
クライアントに紐付く情報を取得する。
複数回にわけてRepositoryの呼び出しを行うことにより処理性能が落ちるような場合は(2)で一括取得する。
(5)
取得した各種情報をOAuthClientDetailsのフィールドに設定する。

oauth2-auth.xmlにクライアント認証に必要な設定を追記する。

  • oauth2-auth.xml
<sec:http pattern="/oth2/*token*/**"
    authentication-manager-ref="clientAuthenticationManager" realm="Realm">  <!-- (1) -->
    <sec:http-basic />           <!-- (2) -->
    <sec:csrf disabled="true"/>  <!-- (3) -->
    <sec:intercept-url pattern="/**" access="isAuthenticated()"/>  <!-- (4) -->
</sec:http>

<oauth2:authorization-server
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize"
     client-details-service-ref="clientDetailsService">  <!-- (5) -->
    <oauth2:authorization-code />
    <oauth2:implicit />
    <oauth2:refresh-token />
    <oauth2:client-credentials />
    <oauth2:password />
</oauth2:authorization-server>

<sec:authentication-manager id="clientAuthenticationManager">  <!-- (6) -->
    <sec:authentication-provider user-service-ref="clientDetailsUserService" >  <!-- (7) -->
        <sec:password-encoder ref="passwordEncoder"/>  <!-- (8) -->
    </sec:authentication-provider>
</sec:authentication-manager>

<bean id="clientDetailsUserService"
    class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">  <!-- (9) -->
    <constructor-arg ref="clientDetailsService" />  <!-- (10) -->
</bean>
項番 説明
(1)
アクセストークン操作に関するエンドポイントへのセキュリティ設定を行うために、エンドポイントURLとして /oth2/*token*/配下をアクセス制御の対象として指定する。
ここでは、アクセス制御対象となるエンドポイントURLを /oth2/から始まる値としているが、 Spring Security OAuthにより定義されているエンドポイントURL、およびそのデフォルト値は以下のとおりである。
・トークン払い出しに使用するエンドポイントのエンドポイントURLである/oauth/token
・トークンを検証するエンドポイントのエンドポイントURLである/oauth/check_token
・JWTの署名を公開鍵暗号方式で作成した場合に、公開鍵を取得するために使用するエンドポイントのエンドポイントURLである/oauth/token_key
authentication-manager-ref属性に(5)で定義しているクライアント認証用のAuthenticationManagerのBeanを指定する。
また、(2)のようにXML NamespaceでBasic認証を有効にした場合、Basic認証のRealm名が "Spring Security Application"となり、
アプリケーションの内部情報が露呈してしまうため、realm属性に適切な値を指定する。
(2)
クライアント認証にBasic認証を適用する。
(3)
/oth2/*token*/**へのアクセスに対してCSRF対策機能を無効化する。
Spring Security OAuthでは、OAuth 2.0のCSRF対策として推奨されている、stateパラメータを使用したリクエストの正当性確認を採用している。
(4)
エンドポイントURLの配下に対して、認証済みユーザーのみがアクセスできる権限を付与する設定。
Webリソースに対してアクセスポリシーの指定方法については、認可を参照されたい。
(5)
client-details-service-ref属性にOAuthClientDetailsServiceのBeanを指定する。
指定するBeanIDは、ClientDetailsServiceの実装クラスで指定したBeanIDと合わせる必要がある。
(6)
クライアントを認証するためのAuthenticationManagerをBean定義する。
リソースオーナの認証で使用するAuthenticationManagerと別名のBean IDを指定する必要がある。
リソースオーナの認証についてはリソースオーナの認証を参照されたい。
(7)
sec:authentication-provideruser-service-ref属性に(9)で定義しているClientDetailsUserDetailsServiceのBeanを指定する。
(8)
クライアントの認証に使用するパスワードのハッシュ化に使用するPasswordEncoderのBeanを指定する。
パスワードハッシュ化の詳細については パスワードのハッシュ化を参照されたい。
(9)
UserDetailsServiceインタフェースの実装クラスであるClientDetailsUserDetailsServiceをBean定義する。
リソースオーナの認証で使用するUserDetailsServiceと別名のBean IDを指定する必要がある。
(10)
コンストラクタの引数に、データベースからクライアント情報を取得するOAuthClientDetailsServiceのBeanを指定する。
指定するBean IDは、ClientDetailsServiceの実装クラスで指定したBean IDと合わせる必要がある。

9.9.2.3.4. リソースオーナの認証

アクセストークンの取得に認可コードグラントを用いる場合、ログイン画面を用意する等、なんらかの方法でリソースオーナを認証する必要がある。

本ガイドラインでは、リソースオーナの認証にSpring Securityを利用する前提とする。
認可の設定には、認証済みユーザーのみ認可エンドポイントURLへアクセスできるよう、認可エンドポイントURLを含んだURLをアクセスポリシーとして定義する必要がある。 また、認可画面の表示を行うコントローラのURLと、認可エンドポイントでの例外をハンドリングするコントローラのURLも同様にアクセスポリシーとして定義する必要がある。
認可画面の表示を行うコントローラについては スコープ認可画面のカスタマイズを、認可エンドポイントでの例外をハンドリングするコントローラについては 認可リクエスト時のエラーハンドリングを参照されたい。

Spring Securityの詳細については 認証及び 認可を参照されたい。

以下に認可エンドポイントURL、認可画面の表示を行うコントローラのURL、認可エンドポイントのエラーハンドリングを行うコントローラのURLを含んだアクセスポリシーの定義例を示す。

  • spring-security.xml
<sec:http authentication-manager-ref="userLoginManager"> <!-- (1) -->
    <sec:form-login login-page="/login"
        authentication-failure-url="/login?error=true"
        login-processing-url="/login" />
    <sec:logout logout-url="/logout"
        logout-success-url="/"
        delete-cookies="JSESSIONID" />
    <sec:access-denied-handler ref="accessDeniedHandler"/>
    <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
    <sec:session-management />
    <sec:intercept-url pattern="/login/**" access="permitAll" />
    <sec:intercept-url pattern="/oth2/**" access="isAuthenticated()" /> <!-- (2) -->
    <!-- omitted -->
</sec:http>

 <sec:authentication-manager id="userLoginManager"> <!-- (3) -->
    <sec:authentication-provider
        user-service-ref="userDetailsService">
        <sec:password-encoder ref="passwordEncoder" />
    </sec:authentication-provider>
</sec:authentication-manager>

<bean id="userDetailsService"
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
    <property name="dataSource" ref="dataSource" />
</bean>
項番 説明
(1)
認可エンドポイントURLである/oth2/authorize、認可画面の表示を行うコントローラのURLである/oauth/confirm_access、認可エンドポイントのエラーハンドリングを行うコントローラのURLである/oauth/errorを含んだルート(“/”)配下をアクセス制御の対象として指定する。
(2)
認可エンドポイントURLである/oth2/authorize、認可画面の表示を行うコントローラのURLである/oauth/confirm_access、認可エンドポイントのエラーハンドリングを行うコントローラのURLである/oauth/errorを含んだルート(“/oth2/”)配下を認証済みユーザーのみがアクセスできるよう指定する。
(3)
リソースオーナを認証するためのAuthenticationManagerをBean定義する。
クライアントの認証で使用するAuthenticationManagerと別名のBean IDを指定する必要がある。

9.9.2.3.5. スコープごとの認可

リソースオーナに認可を求める際に、要求されたスコープを一括で認可するのではなく、各スコープを個別に認可する場合の設定方法を説明する。

認可サーバを再起動した際に認可情報を失わないよう永続管理するために、また複数台の認可サーバで認可情報を共有するためには、認可情報をDBで管理する必要がある。 スコープごとに認可情報を格納するためのDBとして、以下のDBを作成する。以下の例ではPostgreSQLを使用した場合のDB定義を説明する。

../_images/OAuth_ERDiagramApprovals.png
項番 説明
(1)
認可情報を保持するテーブル。userId、clientId、scopeを主キーとする。
各カラムの役割は以下のとおりである。
・userId:認可を行ったリソースオーナのユーザ名を保持するカラム。
・clientId:リソースオーナによって認可されたクライアントのクライアントIDを保持するカラム。
・scope:リソースオーナに認可されたスコープを保持するカラム。
・status:リソースオーナに認可されたかどうかを保持するカラム。認可された場合はAPPROVED、拒否された場合はDENIEDが設定される。
・expiresAt:認可情報の有効期限を保持するカラム。
・lastModifiedAt:認可情報が最後に更新された日時を保持するカラム。

リソースオーナからスコープ毎の認可を取得し、DBに保存して管理するための設定を行う。

実装例は以下のとおりである。

  • oauth2-auth.xml
<oauth2:authorization-server
     client-details-service-ref="clientDetailsService"
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize"
     user-approval-handler-ref="userApprovalHandler"> <!-- (1) -->

     <!-- omitted -->

</oauth2:authorization-server>

<bean id="userApprovalHandler"
      class="org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler">  <!-- (2) -->
    <property name="clientDetailsService" ref="clientDetailsService"/>
    <property name="approvalStore" ref="approvalStore"/>
    <property name="requestFactory" ref="requestFactory"/>
    <property name="approvalExpiryInSeconds" value="3200" />
</bean>

<bean id="approvalStore"
      class="org.springframework.security.oauth2.provider.approval.JdbcApprovalStore">  <!-- (3) -->
    <constructor-arg ref="approvalDataSource"/>
</bean>

<bean id="requestFactory"
      class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory">
    <constructor-arg ref="clientDetailsService"/>
</bean>
項番 説明
(1)
スコープの認可処理を行うUserApprovalHandlerとして、user-approval-handler-refに(2)で定義しているApprovalStoreUserApprovalHandlerのBeanを指定する。
(2)
スコープの認可処理を行うApprovalStoreUserApprovalHandlerをBean定義する。
リソースオーナの認可結果を管理するapprovalStoreプロパティには、(3)で定義しているJdbcApprovalStoreのBeanを指定する。
スコープの認可処理に使用するクライアント情報の取得をするclientDetailsServiceプロパティには、OAuthClientDetailsServiceのBeanを指定する。
requestFactoryプロパティには、DefaultOAuth2RequestFactoryのBeanを指定する。
requestFactoryプロパティに設定したBeanはApprovalStoreUserApprovalHandlerによって使用されないが、設定を行っていない場合はApprovalStoreUserApprovalHandlerのBean生成時にエラーとなるため、requestFactoryプロパティへの設定が必要である。
認可情報の有効期間[秒]を指定する場合は、approvalExpiryInSecondsプロパティに、有効期間[秒]を設定する。設定を行わない場合は、認可情報は認可から一ヶ月間有効となる。
(3)
認可情報をDBで管理するJdbcApprovalStoreをBean定義する。
コンストラクタには、認可情報格納用のテーブルに接続するためのデータソースを指定する。
認可情報をDBにて永続管理する場合の注意点についてはトランザクション制御必ず 参照のこと。

Note

認可情報を永続管理する必要がなく、DBではなくインメモリで管理したい場合は、approvalStoreとしてorg.springframework.security.oauth2.provider.approval.InMemoryApprovalStoreをBean定義すればよい。

9.9.2.3.6. スコープ認可画面のカスタマイズ

スコープ認可画面をカスタマイズしたい場合、コントローラとJSPを作成することでカスタマイズできる。以下ではスコープ認可画面のカスタマイズした場合の例を説明する。

リソースオーナに認可を求めるエンドポイントの呼び出しを行う場合、(コンテキストパス)/oauth/confirm_accessにフォワードされる。 (コンテキストパス)/oauth/confirm_accessをハンドリングするコントローラを作成する。

  • OAuth2ApprovalController.java
@Controller
public class OAuth2ApprovalController {

    @RequestMapping("/oauth/confirm_access") // (1)
    public String confirmAccess() {
        // omitted
        return "approval/oauthConfirm";
    }

}
項番 説明
(1)
@RequestMappingアノテーションを使用して、"/oauth/confirm_access"へのアクセスに対するメソッドとしてマッピングを行う。

次に、スコープ認可画面のJSPを作成する。 認可対象のスコープはscopesキーでModelに登録されているため、これを利用してスコープ認可画面を表示する。

  • oauthConfirm.jsp
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<body>
    <div id="wrapper">
        <h1>OAuth Approval</h1>
        <p>Do you authorize '${f:h(authorizationRequest.clientId)}' to access your protected resources?</p>
        <form id='confirmationForm' name='confirmationForm' action='${pageContext.request.contextPath}/oth2/authorize' method='post'>
            <c:forEach var="scope" items="${scopes}" varStatus="status">  <!-- (1) -->
                <li>
                    ${f:h(scope.key)}  <!-- (2) -->
                    <input type='radio' name="${f:h(scope.key)}" value='true'/>Approve
                    <input type='radio' name="${f:h(scope.key)}" value='false'/>Deny
                </li>
            </c:forEach>
            <input name='user_oauth_approval' value='true' type='hidden'/>  <!-- (3) -->
            <sec:csrfInput />  <!-- (4) -->
            <label>
                <input name='authorize' value='Authorize' type='submit'/>
            </label>
        </form>
    </div>
</body>
項番 説明
(1)
スコープ毎に認可を指定するためのラジオボタンを出力する。対象のスコープはscopesリストに含まれるため、itemsscopesを指定する。
(2)
scopesリストが保持する要素のキー名が、それぞれのスコープ名となっているため、キー名を画面表示する。
許可、拒否を選ぶために、Approve、Denyのラジオボタンの出力設定を行う。
(3)
user_oauth_approvalをhidden項目として埋め込むことで、Spring Security OAuthがリクエストパラメータにuser_oauth_approvalを付与する。
リクエストパラメータに付与されたuser_oauth_approvalは、認可エンドポイントのスコープ認可を行うメソッドを実行するために用いられる。
(4)
CSRFを引き渡すために、HTMLの<form>要素の中に<sec:csrfInput>要素を指定する。

9.9.2.3.7. 認可リクエスト時のエラーハンドリング

認可エンドポイントで認可エラー(クライアント未存在エラー等のセキュリティに関わるエラーや、リダイレクトURLチェックエラー)が発生した場合、Spring Security OAuthが提供するOAuth2Exceptionthrowされ 、リクエストは(コンテキストパス)/oauth/errorにフォワードされる。 そのため認可エンドポイントでの例外をハンドリングする場合は(コンテキストパス)/oauth/errorをハンドリングするコントローラを作成する必要がある。

  • OAuth2ErrorController.java
@Controller
public class OAuth2ErrorController {

    @RequestMapping("/oauth/error") // (1)
    public String handleError() {
        // omitted
        return "common/error/oauthError";
    }

}
項番 説明
(1)
@RequestMappingアノテーションを使用して、"/oauth/error"へのアクセスに対するメソッドとしてマッピングを行う。

次に、表示させるエラー画面のJSPを作成する。 認可エンドポイントで発生したエラー内容はerrorキーでModelに登録されているため、これを利用してエラー内容を画面表示する。

  • oauthError.jsp
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OAuth Error!</title>
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>
<body>
    <div id="wrapper">
        <h1>OAuth Error!</h1>
        <p>${f:h(error.OAuth2ErrorCode)}</p> <!-- (1) -->
        <p>${f:h(error.message)}</p> <!-- (2) -->
    <br>
    </div>
</body>
</html>
項番 説明
(1)
errorに含まれるエラーレスポンスを出力する。
(2)
errorに含まれるエラーメッセージを出力する。

Note

認可エンドポイントで発生したエラーが、認可エラー(クライアント未存在エラー等のセキュリティに関わるエラーや、リダイレクトURLチェックエラー)以外の場合、 リダイレクトすることでクライアント側にエラー通知を行う。

9.9.2.3.8. リソースサーバとのアクセストークン共有方法

リソースサーバがアクセストークンを元にリソースへのアクセスに対する認可判定を行えるよう、認可サーバはTokenServicesを介してアクセストークンを連携する。 連携方法は以下に示すとおり複数存在する。

項番 連携方法 説明
(1)
DBを介した連携
共有DBを利用し、アクセストークンを連携する方法。
リソースサーバと認可サーバがDBを共有している場合に利用可能。
認可サーバはTokenServiceの実装としてDefaultTokenServicesを、TokenStoreの実装としてJdbcTokenStoreを指定する。
(2)
HTTPアクセスを介した連携
HTTPアクセスにより、アクセストークンを連携する方法。
リソースサーバと認可サーバが共有DBを利用できない場合に、この方法を利用する。
リソースサーバはアクセストークンの取得及び検証を認可サーバに依頼するため、認可サーバに負荷がかかる。
認可サーバはTokenServiceの実装としてDefaultTokenServicesを指定する。
アクセストークンをDBで管理する場合はJdbcTokenStoreを、メモリで管理する場合はInMemoryTokenStoreをTokenStoreの実装として指定する。
アクセストークンをメモリで管理する実装はサーバ再起動などでアクセストークンが失われるため、テスト用途専用の実装である。
(3)
JWTを利用した連携
JWTを利用し、アクセストークンを連携する方法。
リソースサーバと認可サーバが共有DBを利用できない場合に、この方法を利用する。
HTTPアクセスを介した連携と比べ、認可サーバにアクセストークンの取得を依頼しないため、認可サーバへの負荷がかからない。
認可サーバはTokenServiceの実装としてDefaultTokenServicesを、TokenStoreの実装としてJwtTokenStoreを指定する。
org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverterを利用することでアクセストークンの署名とエンコード、デコードを行う。
アクセストークンの署名とその検証には公開鍵を使用する方法と、共通鍵を使用する方法がある。
(4)
メモリを介した連携
メモリを共有することで、アクセストークンを連携する方法。
リソースサーバと認可サーバが一つのプロセスとなるようアプリケーションを設計している場合に利用可能。
認可サーバはTokenServiceの実装としてDefaultTokenServicesを、TokenStoreの実装としてInMemoryTokenStoreを指定する。
メモリを介して連携させるため、共有DBやHTTPアクセスによるアクセストークンの連携が不要となる。
メモリを介してアクセストークンを共有する実装はサーバ再起動などでアクセストークンが失われるため、テスト用途専用の実装である。

Todo

TBD

JWTを利用した連携の実装方法については、次版以降で詳細化する予定である。

ここでは、代表的な連携方法として共有DBを介して連携させる方法を説明する。 HTTPアクセスを介した連携ついては本節のHow To Extendにて説明している。

共有DBを介して連携させる場合、Spring Security OAuthが提供しているJdbcTokenStoreを使用する。

実装例は以下のとおりである。

  • oauth2-auth.xml
<oauth2:authorization-server
     client-details-service-ref="clientDetailsService"
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize"
     user-approval-handler-ref="userApprovalHandler"
     token-services-ref="tokenServices">  <!-- (1) -->
    <oauth2:authorization-code />
    <oauth2:implicit />
    <oauth2:refresh-token />
    <oauth2:client-credentials />
    <oauth2:password />
</oauth2:authorization-server>

<bean id="tokenServices"
    class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">  <!-- (2) -->
    <property name="tokenStore" ref="tokenStore" />
    <property name="clientDetailsService" ref="clientDetailsService" />
    <property name="supportRefreshToken" value="true" />  <!-- (3) -->
</bean>

<bean id="tokenStore"
  class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore"> <!-- (4) -->
  <constructor-arg ref="tokenDataSource" />
</bean>
項番 説明
(1)
認可サーバが使用するTokenServiceとしてtoken-services-ref属性に(2)で定義しているtokenServicesを指定する。
(2)
tokenServicesのクラスにDefaultTokenServicesを指定する。
アクセストークンを管理するトークンストアとして、tokenStoreプロパティに(4)で定義しているTokenStoreを指定する。
共有DBを介してリソースサーバとアクセストークンの連携を行う場合、リソースサーバでも本設定を行うこと。
(3)
リフレッシュトークンを有効にする場合はsupportRefreshToken属性にtrueを指定する。
(4)
トークンストアとして JdbcTokenStoreをBean定義する。
コンストラクタには、トークン情報格納用のテーブルに接続するためのデータソースを指定する。

JdbcTokenStoreがアクセストークンを連携するために、Spring Security OAuthがスキーマ定義している以下のDBを作成する。 以下の例では共有DBとしてPostgreSQLを使用した場合のDB定義を説明する。

../_images/OAuth_ERDiagramToken.png
項番 説明
(1)
アクセストークンを管理するテーブル。認可サーバで発行したアクセストークンの情報をリソースサーバと共有するために使用する。
各カラムの役割は以下のとおりである。
・authentication_id:認証情報を一意に識別する認証キーを保持するカラム。主キーとする。
・token:トークンの情報をシリアル化してバイナリとして保持するカラム。保持するトークンの情報としては、アクセストークンの有効期限、スコープ、アクセストークンのトークンID、リフレッシュトークンのトークンID、使用しているトークンの種類を表すトークンタイプを保持する。
・token_id:アクセストークンを一意に識別するトークンIDを保持するカラム。
・user_name:認証されたリソースオーナのユーザ名を保持するカラム。
・client_id:認証されたクライアントのクライアントIDを保持するカラム。
・authentication:リソースオーナとクライアントの認証情報をシリアル化してバイナリとして保持するカラム。
・refresh_token:アクセストークンに紐付くリフレッシュトークンのトークンIDを保持するカラム。
(2)
アクセストークンに紐付くリフレッシュトークンを管理するテーブル。
各カラムの役割は以下のとおりである。
・token_id:リフレッシュトークンを一意に識別するトークンIDを保持するカラム。主キーとする。
・token:トークンの情報をシリアル化してバイナリとして保持するカラム。リフレッシュトークンの有効期限を保持する。
・authentication:リソースオーナとクライアントの認証情報をシリアル化してバイナリとして保持するカラム。アクセストークンを管理するテーブルで保持している認証情報と同じ情報を保持する。

Note

共有DBでトークンを管理する場合、有効期限切れとなったトークンはクライアントがアクセストークンを利用するタイミングに削除される。 そのためトークンが有効期限切れになったとしても、クライアントがアクセストークンを利用しなければ削除されない。 有効期限切れとなったトークンをDBから削除するためには、バッチ処理等で別途パージを行う必要がある。

9.9.2.3.9. トークンの取り消し(認可サーバ)

発行したアクセストークンの取り消しの実装方法について説明する。

アクセストークンの取り消しは、インタフェースConsumerTokenServiceを実装したクラスの revokeTokenメソッドを呼び出すことで実現できる。 クラス DefaultTokenServiceはインタフェースConsumerTokenServiceを実装している。

アクセストークンの取り消し時に認可情報も削除することが可能である。 認可コードグラントやインプリシットグラントを使用している場合に、アクセストークンの取り消し後に認可情報を削除せずに認可リクエストを行うと、前回の認可リクエスト時の認可情報が再利用される場合がある。 前回の認可リクエスト時の認可情報は、認可情報の有効期限が有効であり、認可リクエストしたスコープが全て認可されている場合に再利用される。

以下に、実装例を示す。

トークンの取り消しを行うサービスクラスのインタフェースと実装クラスを作成する。

  • RevokeTokenService.java
public interface RevokeTokenService {

    String revokeToken(String tokenValue, String clientId);

}
  • RevokeTokenServiceImpl.java
@Service
@Transactional
public class RevokeTokenServiceImpl implements RevokeTokenService {

    @Inject
    ConsumerTokenServices consumerService; // (1)

    @Inject
    TokenStore tokenStore; // (2)

    @Inject
    ApprovalStore approvalStore; // (3)

    public String revokeToken(String tokenValue, String clientId){ // (4)
        // (5)
        OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);
        if (authentication != null) {
            if (clientId.equals(authentication.getOAuth2Request().getClientId())) { // (6)
                // (7)
                Authentication user = authentication.getUserAuthentication();
                if (user != null) {
                    Collection<Approval> approvals = new ArrayList<Approval>();
                    for (String scope : authentication.getOAuth2Request().getScope()) {
                        approvals.add(
                                new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
                    }
                    approvalStore.revokeApprovals(approvals);
                }
                consumerService.revokeToken(tokenValue); // (8)
                return "success";

            } else {
                return "invalid client";
            }
        } else {
            return "invalid token";
        }
    }
}
項番 説明
(1)
アクセストークンの取り消しを行うインタフェースConsumerTokenServiceの実装クラスをインジェクションする。
(2)
アクセストークン発行時の認証情報を取得するために使用するTokenStoreの実装クラスをインジェクションする。
(3)
アクセストークンの発行時の認可情報を取得するために使用するApprovalStoreの実装クラスをインジェクションする。
アクセストークンの取り消し時に認可情報を削除しない場合は不要となる。
(4)
取り消しを行うアクセストークンの値と、クライアントのチェックを行うために使用するクライアントIDをパラメータとして受け取る。
(5)
TokenStoreの実装クラスのreadAuthenticationメソッドを呼び出し、アクセストークンを発行した際の認証情報を取得する。
認証情報が正常に取得できた場合のみ、トークンの削除処理を行う。
(6)
認証情報より、アクセストークン発行時に使用したクライアントIDを取得し、リクエストパラメータのクライアントIDと一致するかを確認する。
アクセストークン発行時のクライアントIDと一致する場合のみ、アクセストークンの削除を行う。
(7)
認証情報より、アクセストークン発行時のリソースオーナの認証情報を取得する。
リソースオーナの認証情報が取得できた場合、TokenStoreの実装クラスのrevokeApprovalsメソッドを呼び出し、認可情報の削除を行う。
クライアントクレデンシャルグラントを使用している場合はリソースオーナの認証情報が存在しないため、revokeApprovalsメソッドに渡すパラメータが生成できない。
そのため、リソースオーナの認証情報が取得できない場合は認可情報の削除処理は行わない。
アクセストークンの取り消し時に認可情報を削除しない場合、この処理は不要となる。
(8)
ConsumerTokenServiceの実装クラスのrevokeTokenメソッドを呼び出し、アクセストークンとアクセストークンに紐付くリフレッシュトークンの削除を行う。

トークンの取り消しリクエストを受けるコントローラを作成する。

  • TokenRevocationRestController.java
@RestController
@RequestMapping("oth2")
public class TokenRevocationRestController {

    @Inject
    RevokeTokenService revokeTokenService;

    @RequestMapping(value = "tokens/revoke", method = RequestMethod.POST) // (1)
    @ResponseStatus(HttpStatus.OK)
    public String revoke(@RequestParam("token") String token,
        @AuthenticationPrincipal UserDetails user){

        // (2)
        String clientId = user.getUsername();
        String result = revokeTokenService.revokeToken(token, clientId); // (3)
        return result;
    }
項番 説明
(1)
@RequestMappingアノテーションを使用して、"/oth2/tokens/revoke"へのアクセスに対するメソッドとしてマッピングを行う。
ここで指定するパスはクライアントの認証で行った設定と同様に、Basic認証の適用とCSRF対策機能の無効化を行う必要がある。
(2)
Basic認証で生成された認証情報からトークンの取り消し時のチェックで使用するクライアントIDを取得する。
(3)
RevokeTokenServiceを呼び出し、トークンの取り消しを行う。
リクエストパラメータとして受け取ったアクセストークンの値と、認証情報から取得したクライアントIDをパラメータとして渡す。

Tip

RFC 7009ではリクエストパラメータとしてtoken_type_hintを任意で付与してよいことが記載されている。 token_type_hintは削除対象のトークンがアクセストークンとリフレッシュトークンのどちらであるかを判別するためのヒントである。 Spring Security OAuthが提供するConsumerTokenServiceはアクセストークンを渡すことでアクセストークンとリフレッシュトークンの両方削除するため、上記の実装例では使用していない。

Note

認可サーバにトークンの取り消しをリクエストしたクライアントは、認可サーバの削除後にクライアントで保持しているトークンも取り消す必要がある。 クライアントサーバのトークンの取り消しについてはトークンの取り消し(クライアントサーバ)を参照されたい。


9.9.2.3.10. トランザクション制御

認可サーバにおけるトランザクション制御の注意点について説明する。

認可サーバにおいてSpring Security OAuthが取り扱う情報(認可コード、認可情報、トークン)をDBにて管理する場合には、トランザクション制御の考慮が必要となる。

AuthorizationServerTokenServicesのデフォルト実装であるDefaultTokenServicesではアクセストークン、リフレッシュトークンを発行するメソッドである createAccessTokenrefreshAccessTokenにそれぞれ@Transactionalが付与されていることでトランザクション管理下で呼び出しが行われるが、それ以外は非トランザクション管理となる。

そのため、DataSourceから取得するConnectionの設定がautoCommit=falseとなっている場合、管理対象となる情報がDBに登録されないためトランザクション管理が必須である。 また、autoCommit=trueの場合は必須ではないが、データの一貫性を担保するためのトランザクション管理の考慮が必要となるため、注意すること。

認可コード、認可情報をDBにて管理する場合のトランザクション制御設定例を以下に示す。

  • oauth2-auth.xml
<!-- omitted -->

<tx:advice id="oauthTransactionAdvice">
    <tx:attributes>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="authorizationOperation"
                  expression="execution(* org.springframework.security.oauth2.provider.code.AuthorizationCodeServices.*(..))"/> <!-- (1) -->
    <aop:pointcut id="approvalOperation"
                  expression="execution(* org.springframework.security.oauth2.provider.approval.UserApprovalHandler.*(..))"/> <!-- (2) -->
    <aop:advisor pointcut-ref="authorizationOperation" advice-ref="oauthTransactionAdvice"/>
    <aop:advisor pointcut-ref="approvalOperation" advice-ref="oauthTransactionAdvice"/>
</aop:config>
項番 説明
(1)
AOPを使用し、認可コードを操作する各メソッドにトランザクション境界を設定する。
(2)
AOPを使用し、認可情報を操作する各メソッドにトランザクション境界を設定する。

9.9.2.4. リソースサーバの実装

ここではTODOリソースのREST APIに対して認可設定を行う実装例を用いて、リソースサーバの実装方法について説明する。

9.9.2.4.1. 設定ファイルの作成(リソースサーバ)

リソースサーバを実装する際には新たに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:oauth2="http://www.springframework.org/schema/security/oauth2"
       xmlns:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security.xsd
           http://www.springframework.org/schema/security/oauth2
           http://www.springframework.org/schema/security/spring-security-oauth2.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <sec:http pattern="/api/v1/todos/**" create-session="stateless"
                   entry-point-ref="oauth2AuthenticationEntryPoint"> <!-- (1) -->
        <sec:access-denied-handler ref="oauth2AccessDeniedHandler"/> <!-- (2) -->
        <sec:csrf disabled="true"/> <!-- (3) -->
        <sec:custom-filter ref="oauth2AuthenticationFilter"
                                before="PRE_AUTH_FILTER" /> <!-- (4) -->
    </sec:http>

    <bean id="oauth2AccessDeniedHandler"
              class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" /> <!-- (5) -->

    <bean id="oauth2AuthenticationEntryPoint"
              class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" /> <!-- (6) -->

    <oauth2:resource-server id="oauth2AuthenticationFilter" resource-id="todoResource"
              token-services-ref="tokenServices" entry-point-ref="oauth2AuthenticationEntryPoint" /> <!-- (7) -->

</beans>

項番 説明
(1)
pattern属性にはOAuth 2.0の認可設定の対象とするパスのパターンを指定する。
entry-point-refにはOAuth2AuthenticationEntryPointのBeanを指定する。ここでの設定は定義上必要だが指定したBeanは使用されない。 実際に使用されるのは後述するOAuth2AuthenticationProcessingFilterに指定しているOAuth2AuthenticationEntryPointのBeanである。
(2)
access-denied-handlerにはOAuth2AccessDeniedHandlerのBeanを設定する。ここでは後述するoauth2AccessDeniedHandlerを指定している。
(3)
CSRFトークンチェックによってPOST、PUT、DELETEといったリクエストを受けると必ずエラーになるため、CSRFを無効にする。
(4)
custom-filterにはリソースサーバ用の認証フィルタを指定する。ここでは後述するoauth2AuthenticationFilterを指定している。
この指定によりOAuth2AuthenticationProcessingFilterが設定される。
OAuth2AuthenticationProcessingFilterはリクエストに含まれるアクセストークンを利用してPre-Authenticationを行うためのフィルタであるため、
beforePRE_AUTH_FILTERを指定しPRE_AUTH_FILTERの前にOAuth2AuthenticationProcessingFilterの処理が実行されるように設定する。
Pre-Authenticationについてはこちらを参照されたい。
(5)
Spring Security OAuthが提供するリソースサーバ用のAccessDeniedHandlerを定義する。
OAuth2AccessDeniedHandlerは、認可エラー時に発生する例外をハンドリングしてエラー応答を行う。
(6)
OAuth用のエラー応答を行うためのOAuth2AuthenticationEntryPointをBean定義する。
(7)
Spring Security OAuthが提供するリソースサーバ用のServletFilterを定義する。
id属性に指定した文字列はBeanのIDとなる。ここでは oauth2AuthenticationFilterを指定している。
resource-id属性にはサーバが提供するリソースのIDを指定する。ここではtodoResourceを指定している。
アクセストークンに紐付くクライアント情報のリソースIDに対し、resource-id属性に指定したリソースIDが含まれているか検証が行われる。
検証の結果、リソースIDが含まれている場合のみリソースに対してのアクセスを許可する。
なお、resource-idの定義は任意であり、定義しない場合はリソースIDの検証が行われない。
token-services-ref属性にはTokenServicesのIDを指定する。TokenServicesについては後述する。
entry-point-reff属性にはOAuth2AuthenticationEntryPointのBeanを指定する。ここではoauth2AuthenticationEntryPointを指定している。

作成した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.4.2. リソースにアクセス可能なスコープの設定

リソースごとにアクセス可能なスコープを定義するために、OAuth 2.0用のBean定義ファイルに スコープの定義とSpEL式をサポートするためのBean定義を追加する。

実装例は以下のとおりである。 なお、認可サーバ、リソース間でセキュリティが担保されていることを前提としているため、アクセス制御の設定において、クライアントに対するBasic認証は行っていない。

  • 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:oauth2="http://www.springframework.org/schema/security/oauth2"
       xmlns:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security.xsd
           http://www.springframework.org/schema/security/oauth2
           http://www.springframework.org/schema/security/spring-security-oauth2.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <sec:http pattern="/api/v1/todos/**" create-session="stateless"
                   entry-point-ref="oauth2AuthenticationEntryPoint">
        <sec:access-denied-handler ref="oauth2AccessDeniedHandler"/>
        <sec:csrf disabled="true"/>
        <sec:expression-handler ref="oauth2ExpressionHandler"/> <!-- (1) -->
        <sec:intercept-url pattern="/**" method="GET"
                                access="#oauth2.hasScope('READ')" /> <!-- (2) -->
        <sec:intercept-url pattern="/**" method="POST"
                                access="#oauth2.hasScope('CREATE')" /> <!-- (2) -->
        <sec:custom-filter ref="oauth2ProviderFilter"
                                before="PRE_AUTH_FILTER" />
    </sec:http>

    <!-- omitted -->

    <oauth2:web-expression-handler id="oauth2ExpressionHandler" /> <!-- (3) -->

</beans>

項番 説明
(1)
expression-handlerにはOAuth2WebSecurityExpressionHandlerのBeanを指定する。
(2)
intercept-urlを使用してリソースに対してスコープによるアクセスポリシーを定義する。
pattern属性には保護したいリソースのパスのパターンを指定する。本実装例では /api/v1/todos/ 配下のリソースが保護される。
method属性にはリソースのHTTPメソッドを指定する。
access属性にはリソースへのアクセスを認可するscopeを指定する。設定値は大文字、小文字を区別する。
ここではSpEL式を用いて指定を行っている。
(3)
OAuth2WebSecurityExpressionHandlerをBean定義する。
このBeanを定義することでSpring Security OAuthが提供するOAuth 2.0の認可設定を行うためのSpELがサポートされる。
なお、id属性に指定した値がこのbeanのidとなる。

Spring Security OAuthが用意している主なExpressionを紹介する。

詳細についてはOAuth2SecurityExpressionMethodsJavaDocを参照されたい。

Spring Security OAuthが用意しているExpression
Expression 説明
clientHasRole(String role)
クライアントが引数に指定されたロールを持っている場合にtrueを返却する。
clientHasAnyRole(String... roles)
クライアントが引数に指定されたいずれかのロールを持っている場合にtrueを返却する。
hasScope(String scope)
クライアントがリソースオーナから認可されているスコープと引数のスコープが一致する場合にtrueを返却する。
hasAnyScope(String... scopes)
クライアントがリソースオーナから認可されているスコープと引数のスコープのいずれかが一致する場合にtrueを返却する。
hasScopeMatching(String scopeRegex)
クライアントがリソースオーナから認可されているスコープと引数に指定された正規表現が一致する場合にtrueを返却する。
hasAnyScopeMatching(String... scopesRegex)
クライアントがリソースオーナから認可されているスコープと引数に指定されたいずれかの正規表現が一致する場合にtrueを返却する。
denyOAuthClient
OAuth 2.0でのリクエストを拒否する。リソースオーナのみがリソースにアクセスできるようにするために使用される。
isOAuth
OAuth 2.0でのリクエストを許可する。クライアントがリソースにアクセスできるようにするために使用される。

Note

SpEL式についてはSpring Securityが提供するSpELを合わせて使用することができる。

Spring Securityが提供するSpELについては Built-InのWeb Expressions を参照されたい。


9.9.2.4.3. アクセストークンに関する設定(リソースサーバ)

認可サーバとリソースサーバはアクセストークンをTokenServicesを介して連携する。

連携方法の種類についてはリソースサーバとのアクセストークン共有方法を参照されたい。

ここではデータベースを共有する方法で設定を行う。

設定の解説については認可サーバでのTokenServicesの設定と同様なためリソースサーバとのアクセストークン共有方法を参照されたい。

  • oauth2-resource.xml
<bean id="tokenServices"
    class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    <property name="tokenStore" ref="tokenStore" />
</bean>

<bean id="tokenStore"
  class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
  <constructor-arg ref="tokenDataSource" />
</bean>

Note

認可サーバとリソースサーバを連携させる方法として、Spring Security OAuthで提供されているRemoteTokenServicesを利用する方法や、 JSON Web Tokenを利用する方法がある。 Spring Security OAuthで提供されているRemoteTokenServicesを利用する方法についてはHTTPアクセスを介した連携を参照されたい。


9.9.2.4.4. ユーザ情報の取得

リソースサーバでは、認証処理とSpring MVCの連携で説明されている認証情報の取得方法と同様に、Controllerクラスのメソッド引数にUserDetailsを指定し@AuthenticationPrincipalアノテーションを付与することにより認証されたリソースオーナの情報を受け取ることができる。 以下に実装例を示す。

@RestController
@RequestMapping("api")
public class TodoRestController {

    // omitted

    @RequestMapping(value = "todos", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public Collection<Todo> list(@AuthenticationPrincipal UserDetails user) { // (1)

        // omitted

    }
}
項番 説明
(1)
引数 userにリソースオーナの認証情報が格納される。

UserDetailsを指定した実装の場合、認証処理が行われないクライアントクレデンシャルグラントでは意図した情報が取得できないため注意が必要である。 クライアントクレデンシャルグラントを使用する場合は、Controllerのメソッド引数としてStringを指定し@AuthenticationPrincipalアノテーションを付与することによりクライアントIDを取得することができる。

また、UserDetailsを指定した実装の場合、クライアントID等のクライアントの認証情報は取得できない。 クライアントの認証情報を取得する場合は、Controllerクラスのメソッド引数にOAuth2Authenticationを指定することで可能である。 以下にControllerクラスのメソッド引数にOAuth2Authenticationを指定してクライアントとリソースオーナの認証情報を取得する例を示す。

@RestController
@RequestMapping("api")
public class TodoRestController {

    // omitted

    @RequestMapping(value = "todos", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public Collection<Todo> list(OAuth2Authentication authentication) { // (1)

        String username = authentication.getUserAuthentication().getName(); // (2)
        String clientId = authentication.getOAuth2Request().getClientId();  // (3)

        // omitted

    }
}
項番 説明
(1)
引数 authenticationにリソースオーナ、クライアントの認証情報が格納される。
(2)
authenticationよりリソースオーナ名を取得する。
(3)
authenticationよりクライアントIDを取得する。

Note

上記はアプリケーション層でOAuth2Authenticationを使用する場合の実装例である。 OAuth2Authenticationに依存しない実装を行いたい場合HandlerMethodArgumentResolverを実装することで同様の機能が実現可能である。 具体的な実装方法については、HandlerMethodArgumentResolverの実装を参照されたい。


9.9.2.5. クライアントの実装

クライアントの実装方法は、使用するグラントタイプにより、大きく以下の2つに分類される。

実装方法 グラントタイプ
OAuth2RestTemplate
認可コードグラント
リソースオーナパスワードクレデンシャルグラント
クライアントクレデンシャルグラント
Javascript
インプリシットグラント

本ガイドラインでは、上記の分類にてOAuth2RestTemplateを使用したリソースへのアクセスJavaScriptを使用したリソースへのアクセスをそれぞれ説明する。


9.9.2.5.1. OAuth2RestTemplateを使用したリソースへのアクセス

認可コードグラント、リソースオーナパスワードクレデンシャルグラント、クライアントクレデンシャルグラント向けの クライアントの実装として、OAuth2RestTemplateを使用してリソースへのアクセスを実現する方法を説明する。

Spring Security OAuthでは、RestOperationsのOAuth 2.0向けの実装としてOAuth2RestTemplateを提供している。

OAuth2RestTemplateでは、OAuth 2.0独自の機能として、AccessTokenProviderによるグラントタイプに応じた アクセストークンの取得や、OAuth2ClientContextによる複数リクエスト間でのアクセストークンの共有、リソースサーバへのアクセス時のエラーハンドリングといった機能を実装している。

クライアントでは、OAuth2RestTemplateを利用し、グラントタイプやスコープなどのアプリケーション要件に沿った パラメータを定義することで、OAuth 2.0機能を使用したリソースへのアクセスが可能となる。


9.9.2.5.1.1. 設定ファイルの作成(クライアント)

まず、OAuth 2.0に関する定義を行うための設定ファイルとして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:oauth2="http://www.springframework.org/schema/security/oauth2"
    xsi:schemaLocation="
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
    ">


</beans>

web.xmlに、作成したoauth2-client.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>

<!-- omitted -->
項番 説明
(1)
oauth2-client.xmlを読み込むように設定する。

9.9.2.5.1.2. OAuth2ClientContextFilterの適用

OAuth2ClientContextFilterをサーブレットフィルタとして登録する。

OAuth2ClientContextFilterを登録することでリソースオーナによる認可が取得できていない状態でリソースサーバへのアクセスを試みた場合に 発生するUserRedirectRequiredExceptionを捕捉し、認可サーバが提供するリソースオーナの認可を取得するためのページへリダイレクトする 機能を組み込むことができる。

oauth2-client.xmlには以下の設定を追加する。

  • oauth2-client.xml
<oauth2:client id="oauth2ClientContextFilter" /> <!-- (1) -->
項番 説明
(1)
<oauth2:client>タグを使用することで、OAuth2ClientContextFilterのBeanが定義される。id属性に指定した文字列はBeanのIDとして使用される。

web.xmlに、OAuth2ClientContextFilterの設定を追加する。

  • web.xml
<filter> <!-- (1) -->
    <filter-name>oauth2ClientContextFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>oauth2ClientContextFilter</filter-name>
    <url-pattern>/*</url-pattern> <!-- (2) -->
</filter-mapping>
項番 説明
(1)
DelegatingFilterProxyを使用して、フィルタ名(<filter-name>要素に指定した値)とBean IDが一致するBeanをサーブレットフィルタとして登録する。
フィルタ名には<oauth2:client>id属性に指定したBean IDと同じ値を設定する。
なお、Spring Security OAuthで発生する例外が意図しない例外ハンドリングが行われないようにするために、OAuth2ClientContextFilterはサーブレットフィルタの定義の一番最後に記述することを推奨する。
(2)
UserRedirectRequiredExceptionが発生する可能性があるパスに対してOAuth2ClientContextFilterを適用している。
上記例では、すべてのリクエストに対してOAuth2ClientContextFilterを適用している。

Note

OAuth2ClientContextFilterは、認可サーバが認可処理後にユーザエージェントを戻すリダイレクトURLの生成をフィルタの前処理で行っているため、 /*のような広範囲な定義にするとUserRedirectRequiredExceptionが発生しないパスに対して無駄な処理が行われることになる。

Note

OAuth2ClientContextFilterは、認可サーバがリソースオーナの認可を取得した後にリダイレクトさせるURLをリクエストスコープに currentUriという属性名で格納する。そのため、クライアントではcurrentUriという属性名を使用することはできない。


前述のとおり、Oauth2ClientContextFilterUserRedirectRequiredExceptionを捕捉し、認可サーバに対してリダイレクトさせるサーブレットフィルタである。

ただし、ブランクプロジェクトで予め設定されているSystemExceptionResolverが先にUserRedirectRequiredExceptionをハンドリングしてしまうと Oauth2ClientContextFilterは期待した動作にならない。

そのため、spring-mvc.xmの設定を変更し、SystemExceptionResolverUserRedirectRequiredExceptionをハンドリングしないようにする必要がある。 SystemExceptionResolverの詳しい解説については例外ハンドリングを参照されたい。

spring-mvc.xmlに、UserRedirectRequiredExceptionSystemExceptionResolverの除外対象として追加する。

  • spring-mvc.xml
<bean id="systemExceptionResolver"
    class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">

    <!-- omitted -->

    <property name="excludedExceptions">
        <array>
            <!-- (1) -->
            <value>org.springframework.security.oauth2.client.resource.UserRedirectRequiredException
            </value>
        </array>
    </property>
</bean>
項番 説明
(1)
UserRedirectRequiredExceptionSystemExceptionResolverのハンドリング対象から除外する。

9.9.2.5.1.3. OAuth2RestTemplateの設定

各グラントタイプごとのOAuth2RestTemplateの設定方法を説明する。

9.9.2.5.1.3.1. 認可コードグラント使用時のリソースの設定

認可コードグラントでアクセスする場合のOAuth2RestTemplateの設定例を示す。

  • oauth2-client.xml
<oauth2:resource id="todoAuthCodeGrantResource" client-id="firstSec"
                 client-secret="firstSecSecret"
                 type="authorization_code"
                 scope="READ,WRITE"
                 access-token-uri="${auth.serverUrl}/oth2/token"
                 user-authorization-uri="${auth.serverUrl}/oth2/authorize"/> <!-- (1) -->

<oauth2:rest-template id="todoAuthCodeGrantResourceRestTemplate" resource="todoAuthCodeGrantResource" /> <!-- (2) -->
項番 説明
(1)
OAuth2RestTemplateが参照する、アクセス対象となるリソースに関する詳細情報を定義する。
各項目の設定値については下記表を参照のこと。
(2)
OAuth2RestTemplateを定義する。
idにはOAuth2RestTemplateのBean IDを指定する。
resourceには(1)で定義したBeanのidを指定する。

リソース詳細情報
項目 説明
id
リソースのBean ID。
client-id
認可サーバにてクライントを識別するID。
client-secret
認可サーバにてクライアントの認証に用いるパスワード。
type
グラントタイプ。認可コードグラントの場合authorization_codeを指定する。
scope
認可を要求するスコープをカンマ区切りで列挙する。設定値は大文字、小文字を区別する。
省略時は認可サーバにおいてクライアントに対して設定しているスコープを全て要求する。
access-token-uri
アクセストークンの発行を依頼するための認可サーバのエンドポイントのURL。
user-authorization-uri
リソースオーナの認可を得るための認可サーバのエンドポイントのURL。

Note

<oauth2:resource>タグでは、アクセストークン取得時のクライアント認証方法を指定する方法として client-authentication-schemeパラメータが用意されている。 client-authentication-schemeパラメータに指定可能な値は以下の通り。

  • header: Authorizationヘッダを使用したBasic認証。デフォルト値。
  • query : リクエスト時のURLクエリパラメータを使用した認証。
  • form : リクエスト時のボディパラメータを使用した認証。

本ガイドラインではクライアントの認証にBasic認証を利用するため上記の設定例では未指定としているが、 アプリケーション要件に沿ったパラメータの指定を行うこと。

<oauth2:resource id="todoAuthCodeGrantResource" client-id="firstSec"
                 client-secret="firstSecSecret"
                 type="authorization_code"
                 scope="READ,WRITE"
                 access-token-uri="${auth.serverUrl}/oth2/token"
                 user-authorization-uri="${auth.serverUrl}/oth2/authorize"
                 client-authentication-scheme="form" />
9.9.2.5.1.3.2. リソースオーナパスワードクレデンシャルグラント使用時のリソースの設定
リソースオーナパスワードクレデンシャルグラントでは、クライアントがリソースオーナのユーザ名およびパスワードを使用してアクセストークンの発行を依頼する。
OAuth2RestTemplateにはリソースオーナのユーザ名およびパスワードをそれぞれパラメータとして設定する必要があるが、 複数のリソースオーナが同じクライアントを利用する場合、リソースオーナ毎に設定内容を切り替える考慮が必要となる。
ここでは、OAuth2RestTemplateのリソースをSessionスコープのBeanで設定し、そのBeanにリソースオーナの情報を格納することによって、 リソースオーナ毎の設定内容の切り替えを実現する方法を説明する。

リソースオーナパスワードクレデンシャルグラントでアクセスする場合のOAuth2RestTemplateの設定例を示す。

リソースオーナ毎に設定内容を切り替えられるよう、ResourceOwnerPasswordResourceDetailsをSessionスコープで定義し、OAuth2RestTemplateへの設定を行う。

  • 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:oauth2="http://www.springframework.org/schema/security/oauth2"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
    ">

    <!-- omitted -->

    <bean id="todoPasswordGrantResource" class="org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails"
        scope="session">
        <aop:scoped-proxy>
        <property name="clientId" value="firstSec" />
        <property name="clientSecret" value="firstSecSecret" />
        <property name="accessTokenUri" value="${auth.serverUrl}/oth2/token" />
        <property name="scope" value="READ,WRITE" />
    </bean> <!-- (1) -->

    <oauth2:rest-template id="todoPasswordGrantResourceRestTemplate" resource="todoPasswordGrantResource" /> <!-- (2) -->
項番 説明
(1)
OAuth2RestTemplateが参照する、アクセス対象となるリソースに関する詳細情報を定義する。
各項目の設定値については下記表を参照のこと。
(2)
OAuth2RestTemplateを定義する。
idにはOAuth2RestTemplateのBean IDを指定する。
resourceには(1)で定義したBeanのidを指定する。

項目 説明
class
OAuth2RestTemplateのリソースとするBeanを指定する。ここではResourceOwnerPasswordResourceDetailsを指定する。
scope
sessionを指定し、スコープ範囲をHTTPSessionとする。
<aop:scoped-proxy>
SessionスコープのBeanをSingletonのBeanであるOAuth2RestTemplateにインジェクションするため設定する。
これは、SessionスコープのBeanよりSingletonのBeanの方がライフサイクルが長いため必要になる設定である。
このタグを使用するためにaopのネームスペースとスキーマを追加している。
clientIdプロパティ
BeanのclientIdに対して認可サーバにてクライントを識別するIDを設定する。
clientSecretプロパティ
BeanのclientSecrettに対して認可サーバにてクライアントの認証に用いるパスワードを設定する。
accessTokenUriプロパティ
アクセストークンの発行を依頼するための認可サーバのエンドポイントのURLを指定する。
scopeプロパティ
Beanのscopeに対して認可を要求するスコープの一覧を設定する。

Note

リソースオーナのユーザ名、パスワードの取得は、アクセスが必要となったタイミングでクライアントの画面等でリソースオーナから入力され、Beanに格納することを想定している。 本ガイドラインでは、ユーザ名、パスワードの具体的な取得方法については説明を割愛する。

Warning

本ガイドラインで説明している認可サーバでは、認証に使用するパスワードに対してハッシュ化の後比較検証を行うため、ResourceOwnerPasswordResourceDetailsに設定するパスワードは平文である必要がある。 クライアントがリソースオーナのパスワードを平文で扱うことによるリスクが高いため、リソースオーナパスワードクレデンシャルグラントは クライアント-リソースオーナ間で高い信頼関係があり、かつクライアントがセキュアな環境に配置されているなどの非常に限定的な状況でのみ利用すること。 認可サーバにおけるハッシュ化の設定については クライアントの認証を参照のこと。


9.9.2.5.1.3.3. クライアントクレデンシャルグラント使用時のリソースの設定

クライアントクレデンシャルグラントでアクセスする場合のOAuth2RestTemplateの設定例を示す。

なお、認可コードグラントと共通する設定についての解説は割愛する。

  • oauth2-client.xml
<oauth2:resource id="todoClientGrantResource" client-id="firstSecClient"
                client-secret="firstSecSecret"
                type="client_credentials"
                access-token-uri="${auth.serverUrl}/oth2/token" /> <!-- (1) -->

<oauth2:rest-template resource=id="todoClientGrantResourceRestTemplate" resource="todoClientGrantResource" />
項番 説明
(1)
type属性にはグラントタイプを指定する。クライアントクレデンシャルグラントの場合client_credentialsを指定する。
9.9.2.5.1.4. リソースサーバへのアクセス

OAuth2RestTemplateを用いてリソースサーバへアクセスする方法を説明する。

認可サーバへのアクセスはOAuth2RestTemplateOAuth2ClientContextFilterにより隠蔽されるため、開発の際には 認可サーバを意識する必要がなく、リソースサーバが提供するREST APIに対して行う処理を、通常のREST APIへのアクセスと同様に記述する。

以下にServiceクラスの実装例を示す。

import org.springframework.web.client.RestOperations;

@Service
public class TodoServiceImpl implements TodoService {

    @Inject
    @Named("todoAuthCodeGrantResourceRestTemplate")
    RestOperations restOperations; // (1)

    @Value("${resource.serverUrl}/api/v1/todos")
    String uri;

    @Override
    public List<Todo> getTodoList() {
        Todo[] todoArray = restOperations.
            getForObject(url, Todo[].class); // (2)
        return Arrays.asList(todoArray);
    }
}
項番 説明
(1)
RestOperationsをInejectする。
上記例では、Bean IDがtodoAuthCodeGrantResourceRestTemplateであるBeanをInjectしている。@Namedの指定はOAuth2RestTemplateが複数定義されている場合に必要となる。
(2)
指定したURLにRESTでメソッドGETでアクセスし結果をリストで受け取る。

Note

OAuth2RestTemplateを使用してリソースサーバにアクセスしようとした時点でアクセストークンが発行されていない場合、 一度認可サーバにリダイレクトされる。 その後、アクセストークンの発行が完了するとクライアントへリダイレクトされ、再度リソースサーバへのアクセス処理が呼び出される形となる。 この時、クライアントへのリダイレクトはGETにより行われるため、認可サーバからリダイレクトされる可能性のあるControllerはGETを許容する必要がある。

また、リダイレクト前のリソースサーバへのアクセスがPOSTである場合、リダイレクト後のGETではパラメータが失われてしまう。 その場合、POSTパラメータをセッションに保持する等の対処が必要となるため、注意すること。 本ガイドラインでは、具体的な対処方法の説明については割愛する。


9.9.2.5.2. JavaScriptを使用したリソースへのアクセス

インプリシットグラントでは、Webブラウザなどのユーザエージェントが認可サーバに対して認可の要求を行う。

そのため、通常、インプリシットグラントはWebブラウザ上で動作するJavaScriptなどで利用されるが、 Spring Security OAuthではJavaScriptライブラリを提供していないため、インプリシットグラントを使用する場合は 独自にクライアントを実装する必要がある。

本ガイドラインでは、インプリシットグラント向けのクライアントの実装として、JavaScriptを使用してリソースサーバから JSON形式のデータを取得し、画面に表示させる方法を説明する。

Note

以降に説明する実装例ではJavaScriptライブラリとしてjQueryを使用する。 jsファイルと合わせて src/main/webapp 配下に格納されていることを想定している。

OAuth 2.0機能を独自に実装したAPI例を示す。

  • oauth2.js
var oauth2Func = (function(exp, $) {
    "use strict";

    var
        config = {},
        DEFAULT_LIFETIME = 3600;

    var uuid = function() {
        return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
            var r = Math.random()*16|0, v = c == "x" ? r : (r&0x3|0x8);
            return v.toString(16);
        });
    };

    var encodeURL= function(url, params) {
        var res = url;
        var k, i = 0;
        for(k in params) {
            res += (i++ === 0 ? "?" : "&") + encodeURIComponent(k) + "=" + encodeURIComponent(params[k]);
        }
        return res;
    };

    var epoch = function() {
        return Math.round(new Date().getTime()/1000.0);
    };

    var parseQueryString = function (qs) {
        var e,
            a = /\+/g,
            r = /([^&;=]+)=?([^&;]*)/g,
            d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
            q = qs,
            urlParams = {};

        while (e = r.exec(q)) {
           urlParams[d(e[1])] = d(e[2]);
        }

        return urlParams;
    };

    var saveState = function(state, obj) {
        localStorage.setItem("state-" + state, JSON.stringify(obj));
    };

    var getState = function(state) {
        var obj = JSON.parse(localStorage.getItem("state-" + state));
        localStorage.removeItem("state-" + state);
        return obj;
    };

    var hasScope = function(token, scope) {
        if (!token.scopes) return false;
        var i;
        for(i = 0; i < token.scopes.length; i++) {
            if (token.scopes[i] === scope) return true;
        }
        return false;
    };

    var filterTokens = function(tokens, scopes) { // (1)
        if (!scopes) scopes = [];

        var i, j,
        result = [],
        now = epoch(),
        usethis;
        for(i = 0; i < tokens.length; i++) {
            usethis = true;

            if (tokens[i].expires && tokens[i].expires < (now+1)) usethis = false;

            for(j = 0; j < scopes.length; j++) {
                if (!hasScope(tokens[i], scopes[j])) usethis = false;
            }

            if (usethis) result.push(tokens[i]);
        }
        return result;
    };

    var saveTokens = function(provider, tokens) {
        localStorage.setItem("tokens-" + provider, JSON.stringify(tokens));
    };

    var getTokens = function(provider) {
        var tokens = JSON.parse(localStorage.getItem("tokens-" + provider));
        if (!tokens) tokens = [];

        return tokens;
    };

    var wipeTokens = function(provider) {
        localStorage.removeItem("tokens-" + provider);
    };

    var saveToken = function(provider, token) {
        var tokens = getTokens(provider);
        tokens = filterTokens(tokens);
        tokens.push(token);
        saveTokens(provider, tokens);
    };

    var getToken = function(provider, scopes) {
        var tokens = getTokens(provider);
        tokens = filterTokens(tokens, scopes);
        if (tokens.length < 1) return null;
        return tokens[0];
    };

    var sendAuthRequest = function(providerId, scopes) { // (2)
        if (!config[providerId]) throw "Could not find configuration for provider " + providerId;
        var co = config[providerId];

        var state = uuid();
        var request = {
            "response_type": "token"
        };
        request.state = state;

        if (co["redirectUrl"]) {
            request["redirect_uri"] = co["redirectUrl"];
        }
        if (co["clientId"]) {
            request["client_id"] = co["clientId"];
        }
        if (scopes) {
            request["scope"] = scopes.join(" ");
        }

        var authurl = encodeURL(co.authorization, request);

        if (window.location.hash) {
            request["restoreHash"] = window.location.hash;
        }
        request["providerId"] = providerId;
        if (scopes) {
            request["scopes"] = scopes;
        }

        saveState(state, request);
        redirect(authurl);

    };

    var checkForToken = function(providerId) { // (3)
        var h = window.location.hash;

        if (h.length < 2) return true;

        if (h.indexOf("error") > 0) { // (4)
            h = h.substring(1);
            var errorinfo = parseQueryString(h);
            handleError(providerId, errorinfo);
            return false;
        }

        if (h.indexOf("access_token") === -1) {
            return true;
        }
        h = h.substring(1);
        var atoken = parseQueryString(h);

        if (!atoken.state) {
            return true;
        }

        var state = getState(atoken.state);
        if (!state) throw "Could not retrieve state";
        if (!state.providerId) throw "Could not get providerId from state";
        if (!config[state.providerId]) throw "Could not retrieve config for this provider.";

        var now = epoch();
        if (atoken["expires_in"]) {
            atoken["expires"] = now + parseInt(atoken["expires_in"]);
        } else {
            atoken["expires"] = now + DEFAULT_LIFETIME;
        }

        if (atoken["scope"]) {
            atoken["scopes"] = atoken["scope"].split(" ");
        } else if (state["scopes"]) {
            atoken["scopes"] = state["scopes"];
        }

        saveToken(state.providerId, atoken);

        if (state.restoreHash) {
            window.location.hash = state.restoreHash;
        } else {
            window.location.hash = "";
        }
        return true;
    };

    var handleError = function(providerId, cause) { // (5)
        if (!config[providerId]) throw "Could not retrieve config for this provider.";

        var co = config[providerId];
        var errorDetail = cause["error"];

        // redirect error page
        if(co["errRedirectUrl"]) {
            redirect(co["errRedirectUrl"] + "/" + errorDetail);
        } else {
            alert("Access Error. cause: " + errorDetail);
        }
    };


    var redirect = function(url) {
        window.location = url;
    };

    var initialize = function(c) {
        config = c;
        try {
            var key, providerId;
            for(key in c) {
                    providerId = key;
            }
            return checkForToken(providerId);
        } catch(e) {
            console.log("Error when retrieving token from hash: " + e);
            window.location.hash = "";
            return false;
        }
    };

    var clearTokens = function() {
        var key;
        for(key in config) {
            wipeTokens(key);
        }
    };

    var oajax = function(settings) { // (6)
        var providerId = settings.providerId;
        var scopes = settings.scopes;
        var token = getToken(providerId, scopes);

        if (!token) {
            sendAuthRequest(providerId, scopes);
            return;
        }

        if (!settings.headers) settings.headers = {};
        settings.headers["Authorization"] = "Bearer " + token["access_token"];

        $.ajax(settings);
    };

    return {
        initialize: function(config) {
            return initialize(config);
        },
        clearTokens: function() {
            return clearTokens();
        },
        oajax: function(settings) {
            return oajax(settings);
        }
    };

})(window, jQuery);
項番 説明
(1)
アクセストークンの有効期限とスコープの可否をチェックする関数。
クライントが使用可能なアクセストークンを返却する。
(2)
認可サーバに対して認可を要求する関数。
コンフィギュレーション情報より必要パラメータを取得し、リクエストを作成する。
(3)
認可の応答よりアクセストークンを取得する関数。
アクセストークンが取得できた場合、ローカルストレージに情報を格納する。
(4)
認可の結果、エラーが返却された場合エラー処理としてhandleErrorを呼び出す。
(5)
認可時のエラーを処理する関数。
本ガイドラインでは、エラー処理の実装例としてコンフィギュレーション情報にて指定されている エラー時のリダイレクト先URLへのリダイレクトを行っている。
(6)
リソースサーバに対してリソースへのアクセスを要求する関数。
ローカルストレージよりアクセストークンを取得し、jQueryのajax関数を用いてリソースサーバへリクエストを行う。

以下にクライアントの実装例を示す。

  • todoList.jsp
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/vendor/jquery/jquery.js"></script> <!-- (1) -->
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/app/js/oth2-implicit.js"></script> <!-- (1) -->
<script type="text/javascript">
"use strict";

$(document).ready(function() {
    var result = oauth2Func.initialize({ // (2)
        "todo" : { // (3)
            clientId : "client", // (4)
            redirectUrl : "${client.serverUrl}/oth2/api", // (5)
            errRedirectUrl : "${client.serverUrl}/oth2/error", // (6)
            authorization : "${auth.serverUrl}/oth2/authorize" // (7)
        }
    });

    if (result) {
        oauth2Func.oajax({ // (8)
            url : "${resource.serverUrl}/api/v1/todos", // (9)
            providerId : "todo",  // (10)
            scopes : [ "READ" ],  // (11)
            dataType : "json",    // (12)
            type : "GET",  // (13)
            success : function(data) { // (14)
                $("#message").text(JSON.stringify(data));
            },
            error : function() {
                oauth2Func.clearTokens();
            }
        });
    } else {
        oauth2Func.clearTokens(); // (15)
    }


};

</script>
<div id="wrapper">
    <p id="message"></p>
</div>
項番 説明
(1)
後述する、OAuth 2.0機能を独自に実装したjsファイル、jQueryをそれぞれ格納したパスを指定する。
(2)
認可要求に使用するコンフィギュレーション情報を定義し、初期化する。
(3)
クライアント別にコンフィギュレーション情報を区別するための識別子として一意な値を指定する。
後述するリソースへのアクセス処理では、本項目をキーにコンフィギュレーション情報を管理・取得する。
(4)
クライアントを識別するIDを指定する。
(5)
認可サーバのリソースオーナ認証後にクライアントをリダイレクトさせるURLを指定する。
本実装例ではURLのパラメータをControllerから受け取ることを想定している。
(6)
認可応答として認可サーバよりエラーを受信した場合にリダイレクトさせるURLを指定する。
本実装例ではURLのパラメータをControllerから受け取ることを想定している。
本ガイドラインではエラー受信時の実装例として、クライアントの画面にリダイレクトしエラー画面を 表示させる方法を示す。なお、クライアントのControllerについては説明を割愛する。
(7)
認可サーバの認可エンドポイントURLを指定する。
本実装例ではURLのパラメータをControllerから受け取ることを想定している。
(8)
リソースへのアクセスを実行する。
(9)
リソースサーバのアクセス先URLを指定する。
(10)
参照するコンフィギュレーション情報の識別子を指定する。
(11)
リソースへ要求するスコープを指定する。設定値は大文字、小文字を区別する。
(12)
レスポンスの型を指定する。
(13)
メソッドGETでリソースサーバへアクセスする。
(14)
処理成功時に行う処理を指定する。messageには処理成功時のレスポンスが格納される。
(15)
アクセストークンをクリアする。

Todo

TBD

JavaScriptで作られたクライアントから同一ドメインでない認可サーバやリソースサーバへのアクセスを行う場合、認可サーバやリソースサーバでCross-Origin Resource Sharingのサポートが必要になる。

詳細については、次版以降に記載する予定である。

9.9.2.5.3. トークンの取り消し(クライアントサーバ)

発行したアクセストークンの取り消しの実装方法について説明する。

アクセストークンの取り消しは、認可サーバにリクエストを行い、TokenStoreからアクセストークンの削除を行う。認可サーバへのリクエスト時はクライアントのBasic認証を行うため、Basic認証用のリクエストヘッダを設定する。
認可サーバのトークンの取り消しについては トークンの取り消し(認可サーバ)を参照されたい。
クライアントサーバはアクセストークンの取り消しを認可サーバにリクエスト後、OAuth2RestTemplateで保持しているアクセストークンを削除する必要がある。

以下に、実装例を示す。

まず、認可サーバにトークン取り消し要求を行うためのRestTemplateの設定を設定ファイルに追記する。

  • oauth2-client.xml
<!-- (1) -->
<bean id="revokeRestTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="interceptors">
        <list>
            <ref bean="basicAuthInterceptor" />
        </list>
    </property>
</bean>

<bean id="basicAuthInterceptor" class="com.example.oauth2.client.restclient.BasicAuthInterceptor" />
項番 説明
(1)
認可サーバにトークン取り消し要求を行うためのRestTemplateをBean定義する。
Basic認証用のリクエストヘッダを設定するため、interceptorsプロパティにClientHttpRequestInterceptorの実装クラスを指定する。
Basic認証用のリクエストヘッダを設定するClientHttpRequestInterceptorの実装クラスの実装方法についてはBasic認証用のリクエストヘッダ設定処理を参照されたい。

トークンの取り消しを行うサービスクラスのインタフェースと実装クラスを作成する。

  • RevokeTokenClientService.java
public interface RevokeTokenClientService {

    String revokeToken();

}
  • RevokeTokenClientServiceImpl.java
@Service
public class RevokeTokenClientServiceImpl implements RevokeTokenClientService {

    @Value("${auth.serverUrl}/api/v1/oth2/tokens/revoke")
    String revokeTokenUrl; // (1)

    @Inject
    @Named("todoAuthCodeGrantResourceRestTemplate")
    OAuth2RestOperations oauth2RestOperations; // (2)

    @Inject
    @Named("revokeRestTemplate")
    RestOperations revokeRestOperations; // (3)

    @Override
    public String revokeToken() {

        String token = getTokenValue(oauth2RestOperations);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> variables = new LinkedMultiValueMap<String, String>();
        variables.add("token", token);

        String result = revokeRestOperations.postForObject(revokeTokenUrl,
            new HttpEntity<MultiValueMap<String, String>>(variables, headers),
            String.class); // (4)
        // (5)
        if ("success".equals(result)) {
            initContextToken(oauth2RestOperations);
        }
        return result;
    }

    // (6)
    private String getTokenValue(OAuth2RestOperations oauth2RestOperations) {
        String tokenValue = "";
        OAuth2AccessToken token = oauth2RestOperations.getAccessToken();
        if (token != null) {
            tokenValue = token.getValue();
        }
        return tokenValue;
    }

    // (7)
    private void initContextToken(OAuth2RestOperations oauth2RestOperations) {
        oauth2RestOperations.getOAuth2ClientContext().setAccessToken(null);
    }
}
項番 説明
(1)
アクセストークンの取り消しを認可サーバに依頼する際に使用するURL。
(2)
取り消しを行うアクセストークンを保持しているOAuth2RestTemplateをインジェクションする。
(3)
アクセストークンの取り消しを行うRestTemplateをインジェクションする。
(4)
認可サーバにアクセストークンの取り消しを行うために、RESTでメソッドPOSTでアクセスする。
取り消しを行うアクセストークンの値を認可サーバに渡すためにリクエストパラメータに設定する。
アクセストークンは(5)で定義しているgetTokenValueメソッドにOAuth2RestOperationsを渡して取得する。
(5)
認可サーバの処理結果を判定し、正常の場合のみOAuth2RestOperationsで保持しているアクセストークンを削除する。
アクセストークンの削除は(6)で定義しているinitContextTokenメソッドにアクセストークンを保持しているOAuth2RestOperationsを渡して削除する。
(6)
OAuth2RestOperationsで保持しているアクセストークンを取得するメソッド。
パラメータとして渡されたOAuth2RestOperationsgetAccessTokenメソッドを呼び出すことでアクセストークンを取得し、返却する。
(7)
OAuth2RestOperationsで保持しているアクセストークンを削除するメソッド。
パラメータとして渡されたOAuth2RestOperationssetAccessTokenメソッドにnullを渡すことでアクセストークンを削除する。
クライアントは上記で作成したサービスをアクセストークンが不要になったタイミングで呼び出すことでトークンの取り消しを行う。
Spring Security OAuthのデフォルト実装ではセッションスコープでアクセストークンを保持するため、クライアントのユーザがログアウトした場合やセッションタイムアウトによってセッションが破棄されるタイミングでトークンの取り消しを行うことが考えられる。

9.9.3. How to extend

9.9.3.1. エンドポイントを介した認可サーバとリソースサーバの連携

リソースサーバと認可サーバは、認可サーバのチェックトークンエンドポイントにリソースサーバからHTTPアクセスを行うことで連携が可能である。

チェックトークンエンドポイントは、リソースサーバからアクセストークンの値を受け取り、リソースサーバの代わりにトークンの検証を行うエンドポイントである。 チェックトークンエンドポイントはTokenServicesを用いてアクセストークンの取得、検証を行い、検証に問題がなければアクセストークンに紐づく情報をリソースサーバに渡す。

9.9.3.1.1. HTTPアクセスを介した連携

リソースサーバが認可サーバのチェックトークンエンドポイントにアクセスするためにはTokenServicesの実装クラスであるorg.springframework.security.oauth2.provider.token.RemoteTokenServicesを使用する。
RemoteTokenServicesorg.springframework.web.client.RestTemplateを用いてチェックトークンエンドポイントへHTTPアクセスを行い、アクセストークンに紐づく情報を取得する。
アクセストークンの検証はチェックトークンエンドポイントが行うため、RemoteTokenServicesでは行わない。

以下に、実装例を示す。

まず、認可サーバにトークンを検証するためのorg.springframework.security.oauth2.provider.endpoint.CheckTokenEndpointクラスをコンポーネントとして登録する設定を行う。

  • oauth2-auth.xml
<sec:http pattern="/oth2/check-token" security="none" />  <!-- (1) -->

<oauth2:authorization-server
     client-details-service-ref="clientDetailsService"
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize"
     user-approval-handler-ref="userApprovalHandler"
     token-services-ref="tokenServices"
     check-token-enabled="true"
     check-token-endpoint-url="/oth2/check-token">  <!-- (2) -->
    <oauth2:authorization-code />
    <oauth2:implicit />
    <oauth2:refresh-token />
    <oauth2:client-credentials />
    <oauth2:password />
</oauth2:authorization-server>
項番 説明
(1)
本ガイドラインでは、チェックトークンエンドポイントへのアクセスはリソースサーバのみ許可するネットワーク構成になっていることを前提とし、Spring Securityのセキュリティ機能(Security Filter)の適用対象外とする。
他のセキュリティ設定よりも先に評価されるように、設定ファイルの先頭に記載する必要がある。
(2)
<oauth2:authorization-server>タグcheck-token-enabled属性にtrueを指定することでCheckTokenEndpointがコンポーネントとして登録される。
<oauth2:authorization-server>タグcheck-token-endpoint-url属性にCheckTokenEndpointのURLを指定する。指定しない場合はデフォルト値である “/oauth/check_token” が指定される。

Warning

チェックトークンエンドポイントのセキュリティ対策

  • チェックトークンエンドポイントは、認可サーバとリソースサーバ間のみアクセス可能とし、オープンなネットワークからアクセスできないようにする。 チェックトークンエンドポイントを公開する場合は、適切なセキュリティ対策を行う必要がある。

Note

Spring Security OAuthのバージョン2.0.12以前を使用する場合、check-token-endpoint-urlは、authorization-endpoint-urlまたはtoken-endpoint-urlを指定していない場合は反映されないため注意が必要である。 これは以下のissueで取り上げられており、バージョン2.0.13で改修される予定である。

https://github.com/spring-projects/spring-security-oauth/issues/897

Note

TokenServicesは、共有DBを介して連携させる場合と同様にDefaultTokenServicesを使用する。 TokenServicesが参照するTokenStoreはアプリケーションの要件に合ったインタフェースの実装クラスを使用する。


リソースサーバの設定ファイルにTokenServicesとしてorg.springframework.security.oauth2.provider.token.RemoteTokenServicesを使用する設定を行う。

  • oauth2-resource.xml
<bean id="tokenServices"
    class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
    <property name="checkTokenEndpointUrl" value="${auth.serverUrl}/oth2/check-token" />  <!-- (1) -->
</bean>
項番 説明
(1)
認可サーバのチェックトークンエンドポイントにアクセスしてアクセストークンに紐付く情報を取得できるよう、RemoteTokenServicesをBean定義する。
チェックトークンエンドポイントにアクセスするためのURLをcheckTokenEndpointUrlプロパティに設定する。

Note

チェックトークンエンドポイントでアクセストークンの検証エラーが発生した場合、RemoteTokenServicesにHTTPステータスコード400(Bad Request)が返却される。 RestTemplateのデフォルト実装ではHTTPステータスコード400(Bad Request)が返却された場合、エラーハンドリングを行いクライアントエラー例外を発生させる。 RemoteTokenServicesがデフォルトで使用するRestTemplateはアクセストークンの検証エラーをクライアントサーバに連携するために、レスポンスのHTTPステータスコードが400の場合はエラーハンドリングしないよう拡張されている。 RemoteTokenServicesRestTemplateをインジェクションした場合、この拡張が適用されなくなるため注意が必要である。


RemoteTokenServicesをリソースサーバで使用した場合、ハンドラメソッドでアノテーション@AuthenticationPrincipalStringに引数アノテーションとして指定することでリソースオーナのユーザ名が取得できる。

実装例は以下のようになる。

@RestController
@RequestMapping("api")
public class TodoRestController {

    // omitted

    @RequestMapping(value = "todos", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public Collection<Todo> list(@AuthenticationPrincipal String userName) { // (1)

        // omitted

    }
}
項番 説明
(1)
引数 userNameにリソースオーナのユーザ名が格納される。

9.9.3.1.2. DefaultAccessTokenConverterの拡張

認可サーバとリソースサーバ間でDBを共有しない構成の場合でも、ユーザ情報に付随する項目を認可サーバからリソースサーバに連携したいというケースはありうるが、 リソースサーバの使用するTokenServicesとしてRemoteTokenServicesを使用する場合、リソースサーバのハンドラメソッド引数のアノテーション@AuthenticationPrincipalではユーザ名以外の情報を取得することができない。

そこで、ここではRemoteTokenServicesを使用してアクセストークンを連携するときに使用するクラスであるorg.springframework.security.oauth2.provider.token.DefaultAccessTokenConverterを拡張し、 ユーザ名以外の情報をリソースサーバに連携する例を示す。

9.9.3.1.2.1. DefaultAccessTokenConverterとは

RemoteTokenServicesを使用したアクセストークンの連携では、RestTemplateを使用してリソースサーバから認可サーバに対してアクセストークン値に紐づくリソースオーナ、クライアントの認証情報を要求し、結果をMapとして取得する。 このとき、DefaultAccessTokenConverterは、認可サーバでは認証情報からMapへ、リソースサーバではMapから認証情報へ変換するためのコンバーターとしての役割を持つ。

これを利用し、認可サーバからの返却値をMapに追加するようDefaultAccessTokenConverterの拡張を行うことで、認可サーバ、リソースサーバ間で連携するパラメータをカスタマイズすることが出来るようになる。

以下の説明では、認可サーバ側でDefaultAccessTokenConverterと、そのプロパティであるDefaultUserAuthenticationConverterをそれぞれカスタマイズすることで、ユーザ情報に関連した独自項目と、それ以外の独自項目を連携する例を示す。

9.9.3.1.2.2. 認可サーバの実装

認可サーバ側の実装方法について説明する。

まず、ユーザ情報に関連した独自項目を追加するため、DefaultUserAuthenticationConverterを拡張する。

  • AuthCustomUserTokenConverter.java
public class CustomUserTokenConverter extends DefaultUserAuthenticationConverter {
    @Override
    public Map<String, ?> convertUserAuthentication(
            Authentication authentication) {
        Map<String, Object> response = new LinkedHashMap<String, Object>();
        response.put(USERNAME, authentication.getName());

        if (authentication.getAuthorities() != null &&
                !authentication.getAuthorities().isEmpty()) {
            response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(
                    authentication.getAuthorities()));
        }
        response.put("user_additional_key", "user_additional_value"); // (1)
        return response;
    }
}
項番 説明
(1)
リソースサーバに引き渡す情報を独自項目user_additional_keyとして定義し、responseに設定する。
responseに設定した情報は、チェックトークンエンドポイントのトークン検証時にレスポンスBODYとしてJSON形式でリソースサーバへ返却される。

次に、ユーザ情報以外の独自項目を追加するため、DefaultAccessTokenConverterを拡張する。

  • CustomAccessTokenConverter.java
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

    @Override
    public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {

        @SuppressWarnings("unchecked")
        Map<String, Object> response = (Map<String, Object>) super.convertAccessToken(token, authentication);
        response.put("client_additional_key","client_additional_value"); // (1)
        // omitted

        return response;
    }
}
項番 説明
(1)
リソースサーバに引き渡す情報を独自項目client_additional_keyとして定義し、responseに設定する。
responseに設定した情報は、チェックトークンエンドポイントのトークン検証時にレスポンスBODYとしてJSON形式でリソースサーバへ返却される。

認可サーバの設定ファイルに、作成したCustomUserTokenConverterCustomAccessTokenConverterの設定を行う。

  • oauth2-auth.xml
<oauth2:authorization-server
     client-details-service-ref="clientDetailsService"
     token-endpoint-url="/oth2/token"
     authorization-endpoint-url="/oth2/authorize"
     user-approval-handler-ref="userApprovalHandler"
     token-services-ref="tokenServices"
     check-token-endpoint-url="/oth2/check-token">  <!-- (1) -->
    <oauth2:authorization-code />
    <oauth2:implicit />
    <oauth2:refresh-token />
    <oauth2:client-credentials />
    <oauth2:password />
</oauth2:authorization-server>

<bean id="checkTokenEndpoint"
    class="org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint">  <!-- (2) -->
    <constructor-arg ref="tokenServices" />
    <property name="accessTokenConverter" ref="accessTokenConverter" />
</bean>

<bean id="accessTokenConverter"
    class="com.example.oauth2.auth.converter.CustomAccessTokenConverter"/>  <!-- (3) -->
    <property name="userTokenConverter">
        <bean
            class="com.example.oauth2.auth.converter.CustomUserTokenConverter" />
    </property>
</bean>
項番 説明
(1)
<oauth2:authorization-server>タグcheck-token-endpoint-url属性に(2)で定義しているCheckTokenEndpointのURLを指定する。指定しない場合はデフォルト値である “/oauth/check_token” が指定される。
CheckTokenEndpointのBean定義を(2)で独自で行っているため、<oauth2:authorization-server>タグcheck-token-enabled属性は指定しない。
(2)
CheckTokenEndpointをBean定義する。
accessTokenConverterプロパティに(2)で定義しているCustomAccessTokenConverterのBeanを指定することでCustomAccessTokenConverterCustomUserTokenConverterに追加した独自項目をリソースサーバに連携するようになる。
(3)
DefaultAccessTokenConverterを拡張したCustomAccessTokenConverterをBean定義する。
userTokenConverterプロパティにDefaultUserAuthenticationConverterを拡張したCustomUserTokenConverterのBeanを指定する。
9.9.3.1.2.3. リソースサーバの実装

リソースサーバに、認可サーバから連携された情報をハンドラメソッド引数のアノテーション@AuthenticationPrincipalで取得できるよう機能の追加を行う。 まず、アノテーション@AuthenticationPrincipalで取得する情報を保持するOauthUserクラスを作成する。

  • OauthUser.java
public class OauthUser implements Serializable{

    private static final long serialVersionUID = 1L;

    private String username;

    private String userAdditionalValue;

    private String clientAdditionalValue;

    // omitted

    public User(String username, String additionalValue){
        this.username = username;
        this.additionalValue = additionalValue;
    }

    // Getters and Setters are omitted

}

アノテーション@AuthenticationPrincipalでユーザ情報が取得できるように設定を行うorg.springframework.security.oauth2.provider.token.DefaultAccessTokenConverterを拡張し、ユーザ名以外の情報も取得できるよう機能の追加を行う。

  • CustomUserTokenConverter.java
public class CustomUserTokenConverter extends DefaultUserAuthenticationConverter{

    private Collection<? extends GrantedAuthority> defaultAuthorities; // (1)

    public void setDefaultAuthorities(String[] defaultAuthorities) {
        this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
                .arrayToCommaDelimitedString(defaultAuthorities));
    }

     // (2)
    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USERNAME)) {
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            OauthUser user = new OauthUser(
                    (String) map.get(USERNAME),
                    (String) map.get("user_additional_key"),
                    (String) map.get("client_additional_key"),
                    (String) map.get("client_id")); // (3)

            // omitted

            return new UsernamePasswordAuthenticationToken(user, "N/A", authorities); // (4)
        }
        return null;
    }

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
        if (!map.containsKey(AUTHORITIES)) {
            return defaultAuthorities;
        }
        Object authorities = map.get(AUTHORITIES);
        if (authorities instanceof String) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
        }
        if (authorities instanceof Collection) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
                    .collectionToCommaDelimitedString((Collection<?>) authorities));
        }
        throw new IllegalArgumentException("Authorities must be either a String or a Collection");
    }
}
項番 説明
(1)
DefaultUserAuthenticationConverterに実装されているgetAuthoritiesメソッドがprivateで定義されているため、getAuthoritiesメソッドで使用されるdefaultAuthoritiesgetAuthoritiesメソッドを実装する。
(2)
認可サーバから連携された情報から認証情報を抽出するメソッド。
(3)
認可サーバから連携された情報をOauthUserクラスに設定する。
(4)
UsernamePasswordAuthenticationTokenの第一引数にOauthUserを設定することで、認可サーバから連携された情報をアノテーション@AuthenticationPrincipalで取得できるようになる。

リソースサーバの設定ファイルに、CustomUserTokenConverterの設定を行う。

  • oauth2-resource.xml
<bean id="tokenServices"
    class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
    <property name="checkTokenEndpointUrl" value="${auth.serverUrl}/oth2/check-token" />
    <property name="accessTokenConverter" ref="accessTokenConverter" />
</bean>

<bean id="accessTokenConverter"
    class="org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter">
    <property name="userTokenConverter">
        <bean class="com.example.oauth2.resource.converter.CustomUserTokenConverter"/>  <!-- (1) -->
    </property>
</bean>
項番 説明
(1)
accessTokenConverteruserTokenConverterプロパティにCustomUserTokenConverterクラスを指定することで、ユーザ名以外の情報をアノテーション@AuthenticationPrincipalで取得できるようになる。