おはようございます、ritouです。
(⚠️認可イベントの識別子のあたり、ちょっと見直しました!最初に見ていただいた方はもう一回どうぞ!)
前回、ハイブリッド型と呼ばれる OAuth 2.0 のトークン実装について書きました。
その続きとして JWT(JWS) + RDBでできる実装例を紹介します。 理解するにはそれなりの OAuth 2.0 に関する知識が必要になるかもしれませんが、よかったら参考にしてみてください。
何を考えたのか
OAuth 2.0のRefresh Token, Access Tokenを考えます。 要件から整理しましょう。
要件
結構ありますが、最低限の OAuth 2.0 の Authorization Server を実装しようと思ったらこれぐらいはやらないといけないでしょう。
- RFC6750 で定義されている Bearer Token 相当
- Refresh Token
grant_type=authorization_code
で発行されるgrant_type=refresh_token
で自身が更新される、いわゆるローテートにも対応
- Access Token
grant_type=authorization_code
,grant_type=refresh_token
で発行される- Refresh Token からのダウンスコーピング(scopeを減らして指定)が可能
- 各トークンから参照できる情報 : 色々とセンシティブな金融関係のAPIを利用するためのトークンなどではなく、Clientからこれらの内容は見れても良い想定
- Resource Owner's identifier
- Authorization Request(client_id, scope etc)
- 無効化
- Resource Server の Access Token ハンドリング
- Token Introspection API(もしくは独自の仕組み)で検証可能
- 有効期限前に無効化されていることを許容できるならばResource Server自体で検証可能
このような要件を、DBにテーブルいっぱい作ってやるんじゃなくJWTの特徴を生かしてなんとかできないか、というのがこの記事で紹介する実装の目的です。
データの持ち方
JWT部分は仕様策定中のこのDraftを参考にしてます。
JWT(JWS)
- Header
- Payload
- iss : Authorization Serverの識別子
- aud : Refresh Token なら Authorization Server, Access Token なら Resource Server の識別子
- sub : Resource Owner の識別子
- client_id : Client ID
- scope : 認可イベントで処理された scope の値
- iat : 発行日時
- exp : 発行日時にRefresh Tokenは長め、Access Tokenは短めの時間を追加した有効期限
- auth_id : Refresh Token は認可イベント(Resource Owner がリソースアクセスを許可するタイミング)単位でユニークな identifier (UUID/ULID的なの)を auth_id として持つ。Access Tokenは同時に発行された、もしくは更新された Refresh Token の auth_id の値を持つ。
- アクション
⚠️jti ではなく auth_id として認可イベントの識別子を持つように変更しました。
RDB
- カラム
- id : JWT の auth_id と同じ値(もしくはハッシュ値) プライマリーキー
- client_id : JWT の client_id と同じ値 : 無効化の時に使う。
- user_id : JWT の sub と同じ値 : 無効化の時に使う。
- updated_at : Refresh Token の発行(最終更新)日時。ローテートの時に使う。
- アクション
- create : 認可イベントで作成される
- read : grant_type=refresh_tokenの時や、Token Introspection API(もしくは独自の仕組み)が呼ばれた時、あとはResource Onwer に無効化機能を提供する時に参照
- grant_type=refresh_token では auth_id, updated_at の組み合わせで検索することでローテートされた最新の Refresh Token のみ扱うことが可能
- update : RefreshTokenのローテートのタイミングで updated_at を更新
- delete : Resource Owner 自身による無効化などのタイミングで消す。updated_at の値を見て Refresh Token の有効期限を超えたものは処理の途中や定期的に消す。
ポイントは認可イベントが行われた時にのみ生成されるというあたりです。 Access Token 単位でデータを持つと規模によってはデータ量が増えるわけですが、RDBが持つデータ数を有効なRefreshTokenの数に留めることが可能です。
例
RDBの中身とJWTはこんな感じになります。
# grant_type=authorization_code ## RDB * id : 10870e51-e06a-03cb-33c7-eca6631d8e3d * user_id : "12345" * client_id : "cid_abcde" * updated_at : 1588788749 ## Refresh Token eyJhbGciOiJSUzI1NiIsImtpZCI6Im9hdXRodG9rZW4yMDIwMDUiLCJ0eXAiOiJydCtqd3QifQ.eyJhdWQiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmV4YW1wbGUuY29tLyIsImF1dGhfaWQiOiIxMDg3MGU1MS1lMDZhLTAzY2ItMzNjNy1lY2E2NjMxZDhlM2QiLCJjbGllbnRfaWQiOiJjaWRfYWJjZGUiLCJleHAiOjE1ODg4NzQyNDksImlhdCI6MTU4ODc4Nzg0OSwiaXNzIjoiaHR0cHM6Ly9hdXRob3JpemF0aW9uLXNlcnZlci5leGFtcGxlLmNvbS8iLCJzY29wZSI6Im9wZW5pZCBzYW1wbGUgc2FtcGxlMiIsInN1YiI6IjEyMzQ1In0.Op26WcEUS2qG64KMGT2a4EyLziEhta5gcTGIeE4uu2sw9PC9mxoQo18iBNcaAk34n4OKEH1Qi8lzfb6fWMySEohqWKjv-iZtuBfu0O550z1faCx3jhoNqoSwKInMbOJHR8PtJN5E_GEZTNFwH3i4dEk1Moi8KAX8aJ7OZBR3yFipnbb6AC3Vpy0JuLxZMJjU_EVfO3XOZ2YA9OYlpFerflnPUTpPIE1OV5yQ7ecvnPDpShpVS3A-s7n3iZMp7z6kxvkYIsZc0W5JKxrfN-ivkPHaTeKluxM0eOmXV_Q6hmH7U2S0gf6iJB76UOCsw7VtqsTnANLCRI-rBJAgKTu9vg ## Access Token eyJhbGciOiJSUzI1NiIsImtpZCI6Im9hdXRodG9rZW4yMDIwMDUiLCJ0eXAiOiJhdCtqd3QifQ.eyJhdWQiOiJodHRwczovL3JzLmV4YW1wbGUuY29tLyIsImF1dGhfaWQiOiIxMDg3MGU1MS1lMDZhLTAzY2ItMzNjNy1lY2E2NjMxZDhlM2QiLCJjbGllbnRfaWQiOiJjaWRfYWJjZGUiLCJleHAiOjE1ODg3ODg3NDksImlhdCI6MTU4ODc4Nzg0OSwiaXNzIjoiaHR0cHM6Ly9hdXRob3JpemF0aW9uLXNlcnZlci5leGFtcGxlLmNvbS8iLCJzY29wZSI6Im9wZW5pZCBzYW1wbGUgc2FtcGxlMiIsInN1YiI6IjEyMzQ1In0.v0J_KCws4qQBNYiuWBnqi1Pq0L34_URj4vLnuXXwegdyF087SsgI0j0BTM0Rx9LjgYn4JTmwRKYkNnC7vTW3xpvteXmvQEUVJpMPIfYulwz_oQKOPuB1Nr259Q8d9Bcu7Tlr5skyVjIqya1O6VYEkI7YHBCD2MVh0V6L0pWcS7JazkSzaaS50MdGqDWD7uafLSjdzI52tQfhCiyhhnfi8glU5byg1s3g5hQrXx7hhNqOfECBteySUaVE43W3SVHv2pRlJ-XPla0fohnHtzFqodhPCJ4BuiTU7nh7bzVCeAVfxUZCNitA8oj0SiM9lHjbXtEkqnIXa4JZsJSYFjBLgg
# grant_type=refresh_token ## RDB * id : 10870e51-e06a-03cb-33c7-eca6631d8e3d * user_id : "12345" * client_id : "cid_abcde" * updated_at : 1588788750 <- updated!!! ## Refresh Token(rotated) eyJhbGciOiJSUzI1NiIsImtpZCI6Im9hdXRodG9rZW4yMDIwMDUiLCJ0eXAiOiJydCtqd3QifQ.eyJhdWQiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmV4YW1wbGUuY29tLyIsImF1dGhfaWQiOiIxMDg3MGU1MS1lMDZhLTAzY2ItMzNjNy1lY2E2NjMxZDhlM2QiLCJjbGllbnRfaWQiOiJjaWRfYWJjZGUiLCJleHAiOjE1ODg4NzQyNTAsImlhdCI6MTU4ODc4Nzg1MCwiaXNzIjoiaHR0cHM6Ly9hdXRob3JpemF0aW9uLXNlcnZlci5leGFtcGxlLmNvbS8iLCJzY29wZSI6Im9wZW5pZCBzYW1wbGUgc2FtcGxlMiIsInN1YiI6IjEyMzQ1In0.STXzXd-1x7mT44PosBAGTF46yZ3ZtrfSDhC-6kNYQYRc0bwpHuNEFlFCQkJiX4GS8pCrygoovuqPjPHL4mp68Rebw5hliUJZ6iWGhPpbiSd7JCSXDrXPiJe0hYevgoksfqeGknHp6yiSH0E6KmjZoO7BpZVJr4gcBCC9eeqP4f_TsY7k3m4U_zpUdeyakvqVNfefn3WPzMymxiP-xvH60mbea7HQJXi_dkOweVECLrNEPGoWwkiHAGXaC6GKHs-7Ime1nJF3j6WDYEIzJDtqFkQS_Y14Ps6EgWyFNQegcBMbhc0-LQnFpxqD4e2VEqQPhUwMsPtQhQZDNA2OUbOiAA ## Access Token(down scoped) eyJhbGciOiJSUzI1NiIsImtpZCI6Im9hdXRodG9rZW4yMDIwMDUiLCJ0eXAiOiJhdCtqd3QifQ.eyJhdWQiOiJodHRwczovL3JzLmV4YW1wbGUuY29tLyIsImF1dGhfaWQiOiIxMDg3MGU1MS1lMDZhLTAzY2ItMzNjNy1lY2E2NjMxZDhlM2QiLCJjbGllbnRfaWQiOiJjaWRfYWJjZGUiLCJleHAiOjE1ODg3ODg3NTAsImlhdCI6MTU4ODc4Nzg1MCwiaXNzIjoiaHR0cHM6Ly9hdXRob3JpemF0aW9uLXNlcnZlci5leGFtcGxlLmNvbS8iLCJzY29wZSI6Im9wZW5pZCBzYW1wbGUiLCJzdWIiOiIxMjM0NSJ9.YaX5u_Ef174aoC7QP48twCZxBHOxGELXD0k-e8aRR7W8c6IcPlUrWnZqQzQahLGm0Cjm7P6fnH7I4FJITiOfQAlWY3t-xqUYILngATs6TJ-gIhBd2W-e4uHQ7S98cVyCS_jI-gz_QnMHOaALz_RILvX_bNTsgZ1XSvGyRzRl5zQ_fagU-0QCbEyma6HYonJ_ua6i_QHzW4WzJ7haOut-E7i0kqv-7EI4EEaC2bf-c8xEqP51scHB1VikrBZ97_iMGmCUzLJ5qAwGShB3Z2bZvbaaGrnNxuCKvV04ayl-KhE6iyIpqPP7Tz8CXy-NnboaYI8gaYmZ8K4yYMqIzQlDuA
まとめ
OAuth 2.0 の Refresh Token / Access Token を JWT + RDB で実装する例を紹介しました。 ベタに UUID/ULID を各種トークンに使用するのに比べて
- JWTを利用することでResource Server - AuthZ Sever 間の無駄な通信を抑えられる
- RDB側は最低限のデータ保持で済む
- Refresh Tokenのローテートにもデータを増やさずに対応可能
という特徴があります。 誰かのお役に立てれば幸いでございます。
社会復帰頑張りましょう。 ではまた。