Table of Contents
Internet Engineering Task Force (IETF) が OAuth 2.0 の中心仕様である RFC 6749 を公開したのは 2012 年 10 月 13 日です。IETF の OAUTH ワーキンググループはその後も活動を続け、現在も活発に新しい標準仕様の策定作業をおこなっています。
年月の経過に伴い、同ワーキンググループによる RFC も増えていき、今では 30 を超えています。
一方、OpenID Foundation (OIDF) が OpenID Connect の中心仕様である OpenID Connect Core 1.0 を公開したのは 2014 年です。 同団体のワーキンググループ群は、現在も引き続き標準仕様策定作業をおこなっています。
OIDF のワーキンググループ群が策定した、もしくは策定作業中の仕様群も、それなりの数があります。
これらに加え、他所で作成された標準仕様も OAuth や OpenID の文脈で参照されます。 例えば、OpenID Shared Signals Framework Specification 1.0 は、IETF の SECEVENT ワーキンググループ (解散済み) が策定した次の RFC 群に依存しています。
同様に、FAPI 2.0 Http Signatures は、IETF の HTTPBIS ワーキンググループが策定した次の RFC 群に依存しています。
日付 | RFC | タイトル |
---|---|---|
Structured Field Values for HTTP★ | ||
HTTP Message Signatures★ | ||
Digest Fields |
ご懸念される通り、依存関係に言及し始めると際限がありません。 なぜなら、ほぼ全ての場合において、新しい仕様は既存の仕様群を基盤として作成されるからです。 結果として、一般的には、新しい仕様ほどそれを理解するために要求される前提知識の量が多くなります。
この状況の問題点は、たとえ API 保護のための新しい標準仕様が公開されても、その内容を調査・検討するためのハードルが高いせいで、新しい仕様の利活用が進まないことです。 ひいては、年月の経過とともに、積極的に更新されないシステムの API セキュリティは相対的に下がっていってしまいます。 攻撃側が狡猾に進化していくことを考慮すると、この問題は深刻と言えます。
そこで、本記事では、RFC 6749 以降に開発された標準仕様をフル活用して
API を保護する方法を紹介していこうと思います。アクセストークンの使い方 (RFC 6750)
と情報取得 (RFC 7662) という基本から始め、resource
パラメータ
(RFC 8707) による受信者限定、MTLS (RFC 8705) や
DPoP (RFC 9449) による送信者限定、HTTP メッセージ署名
(RFC 9421)、Cedar、といったトピックを扱います。
その他、我々 (Authlete 社) が常にそうであるように、開発者の皆様に寄り添うため、この記事では実装者視点の事柄も扱います。例えば次のようなトピックを扱います。
@target-uri
(RFC 9421) をリバースプロキシの後ろで計算する方法それでは、始めましょう!
RFC 6750 (Bearer Token Usage) では、アクセストークンをリソースサーバに提示する方法として次の三つが示されています。
Authorization
ヘッダを用いる方法 (Section 2.1)しかしながら、二番目の方法は HTTP リクエストのメッセージボディのフォーマットが
application/
に限定されてしまうこと、三番目の方法はセキュリティ上の問題があることから、一番目の方法が用いられることがほとんどです。
Authorization
ヘッダを用いる方法では、ヘッダの値を
Bearer アクセストークン
という形式にします。Bearer
部分は固定文字列であり、アクセストークンの箇所は実際のアクセストークンの値で置き換えます。
下記は、アクセストークン
i_1X-euOC6-45zdObsQty7hgDMW9RUTjSGb0pzP69X0
を使って https://
にアクセスする例です。
GET https://rs.example.com/resource/1
Authorization: Bearer i_1X-euOC6-45zdObsQty7hgDMW9RUTjSGb0pzP69X0
一方、アクセストークンを DPoP (RFC 9449) という仕組みを用いてよりセキュアにしている場合、Bearer
という固定文字列を DPoP
に変更します。また、DPoP
ヘッダも追加します。
DPoP
ヘッダの値は DPoP Proof JWT と呼ばれる JWT (RFC 7519) の一種です。
Authorization: DPoP アクセストークン
DPoP: DPoP Proof JWT
下記は、DPoP 対応アクセストークンを用いてリソースにアクセスする例です。
GET https://rs.example.com/resource/1
Authorization: DPoP i_1X-euOC6-45zdObsQty7hgDMW9RUTjSGb0pzP69X0
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsIngiOiIxQW1WcjRHb0hkUGdrNDhMV2RTM1Q5bTZtMW1QNFZUY2o5dXNvU0JuQ1FrIiwieSI6InRlLVdJdVVJcTJ3OHRYbVh5ZGxFWDRwZTlsTmUtUEJjb3pBOG43eThYVEUifX0.eyJqdGkiOiJvY1A3ZVc0d3l1bG0yWmMzIiwiaHRtIjoiR0VUIiwiaHR1IjoiaHR0cHM6Ly9ycy5leGFtcGxlLmNvbS9yZXNvdXJjZS8xIiwiaWF0IjoxNzU0MDMxMzU1LCJhdGgiOiJFWVlRSk9vWDlYLUt6TVBBNTI3NnBjelR0cHpVMjhMRzltQWRVeXdIa2dVIn0.Jg8sTKhaZvPJY0p--NaFEHJhkuM0SN4CHUbPe5xaxuZBLHZNfTtRJjpSb_6P-utmNYqQRuQ1rEPUq8ivKPP1MQ
DPoP の詳細は後述します。
提示されたアクセストークンの有効性を確認するため、リソースサーバはまず、アクセストークンの情報を取得する必要があります。
情報がアクセストークン自体に埋め込まれている場合、アクセストークンの内容を読むことで情報を取得できます。 一方、情報が埋め込まれていない場合は、認可サーバのイントロスペクションエンドポイント (RFC 7662) に問い合わせをして情報を取得します。
一見、アクセストークンに情報が埋め込まれている形式の方がリソースサーバの負担は少ないように見えますが、この形式の場合、アクセストークンの内容が改竄されていないことを確認する手間が別途かかります。
情報をアクセストークン自体に埋め込む場合、改竄検出が可能なことから、アクセストークンの形式として JWT (RFC 7519) を用いることがほとんどです。JWT 発行者 (この文脈ではアクセストークンを発行した認可サーバ) の公開鍵を用いて JWT の署名を検証することで、その JWT の内容が改竄されていないことを確認できます。
ここで、JWT 形式のアクセストークンの署名を検証する手順を見ていきましょう。
まず、JWT のペイロード部にある iss
クレーム (RFC 7519 Section 4.1.1)
の値を読み取ります。このクレームは JWT 発行者の識別子を表しています。
アクセストークンを発行するのは、認可サーバ、または認可サーバを兼ねる OpenID
プロバイダなので、JWT アクセストークンの iss
の値は、認可サーバまたは
OpenID プロバイダの識別子を表していることになります。
認可サーバと OpenID プロバイダの識別子は、関連仕様 (RFC 8414,
OIDC Core, OIDC Discovery) により https
で始まる
URL と定められています。結果として、iss
クレームの値は、アクセストークンを発行した認可サーバまたは
OpenID プロバイダ (HTTP サーバ) の URL を表しています。
アクセストークンの発行者識別子が得られれば、その情報を元に発行者のメタデータが公開されている場所を求めることができます。
認可サーバが RFC 8414 (OAuth 2.0 Authorization Server Metadata)
をサポートしていれば、発行者識別子に
/.well-known/
を追加した場所でメタデータが公開されています。
この場所からサーバメタデータを JSON 形式で取得できます。
または、サーバが OpenID Connect Discovery 1.0
をサポートしていれば、発行者識別子に
/.well-known/
を追加した場所でメタデータが公開されています。
サーバメタデータには多くの情報が含まれていますが、ここで重要なのは jwks_uri
メタデータです。これは、サーバの JWK セットドキュメントが公開されている場所を示しています。
この場所から JWK セットドキュメントをダウンロードすることができます。
JWK セットドキュメントの内容は RFC 7517 の
Section 5. JWK Set Format で定義されているフォーマットに従っています。
そのフォーマットは JSON オブジェクトであり、keys
というトップレベルプロパティを一つ含んでいます。
その keys
プロパティの値は、JWK (RFC 7517) の JSON 配列です。
その JWK 群の中に、JWT アクセストークンの署名の検証のための公開鍵が含まれているはずです。
JWT の JWS ヘッダが kid
パラメータ (RFC 7515 Section 4.1.4)
を含んでいれば、その値と同じ kid
(RFC 7517 Section 4.5)
を持つ JWK を探すことで、検証鍵を特定することができます。
JWT が kid
を含んでいない場合は、検証鍵の特定方法は実装固有の方法になるでしょう。
例えば、alg
(RFC 7515 Section 4.1.1,
RFC 7517 Section 4.4) や use
(RFC 7517 Section 4.2)
などで条件を絞り込んで検証鍵を特定する、といった方法が考えられます。
検証鍵が特定できたら、その鍵を用いて JWT の署名を検証します。 検証がパスすれば、JWT アクセストークンが改竄されていないと言えます。
まとめ
アクセストークンは、最大で次の三つの時刻関連属性を持ちえます。
アクセストークンの情報をイントロスペクションエンドポイントから得たのか、または
JWT のペイロードから得たのか、に関わらず、これらの値は iat
(Issued At)、nbf
(Not Before)、exp
(Expiration Time) という名前で参照されます。
名前 | イントロスペクションレスポンス | JWT ペイロード | |
---|---|---|---|
発行時刻 | iat |
RFC 7662 Section 2.2 | RFC 7519 Section 4.1.6 |
有効期間開始時刻 | nbf |
RFC 7662 Section 2.2 | RFC 7519 Section 4.1.5 |
有効期間終了時刻 | exp |
RFC 7662 Section 2.2 | RFC 7519 Section 4.1.4 |
これらの属性の値により、アクセストークンの有効期間が決まります。
次の図は、iat
、nbf
、exp
が全て指定されている場合のアクセストークン有効期間を示したものです。
リソースサーバは、現在時刻がアクセストークンの有効期間内であることを確認します。 具体的には次の確認を行います。
ただし、実際に運用するシステムでは、システム間の時刻ずれの可能性を考慮して確認を行います。
というのは、本来有効であると判定されるべきアクセストークンが、時刻ずれの影響で無効であると判定されることが、現実世界ではよく起こるからです。
例えば、アクセストークンを受け取るシステム (リソースサーバ) の時刻が、アクセストークンを発行したシステム
(認可サーバ) の時刻よりも遅れていると、「アクセストークンの有効期間がまだ始まっていない
(現在時刻が nbf
で指定された時刻より前である)」と判定される可能性があります。
「NTP (Network Time Protocol) を使って時刻同期をしていればそういう問題は起こらないはずだ」 という意見も耳にすることもあるでしょうが、現実的には起こります。 各国のオープンバンキングエコシステムの長年の運用経験から得られた知見が FAPI 2.0 Security Profile に書かれているので、ここでご紹介します。
NOTE 3: Clock skew is a cause of many interoperability issues. Even a few hundred milliseconds of clock skew can cause JWTs to be rejected for being “issued in the future”. The DPoP specification [RFC9449] suggests that JWTs are accepted in the reasonably near future (on the order of seconds or minutes). This document goes further by requiring authorization servers to accept JWTs that have timestamps up to 10 seconds in the future. 10 seconds was chosen as a value that does not affect security while greatly increasing interoperability. Implementers are free to accept JWTs with a timestamp of up to 60 seconds in the future. Some ecosystems have found that the value of 30 seconds is needed to fully eliminate clock skew issues. To prevent implementations switching off
iat
andnbf
checks completely this document imposes a maximum timestamp in the future of 60 seconds.注3: クロックスキュー (時刻のずれ) は、多くの相互運用性の問題の原因となっています。 数百ミリ秒程度のわずかな時刻ずれであっても、JWT が「未来に発行された」と判断されて拒否される可能性があります。 DPoP 仕様 ([RFC9449]) では、数秒から数分といった「現実的に近い未来」であれば JWT を受け入れるよう提案されています。本ドキュメントではさらに踏み込んで、認可サーバは最大で 10 秒未来のタイムスタンプを持つ JWT を受け入れることを必須としています。 この「10 秒」という値は、セキュリティに影響を与えずに相互運用性を大きく向上させる値として選ばれました。 実装者は、最大で 60 秒未来のタイムスタンプを持つ JWT を受け入れても構いません。 一部のエコシステムでは、クロックスキュー問題を完全に解消するためには 30 秒が必要であるとされています。 なお、本ドキュメントでは、実装が
iat
(発行時刻) やnbf
(有効期間開始時刻) のチェックを完全に無効化することを防ぐために、未来時刻の上限を最大 60 秒とする制限を設けています。
技術的には、時刻ずれの考慮には、判定を厳しくする方向 (有効と判定されにくくなる)
と、判定を緩くする方向 (有効と判定されやすくなる) があります。
システム運用の現場で求められているのは後者です。
これを踏まえ、iat
と nbf
を現在時刻と比較する際は許容する最大時刻ずれ
(例えば 10 秒) を引いてから比較をおこない、exp
を現在時刻と比較する際は許容する最大時刻ずれを足してから比較をおこないます。
RFC 6749 が定義するアクセストークンの基本的な属性の一つとして、スコープがあります (RFC 6749 Section 3.3)。 スコープはアクセストークンが持つ権限群を表しています。
API 群は、それぞれの目的に応じ、アクセストークンが特定のスコープを持つことを要求することがあります。 どのようなスコープを要求するかは、API 提供者が任意に定めます。 API の実装は、処理を先に進める前に、提示されたアクセストークンが必要なスコープを持っているかどうかを確認します。
クライアントアプリケーションは自分が利用する予定の API
が要求するスコープを予め調べておき、アクセストークンの発行を認可サーバに依頼する際にそのスコープを依頼内容の一部として含めます。
具体的には、たとえば認可コードフロー (RFC 6749 Section 4.1)
を用いてアクセストークンの発行を依頼する場合、認可リクエスト
(RFC 6749 Section 4.1.1) の scope
リクエストパラメータの値として、個々のスコープ名をスペース区切りで列挙します。
具体的な例を見てみましょう。
クライアントアプリケーションが、あるサーバが公開している GET /ssf/status
API と
POST /ssf/status
API を利用したいとします。そして、GET /ssf/status
API は
ssf:
スコープを、POST ssf/status
API は
ssf:
スコープを要求するとします。
クライアントアプリケーションは、それらのスコープ名を scope
リクエストパラメータの値に含む認可リクエストを認可サーバに送ります。
認可サーバは、ユーザの承認を得た後、それらのスコープ群が紐付いたアクセストークンをクライアントアプリケーションに発行します。
クライアントアプリケーションは、アクセストークンを添えて API にアクセスします。
まとめ
アクセストークンの受信者 (audience) を制限する方法があります。
その方法を用いることにより、例えば、ある特定のリソースサーバだけを受信者と指定したり、特定のパス階層下にあるエンドポイント群のみを受信者として指定したりすることができます。 これにより、アクセストークンの誤用や悪用の可能性を減らすことができます。
アクセストークンの受信者は、イントロスペクションレスポンス (RFC 7662) と
JWT アクセストークンの双方とも、aud
パラメータで表現されます。
名前 | イントロスペクションレスポンス | JWT ペイロード | |
---|---|---|---|
受信者 | aud |
RFC 7662 Section 2.2 | RFC 7519 Section 4.1.3 |
例えば、イントロスペクションレスポンスが次のように aud
プロパティの値に
https://rs.example.com
を含んでいれば、そのアクセストークンは
https://rs.example.com
でのみ使えることを意味します。
{
"aud": [
"https://rs.example.com"
]
}
仮に、そのアクセストークンを https://
に提示しても、https://
上の API
の実装群がアクセストークンの aud
プロパティの値を正しくチェックしていれば、そのアクセストークンは拒否されます。
アクセストークンの受信者の制限は、RFC 8707
(Resource Indicators for OAuth 2.0) で定義されている resource
リクエストパラメータ (RFC 8707 Section 2) を用いておこないます。
認可リクエストやトークンリクエストに resource
リクエストパラメータを含めると、その値が
アクセストークンの受信者として設定されます。下記は RFC 8707 の
Section 2.1 から抜粋した resource
リクエストパラメータの使用例です。
この例では、https://
と
https://
の二つがアクセストークン受信者として指定されています。
GET /as/authorization.oauth2?response_type=code
&client_id=s6BhdRkqt3
&state=tNwzQ87pC6llebpmac_IDeeq-mCR2wLDYljHUZUAWuI
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&scope=calendar%20contacts
&resource=https%3A%2F%2Fcal.example.com%2F
&resource=https%3A%2F%2Fcontacts.example.com%2F HTTP/1.1
Host: authorization-server.example.com
このように受信者が制限されたアクセストークンを RFC 8707 では「受信者限定 (audience-restricted)」アクセストークンと呼んでいます。
従来のアクセストークンは、漏洩してしまうと、攻撃者がそれを用いて API にアクセスすることができてしまいます。 これは、電車の切符を失くしてしまったら、それを拾った他の人が電車に乗れてしまう問題と同じです。
この脆弱性を軽減する手段として、アクセストークンの発行対象者とアクセストークンの利用者が同一であることを API アクセス時にチェックするという方法が考えられます。 これは、国際線航空チケットの利用時に、チケットと併せてパスポートの提示も要求し、正規のチケット利用者とチケット持参者が同一であることを確認する手続きと同じです。
アクセストークンの正当な所有者であることを示すものを Proof of Possession (PoP) と呼びます。 リソースサーバがクライアントアプリケーションに対し、アクセストークンと併せて PoP の提示を要求することにより、アクセストークンだけを窃取しても API を不正利用できなくなります。
PoP を要求することにより、結果として、アクセストークンを送ってくる者 (sender) を制限することになります。 このため、利用時に PoP の提示も要求されるアクセストークンを「送信者限定 (sender-constrained)」アクセストークンと呼びます。
送信者限定アクセストークンを実現する方法に MTLS (RFC 8705 Section 3) と DPoP (RFC 9449) があります。次の二節でこれらについて説明します。
RFC 8705 の Section 3 では、クライアントアプリケーションの X.509 証明書 (RFC 5280) (以降、クライアント証明書) を利用して PoP を実現する方法 (以降 MTLS) を定めています。
MTLS を用いる場合、クライアントアプリケーションと認可サーバのトークンエンドポイント間の TLS 接続、および、クライアントアプリケーションとリソースサーバの API 間の TLS 接続を、相互 TLS 接続とすることが前提となります。また、それらの TLS 接続で、クライアントアプリケーションが同一の証明書を提示することが求められます。
相互 TLS 接続とは、通信の両者 (クライアントとサーバ) が互いの正当性を証明し合う TLS 接続のことです。 通常の TLS 接続では、サーバが自分の証明書を提示し、クライアントがその証明書を検証するだけです。 しかし、相互 TLS 接続ではクライアントも証明書を提示し、サーバもクライアントの証明書を検証します。
MTLS の基本的なアイディアは、「トークンリクエスト時の相互 TLS 接続で用いられたクライアント証明書」を生成するアクセストークンに紐付けて覚えておき、 その後のリソースリクエスト時、「リソースリクエスト時の相互 TLS 接続で用いられたクライアント証明書」がリソースリクエストに含まれるアクセストークンに紐付いているものと同一であるか確認する、というものです。
同一であれば、トークンリクエストをおこなったクライアントアプリケーションとリソースリクエストをおこなっているクライアントアプリケーションが同一であるとみなします。同一でなければ、リソースリクエストをおこなっているクライアントアプリケーションはアクセストークンの正当な保有者ではないと判断し、リソースリクエストを拒否します。
それでは、MTLS により送信者限定アクセストークンが実現される手順を見ていきましょう。
クライアントアプリケーションは、認可サーバのトークンエンドポイントと相互 TLS 接続を確立し、トークンリクエストを送信します。 相互 TLS 接続なので、クライアントはクライアント証明書をサーバに提示します。
トークンエンドポイントの実装では、トークンリクエストが有効であればアクセスストークンを生成し、それをデータベースに保存します。 ただし、アクセストークンが情報を自分自身に埋め込む形式の場合 (JWT アクセストークンなど)、データベースにデータを書き込まない実装もありえます。
このとき、相互 TLS 接続からクライアント証明書を取り出し、そのハッシュ値を計算し、アクセストークンと紐付けて覚えておきます。 ここでハッシュ値は、DER エンコード (X.690) されたクライアント証明書の SHA-256 ハッシュ (NIST FIPS 180-4) です。
このように、証明書と紐付けられたアクセストークンのことを「certificate-bound」アクセストークンと呼びます。
トークンエンドポイントは、生成したアクセストークンを含むトークンレスポンスをクライアントアプリケーションに返します。
アクセストークン取得後、クライアントアプリケーションはリソースサーバの API との間に相互 TLS 接続を確立します。このとき、トークンリクエストの際に用いたのと同じクライアント証明書を使います。
接続確立後、アクセストークンを添えてリソースリクエストを送信します。
API の実装はリクエストからアクセストークンを取り出し、そのアクセストークンを添えて認可サーバのイントロスペクションエンドポイントに問い合わせをおこないます。 ただし、アクセストークンの情報がアクセストークン自身に含まれている場合は、イントロスペクションエンドポイントに問い合わせるかわりにアクセストークンの内容を読みます。
イントロスペクションエンドポイントの実装は、提示されたアクセストークンの情報をデータベースから取り出し、イントロスペクションレスポンスに整形してリソースサーバに返します。
この際、アクセストークンに紐付くクライアント証明書のハッシュ値を base64url
(RFC 4648) エンコードし、cnf
というトッププロパティ内の
x5t#S256
サブプロパティの値としてイントロスペクションレスポンスに埋め込みます。
次に、API の実装は、クライアントアプリケーションとの相互 TLS 接続からクライアント証明書を取り出し、そのハッシュ値を計算します。
そして、計算されたハッシュ値とイントロスペクションレスポンスに含まれるハッシュ値が一致するかどうかを確認します。 一致しなければ、リソースリクエストをおこなっているクライアントアプリケーションはアクセストークンの正当な保有者ではないと判断し、リソースリクエストを拒否します。
まとめ
TLS で保護されたアプリケーションサーバを配備する際、クライアントとアプリケーションサーバの間にリバースプロキシを置くという構成は一般的です。 リバースプロキシは、クライアントとの TLS 接続を担当し、そこで TLS 接続を終端させてから、後方に控えるアプリケーションサーバにクライアントからのリクエストを転送します。
このような構成では、アプリケーションサーバはクライアントと直接 TLS 接続をおこないません。そのため、たとえクライアントがリバースプロキシとの間に相互 TLS 接続を確立していたとしても、その接続内でクライアントが提示したクライアント証明書をアプリケーションサーバは参照できません。 もしもアプリケーションサーバのロジックでクライアント証明書を参照したければ (例えば certificate-bound アクセストークンを生成したり検証したりしたければ)、何らかの方法でリバースプロキシからクライアント証明書を転送してもらわなければなりません。
この目的のため、「クライアント証明書を値として持つカスタム HTTP ヘッダ
(例: X-Ssl-Cert
) を転送するリクエストに追加する」という方法が昔からよく行われてきました。
RFC 9440 は、この HTTP ヘッダの名前として Client-Cert
を定義し、標準化しました。
RFC 9440 は、クライアント証明書を HTTP ヘッダの値として埋め込む際のフォーマットも標準化しました。
それまでは、PEM フォーマット (RFC 7468)
を使っている実装がよく見られましたが、改行の有無や BEGIN
/ END
バウンダリーの有無などの揺れがあり、互換性は低い状態でした。
RFC 9440 は、フォーマットを「DER エンコード (X.690)
されたクライアント証明書をあらわすバイトシーケンス
(RFC 8941 Section 3.3.5)」と定めました。
まとめ
RFC 9449、通称 DPoP は、送信者限定アクセストークンを実現する方法の一つです。
DPoP では、アクセストークン生成時、クライアントアプリケーションが用意した鍵ペアの公開鍵の方をアクセストークンと紐付けておきます。 そして、アクセストークン利用時にアクセストークンと併せて「アクセストークンに紐付いている公開鍵とペアとなっている秘密鍵を持っていること示す証拠」も要求することで、送信者限定を実現します。
その証拠は、公開鍵を埋め込んだデータに秘密鍵で署名することで生成します。いわゆる自己署名トークンです。
自己署名トークン受信者は、自己署名トークンに埋め込まれた公開鍵を取り出し、その公開鍵で自己署名トークンの署名を検証します。 検証がパスした場合、そのトークンを生成した者はペアとなっている秘密鍵を持っていると言えます。
それでは、DPoP の細かい手順を見ていきましょう。
クライアントアプリケーションは、まず、鍵ペアを作成します。 作成した鍵ペアの公開鍵は、以降の処理で生成されるアクセストークンに紐付けられることになります。
次に、PoP を作成します。RFC 9449 で定義される PoP は DPoP proof JWT と呼ばれ、仕様が細かく決められています。要点は次の通りです。
typ
ヘッダパラメータの値は dpop+jwt
jwk
ヘッダパラメータにアクセストークンに紐付ける公開鍵をセットするhtm
クレームの値は、送信予定のリクエストの HTTP メソッドhtu
クレームの値は、送信予定のリクエストのターゲット URI (ただしクエリパラメータ、フラグメントパラメータを除く)jti
クレーム、iat
クレームも必須jwk
ヘッダパラメータに指定した公開鍵とペアになっている秘密鍵で署名する認可サーバのトークンエンドポイントにトークンリクエストを送ります。
この際、事前に作成しておいた DPoP proof JWT を DPoP
ヘッダの値としてリクエストに含めます。
認可サーバのトークンエンドポイントの実装は、トークンリクエストから DPoP proof JWT を取り出します。
その DPoP proof JWT のヘッダから公開鍵を取り出し、その公開鍵で DPoP proof JWT の署名を検証します。
その後、トークンリクエストが有効であればアクセスストークンを生成し、それをデータベースに保存します。 ただし、アクセストークンが情報を自分自身に埋め込む形式の場合 (JWT アクセストークンなど)、データベースにデータを書き込まない実装もありえます。
このとき、ハッシュ関数として SHA-256 (NIST FIPS 180-4) を用いて公開鍵の JWK Thumbprint (RFC 7638) を計算し、アクセストークンと紐付けて覚えておきます。
このように、DPoP proof JWT に埋め込まれた公開鍵と紐付けられたアクセストークンのことを「DPoP-bound」アクセストークンと呼びます。
トークンエンドポイントは、生成したアクセストークンを含むトークンレスポンスをクライアントアプリケーションに返します。
リソースリクエストに先立ち、クライアントアプリケーションは DPoP proof JWT を作成します。
なお、DPoP proof JWT をアクセストークンと同時に使う場合、アクセストークンの
SHA-256 ハッシュを base64url (RFC 4648) エンコードしたものを ath
クレームの値としてペイロードに含める必要があります。
DPoP proof JWT が用意できたら、リソースリクエストをおこないます。
アクセストークンは Authorization
ヘッダに、DPoP proof JWT は
DPoP
ヘッダに埋め込みます。Authorization
ヘッダ値のスキーム部を
Bearer
(RFC 6750) ではなく DPoP
とする点に注意してください。
リソースリクエストを受け取ったリソースサーバは、リクエストから DPoP proof JWT を取り出し、そのヘッダに埋め込まれている公開鍵を用いて署名検証をおこないます。
リソースサーバは認可サーバのイントロスペクションエンドポイントに問い合わせ、アクセストークンの情報を得ます。
イントロスペクションエンドポイントの実装は、アクセストークンが DPoP-bound であれば、紐付いている公開鍵の
JWK Thumbprint を、cnf
(RFC 7800) プロパティの jkt
サブプロパティの値としてイントロスペクションレスポンスに含めます。
リソースサーバは、DPoP proof JWT に含まれる公開鍵の JWK Thumbprint を計算し、その値がイントロスペクションレスポンス内の値と一致するかどうか確認します。 一致しなければ、リソースリクエストをおこなっているクライアントアプリケーションはアクセストークンの正当な保有者ではないと判断し、リソースリクエストを拒否します。
まとめ
サーバは、サーバが提供する nonce 値を DPoP proof JWT に含めることを要求する場合があります。 nonce 値に生存期間を設けることでサーバが DPoP proof JWT の有効期間を制御したり (RFC 9449 Section 8)、nonce 値を予測不能とすることで DPoP proof JWT を作り置きして他所で利用することを難しくしたり (RFC 9449 Section 11.2)、など、nonce を要求することにはセキュリティ上の利点があります。
nonce を要求するリソースサーバに nonce
クレームを含まない DPoP proof JWT
を送ると、DPoP-Nonce
ヘッダを含む use_dpop_nonce
エラーが返ってきます。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce"
DPoP-Nonce: ZgmFr7UWLrJX0fHB
このエラーを受けたクライアントアプリケーションは、DPoP-Nonce
ヘッダの値を取り出し、その値を
nonce
クレームの値として含む DPoP proof JWT を作成します。
そして、その新しく作成した DPoP proof JWT を添えてリソースリクエストを再送します。
クライアントアプリケーションは以降のリソースリクエストで同じ nonce 値を使い続けます。
しかし、いつかはその nonce の有効期間も終了します。そのため、同じ nonce
を使い続けると、最終的にはサーバから invalid_dpop_proof
エラーが返ってきます。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="invalid_dpop_proof"
DPoP-Nonce: HDoSFcMWFrfcWdr3
そのエラーレスポンスに DPoP-Nonce
ヘッダが含まれており、その値がこれまで使っていた
nonce 値と異なるなら、nonce 値が古いことがエラーの原因の可能性があります。
この場合、DPoP proof JWT を作り直してリソースリクエストの再送を試みる価値があります。
リソースサーバが DPoP をサポートする場合、受け取った DPoP proof JWT の htu
クレームの値が、リクエストのターゲット URI
(からクエリーパラメータとフラグメントパラメータを除いたもの)
と一致するか確認しなければなりません。この確認のため、リソースサーバはリクエストの絶対
URI を知る必要があります。
同様に、リソースサーバが HTTP メッセージ署名 (RFC 9421)
をサポートする場合、@target-uri
派生コンポーネント
(RFC 9421 Section 2.2.2)
の値として用いるため、リソースサーバはリクエストの絶対 URI を知る必要があります。
しかしながら、リソースサーバがリバースプロキシの後ろで動いている場合、リソースサーバはリクエストの絶対 URI を直接知ることはできません。というのは、リソースサーバが認識する HTTP リクエストは、クライアントアプリケーションがリバースプロキシに送ったものではなく、リバースプロキシがリソースサーバに送ったものであり、たいていの場合、スキーマ、ホスト名、ポート番号が、オリジナルのリクエスト (クライアントアプリケーションがリバースプロキシに送ったリクエスト) とは異なるからです。
クライアントアプリケーションが送ったリクエストとリソースサーバが受け取ったリクエストは異なるため、リソースサーバが受け取ったリクエストのターゲット
URI を DPoP proof JWT の htu
クレームの値と比較しても一致せず、DPoP
proof JWT を含むリソースリクエストは全て拒否されてしまいます。
同様に、クライアントアプリケーションから見た @target-uri
派生コンポーネントの値とリソースサーバが認識する @target-uri
は一致しないため、HTTP
メッセージ署名の検証はパスせず、リソースリクエストは全て拒否されてしまいます。
リソースサーバにおける DPop proof JWT や HTTP メッセージ署名の検証は、自身が受け取ったリクエストのターゲット URI ではなく、リバースプロキシが受け取ったリクエストのターゲット URI に基づいておこなわなければなりません。
リバースプロキシが HTTP リクエストを転送する際、オリジナルの HTTP リクエストに関する情報を含むカスタム HTTP ヘッダ群を追加するのは一般的に行われています。
この用途のため、RFC 7239 Forwarded HTTP Extension により
Forwarded
HTTP ヘッダが定義されました。例えば、オリジナルリクエストのスキームが https
、
ホスト名が rs.
であることを伝える場合、次のような
Forwarded
ヘッダが追加されるでしょう。
Forwarded: proto=https;host=rs.example.com
Forwarded
HTTP ヘッダは、それまで X-Forwarded-For
、X-Forwarded-By
、X-Forwarded-Proto
などの非標準 HTTP フィールドを使って実現していたことを標準化したものです。
ですので、Forwarded
HTTP ヘッダの利用が推奨されます。
しかしながら、Forwarded
HTTP ヘッダの値の構文は意外と複雑で、そのパース処理を
(不可能ではないにしても) 正規表現でおこなうのは難しく、また、RFC 8941
で定義される汎用構文とも異なるので汎用ライブラリを用いることもできません。
結局は、Forwarded
HTTP ヘッダ専用のパース処理を書かなければなりません
(例:http-field-parser)。
そのため、処理しやすいカスタム HTTP ヘッダ群は依然として広く使われています。 以下は、そのようなカスタム HTTP ヘッダ群の例です。
ヘッダ名 | 参照 |
---|---|
Front-End-Https |
[Microsoft] Helping to Secure Communication: Client to Front-End Server |
X-Forwarded-For |
MDN Web Docs / X-Forwarded-For |
X-Forwarded-Host |
MDN Web Docs / X-Forwarded-Host |
X-Forwarded-Port |
[AWS] HTTP headers and Classic Load Balancers # X-Forwarded-Port |
X-Forwarded-Proto |
MDN Web Docs / X-Forwarded-Proto |
X-Forwarded-Protocol |
MDN Web Docs / X-Forwarded-Proto |
X-Forwarded-Ssl |
MDN Web Docs / X-Forwarded-Proto |
X-Url-Scheme |
MDN Web Docs / X-Forwarded-Proto |
しかし、標準化された Forwarded
HTTP ヘッダや広く使われているカスタム
HTTP ヘッダ群は、ターゲット URI を求めるための手段としては不完全です。
というのは、パス部やクエリー部の情報を含まないからです。
HTTP メッセージ署名の @target-uri
派生コンポーネントを利用するためにはクエリー部の情報も必要なのですが、残念ながら現時点では、パス部やクエリー部も含む絶対
URL の情報を伝えるための標準化されたヘッダや広く使われているカスタムヘッダはありません。
絶対 URL を表す何らかのカスタム HTTP ヘッダ (例:X-Forwarded-URL
) の普及や、標準
HTTP ヘッダ (例:Target-URI
) の定義、新しい
HTTP Forwarded パラメータの追加などが望まれます。
HTTP メッセージは、幾つもの中継サーバを経由して届けられるのが一般的です。 その中継処理の間に、TLS 終端や HTTP ヘッダ群の追加・統合・変更などもよく行われます。 このような条件下において、送信者と受信者の間で (End-to-End で)、HTTP メッセージの完全性 (integrity) と真正性 (authenticity) を保証する方法が長年検討されてきました。
一般的に、完全性と真正性の実現方法はデジタル署名です。しかし、HTTP メッセージの場合、中継処理の途中で部分的に変更されうるので、単純に HTTP メッセージ全体を対象としてデジタル署名をおこなう方法は機能しません。 そのため、送信者と受信者の双方にとって意味のある部分のみを選択し、中継サーバによる変更の影響を受けないように正規化をおこない、その部分集合に対して署名をおこなう方法が必要になります。
この目的のため、かなりの数の競合する仕様案が提案されました。 実際に実装・運用される仕様案もありました。そして、最終的に IETF が採択したものが RFC 9421 HTTP Message Signatures となりました。
RFC 9421 では、署名対象となりうる HTTP メッセージの部分を HTTP メッセージコンポーネントと呼んでいます。
最も分かりやすい HTTP メッセージコンポーネントは、HTTP フィールドです。
例えば、Content-Type
HTTP フィールドや Date
HTTP フィールドは、それぞれ
HTTP メッセージコンポーネントの一種です。
一方、HTTP フィールド以外の属性から派生する HTTP メッセージコンポーネントもあり、派生コンポーネント (derived component) と呼ばれます。HTTP メソッドやターゲット URI、HTTP ステータスコードは派生コンポーネントの一種です。
どのコンポーネントを署名対象とするかは、アプリケーションがそれぞれの目的に応じて自由に選択します。 例えば、FAPI 2.0 Http Signatures という仕様では、HTTP リクエストに HTTP メッセージ署名をおこなう際、下記のコンポーネント群を署名対象とすると定めています。
Authorization
HTTP フィールドDPoP
HTTP フィールド (存在する場合)Content-Digest
HTTP フィールド (メッセージボディを持つ場合)HTTP メッセージ署名では、デジタル署名の入力となる文字列をシグネチャベース (Signature Base) と呼びます。 シグネチャベースは、署名対象の HTTP メッセージコンポーネントの識別子と値の組を列挙し、末尾に署名に関するメタデータを付加したものです。
HTTP メッセージ署名の生成や検証に先立って、このシグネチャベースを組み立てる必要があります。 シグネチャベースの正確な構文については RFC 9421 の Section 2.5. Creating the Signature Base を参照してください。
下記は RFC 9421 から抜粋したシグネチャベースの例です。
"@method": POST
"@authority": example.com
"@path": /foo
"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
"content-length": 18
"content-type": application/json
"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"
このシグネチャベースの例では、1 行目から 3 行目が派生コンポーネントの識別子と値の組となっています。派生コンポーネントのコンポーネント名は、@method
のように @
で始まります。一方、4 行目から 6 行目は HTTP フィールドとその値の組です。
シグネチャベースの最後の行 (この例では 7 行目) には、シグネチャのメタデータ情報が置かれます。必ず "@signature-params":
で始まり、その後に内部リスト (RFC 8941 Section 3.1.1) が続きます。この内部リストには、署名対象の HTTP メッセージコンポーネント群のコンポーネント識別子 (component identifier) が列挙されます。この例では次の 6 つが含まれています。
"@method"
"@authority"
"@path"
"content-digest"
"content-length"
"content-type"
丸括弧で囲まれたコンポーネント識別子群の後ろに続く、セミコロン (;
)
で始まる部分は、内部リストのオプショナルパラメータ群です。
この例では、created
パラメータと keyid
パラメータが含まれています。
これらのパラメータは RFC 9421 Section 2.3 で定義されています。
RFC 9421 Section 3.3 で HTTP_SIGN
と抽象的に表現される署名メソッドは、シグネチャベース (M
) と署名鍵 (Ks
)
を入力として受け取り、署名 (S
) を出力します。
HTTP_SIGN (M, Ks) -> S
RFC 9421 Section 3.3 には署名アルゴリズムが列挙されており、IANA の HTTP Signature Algorithms レジストリには次のアルゴリズム群が登録されています。
アルゴリズム名 | 説明 | 参照 |
---|---|---|
rsa-pss-sha512 |
RSASSA-PSS using SHA-512 | RFC 9421, Section 3.3.1 |
rsa-v1_5-sha256 |
RSASSA-PKCS1-v1_5 using SHA-256 | RFC 9421, Section 3.3.2 |
hmac-sha256 |
HMAC using SHA-256 | RFC 9421, Section 3.3.3 |
ecdsa-p256-sha256 |
ECDSA using curve P-256 DSS and SHA-256 | RFC 9421, Section 3.3.4 |
ecdsa-p384-sha384 |
ECDSA using curve P-384 DSS and SHA-384 | RFC 9421, Section 3.3.5 |
ed25519 |
EdDSA using curve edwards25519 | RFC 9421, Section 3.3.6 |
OAuth 2.0 や OpenID Connect の文脈で馴染みのある JWS アルゴリズム群については、RFC 9421 の Section 3.3.7. JSON Web Signature (JWS) Algorithms で明示的に言及されています。これらのアルゴリズムを用いる場合、シグネチャベースを JWS Signing Input (RFC 7515) として用いることと定められています。
HTTP メッセージ署名には JWS ヘッダに相当するものが存在しないので、署名アルゴリズムを別の方法で署名検証者に伝える必要があります。
この目的のため、RFC 9421 Section 2.3 で定義されている alg
パラメータを用いるのが適切に思えます。しかしながら、RFC 9421 Section 3.3.7
の最終段落でわざわざ次のように述べているため、alg
パラメータは用いません。
JSON Web Algorithm (JWA) values from the “JSON Web Signature and Encryption Algorithms” registry are not included as signature parameters. Typically, the JWS algorithm can be signaled using JSON Web Keys (JWKs) or other mechanisms common to JOSE implementations. In fact, JWA values are not registered in the “HTTP Signature Algorithms” registry (Section 6.2), and so the explicit
alg
signature parameter is not used at all when using JOSE signing algorithms.(日本語訳) 「JSON Web Signature and Encryption Algorithms」レジストリにある JSON Web Algorithm (JWA) の値は、署名パラメータには含まれません。通常、JWS アルゴリズムは JSON Web Key (JWK) や、JOSE 実装で一般的に用いられるその他の仕組みによって通知されます。 実際、JWA の値は「HTTP Signature Algorithms」レジストリ (セクション 6.2) には登録されていないため、JOSE の署名アルゴリズムを使用する際に署名パラメータとして明示的な
alg
が使われることはありません。
RFC 9421 Section 3.3 で HTTP_VERIFY
と抽象的に表現される検証メソッドは、シグネチャベース (M
)、検証鍵 (Kv
)、署名 (S
)
を入力として受け取り、検証結果 (V
) を出力します。
HTTP_VERIFY (M, Kv, S) -> V
検証処理の入力のうち、検証鍵は HTTP メッセージに含まれていないため、何らかの方法で入手する必要がありますが、 RFC 9421 はその入手方法を定めていません。
HTTP リクエストの HTTP メッセージ署名をリソースサーバが検証しようとする場合、一見、次の手順で検証鍵を入手できそうに思えます。
jwks_uri
メタデータの情報を取得するjwks_uri
が指す場所から JWK Set を取得するkeyid
の値に基づき、JWK Set 内にある検証鍵を特定するしかし、よくよく検討してみると、リソースサーバの文脈では、標準仕様だけではクライアントアプリケーションの
jwks_uri
メタデータの値を取得できないことが分かります。
個人による仕様案を除くと、唯一利用可能な標準仕様は OpenID Federation
(解説記事) のみですが、この仕様の実装と運用は重い作業であり、
当仕様が広く普及しているとも言い難いので、検証鍵の入手方法としては汎用解とはなりません。
そのため、現状では、リソースサーバが HTTP リクエストの HTTP メッセージ署名を検証するためには、標準化されていない実装固有の方法で検証鍵を入手しなければなりません。
HTTP メッセージ署名は、Signature
HTTP フィールドと Signature-Input
HTTP
フィールドを用いて HTTP メッセージに付加します。
Signature
HTTP フィールドの値のフォーマットはディクショナリ
(RFC 8941 Section 3.2) です。
個々のキー・バリューの組は、任意のラベルと、バイトシーケンス
(RFC 8491 Section 3.3.5) で表現された署名です。
Signature: ラベル=:Base64エンコードされた署名:, ラベル=:Base64エンコードされた署名:, ...
Signature-Input
HTTP フィールドの値のフォーマットもディクショナリ
(RFC 8941 Section 3.2) です。
個々のキー・バリューの組は、任意のラベルと、内部リスト
(RFC 8941 Section 3.1.1) で表現された署名メタデータです。
Signature-Input: ラベル=(コンポーネント識別子群)任意パラメータ群, ラベル=(コンポーネント識別子群)任意パラメータ群, ...
ラベルは任意に付けられますが、Signature
HTTP フィールド内の使われたラベルと同じラベルが
Signature-Input
HTTP フィールド内にも存在しなければなりません。
下記は RFC 9421 Section 4.3 から抜粋した Signature
HTTP
フィールドと Signature-Input
HTTP フィールドを含む HTTP リクエストの例です。
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Length: 18
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "content-type" "content-length");created=1618884475;keyid="test-key-ecc-p256"
Signature: sig1=:X5spyd6CFnAG5QnDyHfqoSNICd+BUP4LYMz2Q0JXlb//4Ijpzp+kve2w4NIyqeAuM7jTDX+sNalzA8ESSaHD3A==:
{"hello": "world"}
RFC 9421 自身が述べているように、当仕様はツールとして設計されているため、実際に利用する際はアプリケーションやプロファイルの目的に合わせて要件を追加する必要があります。 RFC 9421 の Section 1.4 にはそのような要件が例示されています。 例えば、次のような項目が挙げられています。
FAPI 2.0 仕様ファミリーの一つである FAPI 2.0 Http Signatures は、リソースサーバへのリソースリクエストとリソースサーバからリソースレスポンスに HTTP メッセージ署名を適用するために RFC 9421 をプロファイリングした仕様で、具体的な要件を定めています。
仕様の要約は次の通りです。なお、仕様策定作業中のため、今後仕様が変更される可能性があります。
リクエスト署名 | ||
---|---|---|
コンポーネント | "@method" |
|
"@target-uri" |
||
"authorization" |
||
"dpop" |
DPoP が使われている場合 | |
"content-digest" |
メッセージボディがある場合 | |
メタデータパラメータ | created |
|
tag |
値は "fapi-2-request" で固定 |
レスポンス署名 | ||
---|---|---|
コンポーネント | "@method";req |
|
"@target-uri";req |
||
"authorization";req |
||
"dpop";req |
DPoP が使われている場合 | |
"content-digest";req |
リクエストにメッセージボディがある場合 | |
"@status" |
||
"content-digest" |
レスポンスにメッセージボディがある場合 | |
メタデータパラメータ | created |
|
tag |
値は "fapi-2-response" で固定 |
FAPI 2.0 Http Signatures 仕様を用いる場合、 FAPI 2.0 Security Profile 仕様も適用されるため、リソースリクエストの際は MTLS または DPoP を用いた送信者限定アクセストークンが用いられます。
MTLS と DPoP のどちらの場合でも、リソースサーバにはクライアントアプリケーションの公開鍵が渡ってきます。 この技術的特性を念頭に置き、ここで「送信者限定アクセストークンに紐付いた公開鍵とペアとなっている秘密鍵で HTTP リクエストに署名する」という約束事を導入すると「検証メソッド」で言及した検証鍵取得問題を解決することができます。
この解決方法の良い点は、承認済みの標準仕様 (RFC 8705 と RFC 9449) のみで検証鍵を取得できることです。標準化されていない実装固有の方法を避けることができるため、相互運用性が高まります。
クライアントアプリケーションがリソースレスポンスの HTTP メッセージ署名を検証するためには、リソースサーバが提供する検証鍵を入手する必要があります。
クライアントアプリケーションによる検証鍵提供に比べ、リソースサーバによる検証鍵提供は分かりやすい方法で実現できます。 認可サーバや OpenID プロバイダがそうしているように、リソースサーバも JWK Set ドキュメント (RFC 7517) を公開し、その文書内に HTTP メッセージ署名を検証するための検証鍵を入れておけばよいのです。
JWK Set ドキュメントの公開場所については、
/.well-known/
で公開するリソースサーバメタデータ (RFC 9728) 内の jwks_uri
プロパティにより示すことができます。
JWK Set ドキュメントにより検証鍵を提供する場合、複数存在する鍵の中から検証鍵を特定することを可能とするため、鍵
(JWK) および HTTP メッセージ署名の双方に同じ鍵識別子を付けておくのがよいでしょう。
鍵には kid
プロパティ (RFC 7517 Section 4.5) で、HTTP
メッセージ署名には keyid
パラメータ (RFC 9421 Section 2.3)
で鍵識別子を関連付けることができます。
アクセストークンのスコープ (RFC 6749 Section 3.3) は、そのアクセストークンが持つ粗粒度の権限を表しています。
より細かく権限を表現するため、認可サーバの実装によっては、スコープ名に構造を持たせて一部を変数として扱う仕組みを導入しました。 そのような仕組みはパラメータ化スコープ (parameterized scope) や動的スコープ (dynamic scope) などと呼ばれます。もちろん実装間に互換性はありません。
実装が幾つもあるとはいえ、スコープ名に情報を詰め込むのは、率直に言うとバッドプラクティスです。
この状況を改善するために RFC 9396 OAuth 2.0 Rich Authorization Requests
(通称 RAR) が策定されました。
RAR 仕様は、権限を構造的に表現するための汎用的な仕組みとして、authorization_
JSON 配列を定義しました。
authorization_
JSON 配列の各要素は JSON オブジェクト
(以降 RAR オブジェクト) です。RAR オブジェクトが必ず持たなければならない唯一のプロパティは
type
です。この type
プロパティは、その RAR オブジェクトがどのような構造を持っているかを示します。
RFC 9396 自体は type
プロパティの値を定めていません。
どのような値を用いるかは、アプリケーションやプロファイルが独自に決めます。
例えば、OpenID for Verifiable Credential Issuance
(OID4VCI) 仕様では openid_
という値を定義しました。
以下は同仕様から抜粋した authorization_
配列の例です。
[
{
"type": "openid_credential",
"credential_configuration_id": "UniversityDegreeCredential"
}
]
RAR 仕様は、type
プロパティの他に次のようなプロパティ群を定義しています。
これらを利用するか否かは自由です。
locations
actions
datatypes
identifier
privileges
先ほど挙げた OID4VCI 仕様での利用例も示す通り、アプリケーションやプロファイルが
RAR オブジェクトに独自のプロパティを追加してもかまいません。下記は RFC 9396
から抜粋した authorization_
配列の例です。
geolocation
や currency
といった、RFC 9396
では定義していないトップレベルプロパティが使われています。
[
{
"type": "photo-api",
"actions": [
"read",
"write"
],
"locations": [
"https://server.example.net/",
"https://resource.local/other"
],
"datatypes": [
"metadata",
"images"
],
"geolocation": [
{
"lat": -32.364,
"lng": 153.207
},
{
"lat": -35.364,
"lng": 158.207
}
]
},
{
"type": "financial-transaction",
"actions": [
"withdraw"
],
"identifier": "account-14-32-32-3",
"currency": "USD"
}
]
RAR 仕様によれば、scope
パラメータが利用可能な全ての箇所で (実装がサポートしていれば)
authorization_
パラメータを使えるとされています。
リソースサーバにとっては、イントロスペクションレスポンスや JWT
アクセストークンのペイロード部の authorization_
パラメータが重要になります。
下記は RFC 9396 Section 9.2. Token Introspection から抜粋したイントロスペクションレスポンスの例です。
{
"active": true,
"sub": "24400320",
"aud": "s6BhdRkqt3",
"exp": 1311281970,
"acr": "psd2_sca",
"txn": "8b4729cc-32e4-4370-8cf0-5796154d1296",
"authorization_details": [
{
"type": "https://scheme.example.com/payment_initiation",
"actions": [
"initiate",
"status",
"cancel"
],
"locations": [
"https://example.com/payments"
],
"instructedAmount": {
"currency": "EUR",
"amount": "123.50"
},
"creditorName": "Merchant123",
"creditorAccount": {
"iban": "DE02100100109307118603"
},
"remittanceInformationUnstructured": "Ref Number Merchant"
}
],
"debtorAccount": {
"iban": "DE40100100103307118608",
"user_role": "owner"
}
}
Cedar は認可ポリシーを記述するための言語です (Cedar Policy Language Reference Guide)。Cedar で記述された認可ポリシー (以降 Cedar ポリシー) を、リソースアクセスを許可するかどうかの判定に利用することができます。
Cedar の大きな利点の一つ目は、認可ロジックをビジネスロジックから切り離すことができることです。Cedar ポリシーは外部化が可能で、ビジネスロジックとは別の場所で管理することができます。実際に Cedar ポリシーを管理する商用サービスも存在します (Integrations 参照)。
Cedar の大きな利点の二つ目は、細かい粒度で認可ロジックを記述できることです。 例えば、「Jane の友達であれば、Jane の旅行アルバム内の写真を見てコメントすることを許可する」という認可ロジックを記述できます。 下記は Cedar Policy Language Reference Guide の Example scenario から抜粋した Cedar ポリシーの例で、この認可ロジックを表現しています。
permit ( principal in Group::"janeFriends", action in [Action::"view", Action::"comment"], resource in Album::"janeTrips" );
Cedar ポリシーには、when
節や unless
節を足してポリシーを適用する条件を指定することができます。
それらの条件の中で、比較演算子、論理演算子、算術演算子、などを使うこともでき、高い表現力があります。
permit ( principal, action == Action::"read", resource ) when { resource.owner == principal || resource.tag == "public" };
アクセストークンにスコープ (RFC 6749 Section 3.3) を紐付けることで、そのアクセストークンの粗粒度の権限を表現することができます。 同様にして、もしもアクセストークンに Cedar ポリシーを紐付けることができれば、 そのアクセストークンの細粒度の権限を表現することができるのではないでしょうか?
このアイディアを実現する手段として有力視されているのが、Cedar ポリシーを RAR オブジェクトとして表現する方法です。
表現方法の詳細を決めるにあたり、考慮すべき点として次のものがあります。
Cedar ポリシーのオリジナルの記法は独特。その独特の記法のまま RAR
オブジェクトに埋め込むべきか、それとも JSON 形式
(JSON Policy Format) に変換して埋め込むべきか?
JSON 形式は RAR との親和性は高いが、冗長であるため (特に when
/unless
条件部の表現)、Cedar コミュニティの大半の人々はオリジナル記法の方を好むと思われる。
Cedar ポリシーのオリジナル記法と JSON 形式、両方サポートすべきか、それともどちらかに限定するべきか?
両方の書式をサポートする場合、どちらの書式が使われているかをどのように示すべきか?
type
プロパティの値に書式の情報を埋め込むか、それとも書式を示すプロパティ
(format
等) を別途設けるべきか?
ポリシーセットを一つの RAR オブジェクトとして表現すべきか、それとも一つの RAR
オブジェクトで一つのポリシーのみを表現するべきか?
「Representing a policy set with JSON」で示されているポリシーセットの
JSON 表現では、一つの JSON オブジェクト内に staticPolicies
、templates
、templateLinks
が含まれているが、この表現方法に倣うべきか? そもそも RAR の文脈で templates
のサポートは必要なのか?
Cedar ポリシー以外のもの (例えば Cedar スキーマ) を表現する必要に迫られる可能性はあるか?
その可能性を見越したとき、type
の値はどうすべきか?
下記は “Cedar in RAR Object” 仕様案の一例です。
Cedar in RAR Object 仕様案 | |||
---|---|---|---|
プロパティ | type |
値は "cedar-policy" で固定。
|
|
format |
任意 |
"cedar" (デフォルト) または "json" 。
|
|
policy_id |
任意 | ポリシー ID。JSON 文字列。 | |
policy |
必須 |
Cedar ポリシー。format プロパティの値が "cedar"
であれば、Cedar オリジナル記法で書かれた Cedar ポリシーを含む単一の JSON 文字列。
format プロパティの値が "json"
であれば、JSON
Policy Format で記述された単一の Cedar ポリシーを表す JSON オブジェクト。
|
|
エンティティ | principal |
アクセストークンにサブジェクトが紐付いていれば (イントロスペクションレスポンスや JWT
アクセストークンのペイロードに sub クレームがあれば)、principal
のエンティティは User 。そうでなければ Client 。
User エンティティの場合はアクセストークンの sub の値を一意識別子として扱う。
Client エンティティの場合はアクセストークンの client_id の値を一意識別子として扱う。
アクセストークンの groups 、roles 、entitlements
(RFC 9068
Section 2.2.3) の各要素はそれぞれ Group 、Role 、Entitlement
エンティティにマッピングされ、principal の親エンティティとして設定される。
RFC 7643
Section 2.4 に従っていれば、各要素の値は JSON オブジェクトであり、value
プロパティを持っているはずである
(例: "groups" )。
その value プロパティの値をエンティティの一意識別子として扱う。
RFC 7643
Section 2.4 に従っていない場合、JSON 文字列と仮定し
(例: "groups" )、その文字列の値をエンティティの一意識別子として扱う。
|
|
action |
action のエンティティはリソースサーバが定義する。
|
||
resource |
resource のエンティティはリソースサーバが定義する。
|
||
コンテキスト | time |
Unix epoch からの経過ミリ秒数で表現された現在時刻。 | |
access_token |
アクセストークンの情報。サブプロパティとして
iss 、sub 、client_id を持つ。
|
||
ip_address |
クライアントの IP アドレス。 |
下記は、この仕様案に基づいて記述された authorization_
配列の例です。一つ目の RAR オブジェクトは Cedar オリジナル記法で Cedar ポリシーを記述し、二つ目の
RAR オブジェクトは JSON Policy Format で Cedar ポリシーを記述しています。
[
{
"type": "cedar-policy",
"format": "cedar",
"policy_id": "policy-0",
"policy": "permit (principal == User::\"12UA45\", action == Action::\"view\", resource);"
},
{
"type": "cedar-policy",
"format": "json",
"policy_id": "policy-1",
"policy": {
"effect": "permit",
"principal": { "op": "==", "entity": { "type": "User", "id": "12UA45" } },
"action": { "op": "==", "entity": { "type": "Action", "id": "view" } },
"resource": { "op": "All" }
}
}
]
リソースサーバが提供する API の中には、他の API よりも高いセキュリティ要件を設けているものがあるかもしれません。 例えば、残高照会 API よりも送金 API の方が高いセキュリティを要求するのは一般的です。
そのようなセキュリティ要件の中で、「アクセストークン取得時に通常よりも高いユーザ認証要件を満たさなければならない」というものがありえます。
提示されたアクセストークンが取得された際に実行されたユーザ認証が、求める基準に満たない場合、API はその旨をクライアントアプリケーションに通知し、アクセストークンの再取得を促したいと思うでしょう。 このようなケースで利用できるのが、 RFC 9470 OAuth 2.0 Step Up Authentication Challenge Protocol です。
API は、エラーコードとして insufficient_
を用いることで、アクセストークン取得時のユーザ認証が要件を満たしていないことを通知できます。
エラーレスポンスには、acr_
パラメータや
max_
パラメータを用いて要件に関する情報を追加することができます。
下記は RFC 9470 から抜粋したエラーレスポンスの例です。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication", error_description="A different authentication level is required.", acr_values="myACR"
RFC 9470 については『RFC 9470 OAuth 2.0 ステップアップ認証チャレンジプロトコル』で詳しく解説しているので、そちらを参照してください。
OIDF の AuthZEN ワーキンググループ では Authorization API 1.0 仕様の開発が進められています。
この仕様の中心となるのは Access Evaluation API です。 アクセスを許可するかどうかを判定するのに必要な情報をこの API に渡すと、判定結果を真偽値で返してくれます。 下記は、この API に対するリクエストとレスポンスの例です。
{
"subject": {
"type": "user",
"id": "alice@acmecorp.com"
},
"resource": {
"type": "account",
"id": "123"
},
"action": {
"name": "can_read",
"properties": {
"method": "GET"
}
},
"context": {
"time": "1985-10-26T01:22-07:00"
}
}
{
"decision": true
}
入力するパラメータはサブジェクト、リソース、アクション、コンテキストで、出力されるパラメータは判定結果です。 入出力の構成要素は正しいとは思うものの、直感としては、入力要素を全て一極集中で管理しているサーバ (ユーザの管理もリソースの管理も一手に担っているサーバ) でなければ Access Evaluation API を実装できなさそうに思えます。今後どのような実装が出てくるのかに注目です。
2012 年に公開された OAuth 2.0 の中心仕様である RFC 6749 の時点では、API アクセス制御に利用できるのはスコープ程度しかありませんでしたが、その後、様々な機能が追加されていきました。
中でも、MTLS や DPoP による送信者限定は重要であり、FAPI 2.0 ではどちらかの実装が必須とされています。 2022 年 4 月、Heroku / Travis-CI から盗まれたアクセストークンが他所 (GitHub) で悪用される事件が発生しましたが、この事件は送信者限定機能の重要性を知らしめました。
Authlete 社は、RFC 9449 (DPoP) や FAPI 2.0 Security Profile、FAPI 2.0 Http Signatures などの高度なセキュリティ関連仕様の策定作業に貢献しています (著者として仕様書に名前が記載されています)。また、それらの商用実装を提供しています。
API 保護の専門知識や商用実装をお求めの方は、是非お問い合せください。