Tagbangers Blog

OAuth2 Authentication in Microservice Web Application

## OAuth2

巨大なシステムをMicroservice化する場合、
開発者は機能の種類と関連性に基づいてサービスを分割する。
もし各サービスに独立な認証機能を持たせると、
利用者に大量のアカウントとパスワードを覚えさせなければならない、
一方で、もし全てのサービスに
共通なアカウントとパスワードでの認証機能を持たせると、
ユーザーの機密認証データが頻繁にネットワーク上に流され、
万が一何らかのシステム脆弱性に関わる原因で漏れた場合、
そのユーザーの認証情報がいつでもどこでも悪用される可能性がある。
それらを避けるため、専用の認証サーバーを設け、
認証結果を共有する中心化認証機能(Single Sign-On)が必要である。


そのソリューションとして、OAuth2フレームワークが広く使われている。
OAuth2は、特定の言語、実装、ライブラリなどに依存しない規約フレームワークであり、
認証と承認に関わるコンセプトと役柄を定義した。
OAuth2には、データと操作機能を持っている「リソースサーバー」、
そのリソース(データと操作機能)を使用する権限を持っている「ユーザー」、
ユーザーの代わりにリソースにアクセスする「クライアント」、
リソースサーバーにクライアントのアクセスを許可させるためにtokenを発行する「承認サーバー(authorization server)」を含む。
その中に、クライアントはリソースサーバーへリクエストする際に、
クライアント自身の身分を証明する認証情報が必要であり、
一方ユーザーの認証情報が不要な場合もある。
その原因は、OAuth2の認証方式は多数存在するからである。

## Grant types in OAuth2


OAuth2の認証方式は「grant(s)」と呼ぶ。
最も慣用されるgrantsは「authorization code」、「password」、
「refresh token」、「client credentials」、「JWT bearer」であり、
サービスの要件に応じて適切なgrantを選ぶことが極めて重要である。

### Authorization code grant type


Authorization code認証方式では、
ユーザーはaccess token(以下「token」という。)を持っていない状態でクライアントにてリソースにアクセスする時、
まずクライアントに承認サーバーへリダイレクトされ、
直接に承認サーバーのページに認証情報を入力する。
もし通過したら承認サーバーはユーザーにauthorization codeを発行し、
クライアントはユーザーからもらったauthorization codeと、
クライアント自身の認証情報にて承認サーバーにtokenを申請する。
最後にクライアントはtokenをリクエストのheaderに入れ、
リソースサーバーへリクエストして操作する。

ここで注意すべきことは、
クライアントはユーザーの認証情報に接触しないことである。
つまり、それは一般的に、
承認サーバーの所有者にとって、
クライアントサーバーの所有者は信用できない第三者の組織である場合に使う認証方式であろう。

### Password grant type


一方で、Password認証方式では、
クライアントは直接にユーザーの認証情報を預かり、
承認サーバーにてユーザーとクライアント自身の認証情報をtokenに交換する。
クライアントと承認サーバーの所有者は同じ組織であれば、
その認証方式の方がシンプルであろう。

### Client credentials grant type


Client credentials認証方式はPassword認証方式と似ている、
但し、Client credentials認証方式ではユーザー認証方式が不要であり、
クライアント認証情報だけでtokenを取得する。
ユーザーに関わらないリソースに対してはその認証方式がナイスであろう。

### Refresh token grant type


ここまで挙げた認証方式では最後にaccess tokenが発行される。
Access tokenに普通は使用期限があり、切れたら無効になる。
もし使用期限がなければ何が発生する?
そのaccess tokenが永遠に有効になり、
HTTP headerに入れられてネット上に頻繫に流れる。
万が一盗まれら、ユーザー認証情報と同様に悪用される可能性がある。
それを避けるため、なるべく短い使用期限を設けた方が安全であろう。

Access tokenの使用期限が切れた場合、
新しいaccess tokenを申請する必要がある。
認証方式は再びその前使用した方式を繰り返すことができるが、
サービス利用中のユーザーにとって、
再三認証情報を入力させられることが悩ましいであろう。
Password認証方式であれば、
クライアントにユーザーの認証情報を保存することを通じて、
重複の入力を解決する、と考えている開発者があるかもしれないが、
それは極めて危険であり、(特に平文で保存)すべきではない!
OAuth2が更に簡単で安全な方法を提供してくれた、
それはrefresh token認証方式である。

承認サーバーはaccess tokenを発行すると同時にrefresh tokenを発行し、
クライアントはrefresh tokenを保存する。
Access tokenが切れた(或いは切れそうな)際に、
クライアントは承認サーバーへrefresh tokenとクライアント認証情報を含んだリクエストを送り、
新しいaccess tokenとrefresh tokenをもらう。
それで、access tokenが自動的に更新され、
次のaccess token期限切れの時にも同様な処理を行う。

## Validating token


ここまでは四つのOAuth2認証方式を紹介した、
認証の結果として、
クライアントは承認サーバーが発行したtokenを、
リソースサーバーへの全てのリクエストのHTTP headerに入れる。
しかし、そのままであると、
リソースサーバーはtokenの意味を識別できない、
開発者はリソースサーバーにtokenを検証する機能を持たせる必要がある。
認証方式と同様に、検証の方法も複数存在する、
ここで三つの検証方法を挙げる。

### Blackboarding


最もシンプルなtoken検証方法は、
承認サーバーは発行したtokenをデータベースに保存し、
直接にデータベースをリソースサーバーとシェアすることであろう。
この方法は「blackboarding」と呼ぶ。

しかし、この方法では、
データベースがシステムのボトルネックになる可能性がある。
Microservicesからの大量の接続負荷はデータベースの応答、
延いては各サービスの応答を滞らせる。
それを解消するために、
読み取りと書き込みの分離を実現したデータベースクラスター
を導入する解決策が可能であろう。

### Token introspection


承認サーバーに検証API endpointを公開させることで、
tokenデータベースのシェアを避けるという方法も存在する。
リソースサーバーは全てのリクエストに対して、
HTTP headerの中にあるtokenを検証API endpointに送る。
承認サーバーは有効なtokenに対してそのtokenの所有者の権限を返す。
この方法は「introspection」と呼ぶ。

この方法を導入するとき、三つ注意すべきことがある。
一つ目は、ネットワーク不具合が検証の応答、
延いてはサービスの応答に影響することである。
不要な通信量を軽減するために、
一回検証したtokenとその有効期限をリソースサーバー自身のデータベース又はキャッシュに保存し、
識別できないtokenのみに対して検証API endpointを呼ぶ、
という解決策があるであろう。
しかし、この方法を適応すると、
承認サーバーが期限までの途中でtokenを無効化することができなくなる。

二つ目は、検証処理は承認サーバーに負荷をかけることである、
夥しい数のリクエストが来た場合、性能に影響する。
それを解決するには、
tokenの発行と検証を分離し、
token検証用サーバーを設ける方法があるであろう。

三つ目は、
この検証API endpointはリソースサーバーのみに提供することである。
検証APIがもしDOS攻撃されると、全てのリソースサーバーに影響してしまう。
そのため、
ファイアウォールで保護した内部ネットワークに設置し、
或いは内部認証を加えた方が安全であろう。

### Self-encoded access tokens


普通のtokenのコンテンツにはtokenを唯一に識別するtoken IDしかない。
tokenの検証は、token IDを利用し、データベースや外部サーバーと対話する。
一方、必要なユーザー情報をtokenに入れることで、
リソースサーバー自身で検証する方法もある。
しかし、tokenにあるユーザー情報が傍受者に改ざんされるリスクが存在する。
tokenコンテンツの完全性を確保するために、
デジタル署名を利用した認証方式がある、
それは、上記の「JWT bearer」というOAuth2認証方式である。

## JWT


JWT(JSON Web Token)には、header、body(payload)、signatureという三つの部分がある。
一般的に、headerにはtokenのタイプ即ちJWTと、
デジタル署名に用いるアルゴリズムがある。
bodyには、ユーザーIDやユーザー権限など、
リソースサーバーのリソース権限マネジメントに必要な情報である。
headerとbodyは名前の通りJSONフォーマットではあるが、
Base64に変換された。
最後のsignatureは、
headerとbodyの元に、
デジタル署名アルゴリズムにて生成したデジタル署名である。
承認サーバーはキーを使ってtokenにデジタル署名し、
クライアントに渡す。
リソースサーバーはクライアントからのtokenに対して、
検証用のキーを使ってsignatureを復元し、
headerとbodyの内容と比較する。
もし傍受者にコンテンツを改ざんされ場合、
signatureから復元したデータはheaderとbodyの内容に一致しない、
その時リソースサーバーはリクエストを拒否する。
つまり、内容がマッチすれば、
tokenが改ざんされていないことが確保できる。

キーがなくても、tokenのコンテンツを見ることができる、
ではあるが、正確なキーを持たないと、変えられない。
何故かというと、別のキーでデジタル署名したsignatureは、
リソースサーバーの持っているキーで復元できないからである。

デジタル署名用のキーは、
対称キーと非対称キー二種類がある、
対称キーが使用できるかどうかは要件による。

### Symmetric key

承認サーバーがtokenをデジタル署名するとき使うキーと、
リソースサーバーがtokenを検証するとき使うキーは同一である場合、
対称キーと呼ぶ。
対称キーは単純なバイト列であり、
一般的には長さを指定してアルゴリズムでランダムに生成するのである。
実際に、安全性のため、258バイト以上の長さが薦められている。
対称キーは非常に重要な機密であり、
任意のtokenにデジタル署名してもリソースサーバーに信頼される。
もし漏れたら第三者に身分を偽造されるリスクがある、
そのため、
承認サーバーとリソースサーバーの所有者が同じである場合のみ使える。
更に、同じ組織であっても、電子メールなどの不安全なチャンネルにてのシェアもダメであろう。

### Asymmetric key


非対称キーとは署名と復元のキーが異なるという意味であり、
二つのキーは一対一関連する、
そのため、一般的にはキーペア(key pair)と呼ぶ。
具体的に、承認サーバーは秘密キーでtokenをデジタル署名し、
リソースサーバーは公開キーでデジタル署名を復元する。
名前の通り、公開キーは公開するキーであり、
秘密キーは公開しないキーである。
公開キーはデジタル署名の復元以外に使えないため、
第三者の組織に公開しても悪用されるリスクはない。

JDKに含まれる「keytool」と、Git Bashに含まれる「OpenSSL」を利用して、
簡単に非対称キーペアを生成することができる。
まず、以下のコマンドを打つと秘密キーを生成する:

keytool -genkeypair -alias keyname -keyalg RSA -keypass keypassword -keystore keystore.jks -storepass storepassword

更に、以下のコマンドを打つと秘密キーを元に公開キーを生成する:

keytool -list -rfc \texttt{--}keystore keystore.jks \texttt{|} openssl x509 -inform pem -pubkey

定期的にキーを変更すると秘密キーの漏れるリスクが軽減する。
公開キーの共有には、
手動で各リソースサーバーのコンフィグレーションにコピーするというやり方もあるが、
メンテナンス性が低下であろう。
その解決策は、
秘密キーと公開キー両方を承認サーバーに管理させ、
endpointにて公開キーを提供させることである。
リソースサーバーのコンフィグレーションにそのendpointのurlを指定すれば、
自動的に最新の公開キーを取得する手法がある、
しかし、そのリクエストに伴って処理時間もより長いであろう。

## Conclusion


ここまで、OAuth2における慣用的な認証方式とtoken検証方式を考察した。
どの方式も完全に無欠というわけにはいかない、
どの方式を導入するかは、
開発の難易度、処理スピード、
既存のシステムとの相性などを総合的に考慮しなければならないであろう。


---


## The alternatives to Spring Security OAuth Project


OAuth2認証機能を実装するために、
過去のJava・Spring開発者はよく「Spring Security OAuth」というフレームワークを利用した。
Spring Security OAuthフレームワークは承認サーバー、クライアント、ソースサーバー全部サポートし、
コンフィグレーションクラスで処理フローを設定すれば簡単に認証をカスタマイズすることができる。
しかし、このプロジェクトは既にSpringチーム(以下「Pivotal」という。)に廃棄された(deprecated)。
つまり、このフレームワークはこれ以上更新されない、
そのため業務アプリケーションの開発者はシステムを他のソリューションへ移行しないといけない。
PivotalはSpring Security OAuthプロジェクトのリソースサーバーとクライアントの部分を「Spring Security」プロジェクトに合併し、
承認サーバーの実装ソリューションを提供しないとアナウンスした。

### Using Spring Security


Spring SecurityフレームワークはOAuth2リソースサーバー機能だけではなく、
HTTPベーシック認証、フォーム認証、フィルタリング、ユーザー管理、暗号化、メソッドセキュリティなど、
幅広いセキュリティ機能を提供する非常に強力なソリューシである。
Spring Security OAuthと似たように、
Spring SecurityでのOAuth設定も「WebSecurityConfigurerAdapter」というクラスの「configure(HttpSecurity)」メソッド内で簡単に定義できる。
認証に通過したリクエストに対して、ユーザー情報はSecurity Contextにキャッシュされ、
メソッドセキュリティなどの業務権限ロジックに再利用できる。

しかし、Spring Securityには承認サーバーの実装ソリューションを提供しないため、
他の選択肢も必要であろう。

### Using Keycloak


現在、中小企業でよく使われているソリューションの一つは「Keycloak」である。
KeycloakとはRed Hat社がメンテナンスしているWeb上でのSSOを実現するためのオープンソース認証ソフトウェアであり、
Apacheライセンスとしてソースコードが公開されている。
KeycloakはOAuth2だけではなく、
SAML2.0、OpenID Connectにもサポートする。
更に、「keycloak-spring-boot-starter」というMaven dependencyがあるため、
KeycloakとSpring Bootアプリケーションのインテグレーションは非常に容易である。
Keycloakの商用対応物は「Red Hat Single Sign-On」である。

### Using Spring Authorization Server in the future


PivotalがSpringプロジェクトから承認サーバーに関わるコードを削除した後、
Springコミュニティの開発者達から多くの批判と抗議が寄せられた。
結局、コミュニティの意見を慎重に考慮したら、
Pivotalは「Spring Authorization Server」という新たなプロジェクトをアナウンスした。
Spring Authorization Serverプロジェクトの目標はOAuth2.1承認サーバーの開発にサポートすることである。
2021年8月19日に最初の正式にサポートされた本番環境対応バージョンであるバージョン0.2.0が発表された、
現在(2022年1月20日)の最新バージョン番号は0.2.1であり、
成熟するまで恐らく1\textasciitilde{}3年かかるであろう。

## Conclusion


もし、商用のアプリケーションを開発するなら、
Keycloakを承認サーバーとし、
Spring Securityのリソースサーバーで繋げるというソリューションが良いであろう。
勉強か研究の目的であれば、今からSpring Authorization ServerとSpring Securityを学んでも将来的に価値があるであろう。