FAPI : JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) とは

ritou です。

みんな大好きJWT。今日もみんなで eyJ!ということで、今回はこちらの仕様について説明します。

openid.net

概要

This document defines a new JWT-based mode to encode authorization responses. Clients are enabled to request the transmission of the authorization response parameters along with additional data in JWT format. This mechanism enhances the security of the standard authorization response since it adds support for signing and encryption, sender authentication, audience restriction as well as protection from replay, credential leakage, and mix-up attacks. It can be combined with any response type.

この仕様を3行で説明しろと言われたらこんな感じでしょう。

  • JWT形式で Authorization Response を返すための仕様です
  • Response Mode を指定するための response_mode パラメータも拡張する
  • Client / Authorization Server それぞれの metadata も拡張する

まずは Response Mode の振り返りからはじめましょう。

これまでの Response Mode とは

response_mode パラメータが定義されているのは、この仕様です。

openid.net

Response Mode

The Response Mode determines how the Authorization Server returns result parameters from the Authorization Endpoint. Non-default modes are specified using the response_mode request parameter. If response_mode is not present in a request, the default Response Mode mechanism specified by the Response Type is used.

Response Mode は Authorization Server が(認可処理の)結果のパラメータを Authorization Endpoint から「どのように返すか」を決定します。
デフォルト値ではないmodeは response_mode というリクエストパラメータを用いて指定され、指定されない場合は Response Type に対応した デフォルトの Response Mode が利用されます。

OAuth 2.0 の Response Type で表現すると

  • response_type=code : いわゆる Authorization Code Grant であり、Authorization Response はクエリパラメータとして指定される -> デフォルトの Response Mode は query
  • response_type=fragment : いわゆる Implicit Grant であり、Authorization Response はフラグメント部分に含まれる -> デフォルトの Response Mode は fragment

という感じになります。

また、こちらの仕様により response_mode パラメータに form_post が追加されました。

openid.net

例を見て見ましょう。まず Authorization Request に response_mode パラメータが指定されます。

GET /authorize?
   response_type=id_token
   &response_mode=form_post
   &client_id=some_client
   &scope=openid
   &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback
   &state=DcP7csa3hMlvybERqcieLHrRzKBra
   &nonce=2T1AgaeRTGTMAJyeDMN9IJbgiUG HTTP/1.1
  Host: server.example.com

Authorization Response は HTML Form を用いた POST リクエストにより返されます。

  HTTP/1.1 200 OK
  Content-Type: text/html;charset=UTF-8
  Cache-Control: no-cache, no-store
  Pragma: no-cache

  <html>
   <head><title>Submit This Form</title></head>
   <body onload="javascript:document.forms[0].submit()">
    <form method="post" action="https://client.example.org/callback">
      <input type="hidden" name="state"
       value="DcP7csa3hMlvybERqcieLHrRzKBra"/>
      <input type="hidden" name="id_token"
       value="eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJzdWIiOiJqb2huIiw
         iYXVkIjoiZmZzMiIsImp0aSI6ImhwQUI3RDBNbEo0c2YzVFR2cllxUkIiLC
         Jpc3MiOiJodHRwczpcL1wvbG9jYWxob3N0OjkwMzEiLCJpYXQiOjEzNjM5M
         DMxMTMsImV4cCI6MTM2MzkwMzcxMywibm9uY2UiOiIyVDFBZ2FlUlRHVE1B
         SnllRE1OOUlKYmdpVUciLCJhY3IiOiJ1cm46b2FzaXM6bmFtZXM6dGM6U0F
         NTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZCIsImF1dGhfdGltZSI6MTM2Mz
         kwMDg5NH0.c9emvFayy-YJnO0kxUNQqeAoYu7sjlyulRSNrru1ySZs2qwqq
         wwq-Qk7LFd3iGYeUWrfjZkmyXeKKs_OtZ2tI2QQqJpcfrpAuiNuEHII-_fk
         IufbGNT_rfHUcY3tGGKxcvZO9uvgKgX9Vs1v04UaCOUfxRjSVlumE6fWGcq
         XVEKhtPadj1elk3r4zkoNt9vjUQt9NGdm1OvaZ2ONprCErBbXf1eJb4NW_h
         nrQ5IKXuNsQ1g9ccT5DMtZSwgDFwsHMDWMPFGax5Lw6ogjwJ4AQDrhzNCFc
         0uVAwBBb772-86HpAkGWAKOK-wTC6ErRTcESRdNRe0iKb47XRXaoz5acA"/>
    </form>
   </body>
  </html>

最終的に、Client に送られるのは以下のようなPOSTリクエストになります。

  POST /callback HTTP/1.1
  Host: client.example.org
  Content-Type: application/x-www-form-urlencoded

  id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJzdWIiOiJqb2huIiwiYX
         VkIjoiZmZzMiIsImp0aSI6ImhwQUI3RDBNbEo0c2YzVFR2cllxUkIiLCJpc
         3MiOiJodHRwczpcL1wvbG9jYWxob3N0OjkwMzEiLCJpYXQiOjEzNjM5MDMx
         MTMsImV4cCI6MTM2MzkwMzcxMywibm9uY2UiOiIyVDFBZ2FlUlRHVE1BSnl
         lRE1OOUlKYmdpVUciLCJhY3IiOiJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTD
         oyLjA6YWM6Y2xhc3NlczpQYXNzd29yZCIsImF1dGhfdGltZSI6MTM2MzkwM
         Dg5NH0.c9emvFayy-YJnO0kxUNQqeAoYu7sjlyulRSNrru1ySZs2qwqqwwq
         -Qk7LFd3iGYeUWrfjZkmyXeKKs_OtZ2tI2QQqJpcfrpAuiNuEHII-_fkIuf
         bGNT_rfHUcY3tGGKxcvZO9uvgKgX9Vs1v04UaCOUfxRjSVlumE6fWGcqXVE
         KhtPadj1elk3r4zkoNt9vjUQt9NGdm1OvaZ2ONprCErBbXf1eJb4NW_hnrQ
         5IKXuNsQ1g9ccT5DMtZSwgDFwsHMDWMPFGax5Lw6ogjwJ4AQDrhzNCFc0uV
         AwBBb772-86HpAkGWAKOK-wTC6ErRTcESRdNRe0iKb47XRXaoz5acA&
  state=DcP7csa3hMlvybERqcieLHrRzKBra

Response Mode についてなんとなくわかりましたでしょうか。 今回の仕様では、この辺りをさらに拡張するぞ!って話です。

JWT形式で Authorization Response を返すための Response Mode

仕様では Authorization Response に含まれる JWT の内容から先に定義されていますが、Response Mode のあたりから説明していきます。

この仕様で定義されている response_mode パラメータは次の4つです。

  • query.jwt : JWT形式の Authorization Response をクエリパラメータに指定する
  • fragment.jwt : JWT形式の Authorization Response をフラグメントに指定する
  • form_post.jwt : JWT形式の Authorization Response を HTTP POST で送る
  • jwt : Response Type に対応した Response Mode にて JWT形式の Authorization Response を返す
    • response_mode=jwt&response_type=code : response_mode=query.jwt
    • response_mode=jwt&response_type=(code, none以外) : response_mode=fragment.jwt

Authorization Response の例の前に、response_type パラメータ毎にどのようなJWTになるかを見ていきましょう。

JWT Response Document

JWTは以下のデータを必ず含みます。

  • iss : レスポンスを作成した Authorization Server の Issuer URL
  • aud : レスポンスの対象である Client の client_id
  • exp : JWT の有効期限

さらに、JWTはエラーレスポンスの場合でも Authorization Endpoint のレスポンスパラメータを含みます。 複数の Response Type の組み合わせに対しても適用されますが、この仕様では code, token について示します。 OIDC の Session Management のような拡張仕様で定義されているレスポンスパラメータもJWTに含まれます。

Response Type が "code" である時の Authorization Response

いわゆる Authorization Code Grant のレスポンスが JWT に含まれます。

  • code : Authorization Code
  • state : リクエストに含まれる state パラメータ

JWT の Payload は以下のようになります。

{
   "iss":"https://accounts.example.com",
   "aud":"s6BhdRkqt3",
   "exp":1311281970,
   "code":"PyyFaux2o7Q0YfXBU32jhw.5FXSQpvr8akv9CeRDSd0QA",
   "state":"S8NJ7uqk5fY4EjNvP_G_FtyJu6pUsvH9jsYni9dMAJw"
}

エラーレスポンスの時は JWT に RFC6749 の sections 4.1.2.1 で定義されているエラーレスポンスパラメータを含みます。

  • error
  • error_description (OPTIONAL) - 人間が読めるようなエラー内容
  • error_uri (OPTIONAL) - エラーの内容についてのURL
  • state

JWT の Payload は以下のようになります。

{
   "error":"access_denied",
   "state":"S8NJ7uqk5fY4EjNvP_G_FtyJu6pUsvH9jsYni9dMAJw"
}

エラーレスポンスについては Client に戻すべきエラーとそうでないものがあるので、その辺りは今まで通り気をつける必要がありそうです。

Response Type が "token" である時の Authorization Response

Implicit Grant の場合は次のようになります。

  • access_token - Access Tokenの値
  • token_type - Access Token の種類。 "Bearer" など
  • expires_in - Access Token の有効期限
  • scope - Access Token の Scope
  • state

RFC6749 と照らし合わせると、Scope は オプションですかね。

{
   "iss":"https://accounts.example.com",
   "aud":"s6BhdRkqt3",
   "exp":1311281970,
   "access_token":"2YotnFZFEjr1zCsicMWpAA",
   "state":"S8NJ7uqk5fY4EjNvP_G_FtyJu6pUsvH9jsYni9dMAJw",
   "token_type":"bearer",
   "expires_in":"3600",
   "scope":"example"
}

エラーレスポンスについては code の場合と同様です。

Signing and Encryption

上記のレスポンスパラメータはJWS で署名をつける もしくは JWE にて暗号化されます。 Authorization Server は 自分自身とClient の metadata を用いてアルゴリズムを決定します。

次は Response Mode 毎の Authorization Response を見ていきましょう。

Response Mode "query.jwt"

response というクエリパラメータとしてJWTの値が指定され、HTTP リダイレクトにより Client に送られます。 フォーマットは application/x-www-form-urlencoded です。

HTTP/1.1 302 Found
Location: https://client.example.com/cb?
response=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLm
V4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsImV4cCI6MTMxMTI4MTk3MCwiY29kZSI6IlB5eU
ZhdXgybzdRMFlmWEJVMzJqaHcuNUZYU1FwdnI4YWt2OUNlUkRTZDBRQSIsInN0YXRlIjoiUzhOSjd1cW
s1Zlk0RWpOdlBfR19GdHlKdTZwVXN2SDlqc1luaTlkTUFKdyJ9.HkdJ_TYgwBBj10C-aWuNUiA062Amq
2b0_oyuc5P0aMTQphAqC2o9WbGSkpfuHVBowlb-zJ15tBvXDIABL_t83q6ajvjtq_pqsByiRK2dLVdUw
KhW3P_9wjvI0K20gdoTNbNlP9Z41mhart4BqraIoI8e-L_EfAHfhCG_DDDv7Yg

response_typetoken, id_token が含まれ、 JWE を"使わない"時、 URL からの漏洩を防ぐために query.jwt の値を使ってはいけません。

Response Mode "fragment.jwt"

フラグメント部に response というパラメータとしてJWTの値が指定され、HTTP リダイレクトにより Client に送られます。 フォーマットは application/x-www-form-urlencoded です。

HTTP/1.1 302 Found
Location: https://client.example.com/cb#
response=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLm
V4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsImV4cCI6MTMxMTI4MTk3MCwiYWNjZXNzX3Rva2
VuIjoiMllvdG5GWkZFanIxekNzaWNNV3BBQSIsInN0YXRlIjoiUzhOSjd1cWs1Zlk0RWpOdlBfR19GdH
lKdTZwVXN2SDlqc1luaTlkTUFKdyIsInRva2VuX3R5cGUiOiJiZWFyZXIiLCJleHBpcmVzX2luIjoiMz
YwMCIsInNjb3BlIjoiZXhhbXBsZSJ9.bgHLOu2dlDjtCnvTLK7hTN_JNwoZXEBnbXQx5vd9z17v1Hyzf
Mqz00Vi002T-SWf2JEs3IVSvAe1xWLIY0TeuaiegklJx_gvB59SQIhXX2ifzRmqPoDdmJGaWZ3tnRyFW
NnEogJDqGFCo2RHtk8fXkE5IEiBD0g-tN0GS_XnxlE

Response Mode "form_post.jwt"

response というHTML フォームの値としてJWTの値が指定され、User-Agent内で自動的に送信されることで HTTP POSTメソッドにより Client に送られます。 フォーマットは application/x-www-form-urlencoded です。

HTTP/1.1 200 OK
Content-Type: text/html;charset=UTF-8
Cache-Control: no-cache, no-store
Pragma: no-cache

<html>
 <head><title>Submit This Form</title></head>
 <body onload="javascript:document.forms[0].submit()">
  <form method="post" action="https://client.example.com/cb">
    <input type="hidden" name="response"
     value="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2
      FjY291bnRzLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsImV4cCI6MTM
      xMTI4MTk3MCwiYWNjZXNzX3Rva2VuIjoiMllvdG5GWkZFanIxekNzaWNNV3BBQSIs
      InN0YXRlIjoiUzhOSjd1cWs1Zlk0RWpOdlBfR19GdHlKdTZwVXN2SDlqc1luaTlkT
      UFKdyIsInRva2VuX3R5cGUiOiJiZWFyZXIiLCJleHBpcmVzX2luIjoiMzYwMCIsIn
      Njb3BlIjoiZXhhbXBsZSJ9.bgHLOu2dlDjtCnvTLK7hTN_JNwoZXEBnbXQx5vd9z1
      7v1HyzfMqz00Vi002T-SWf2JEs3IVSvAe1xWLIY0TeuaiegklJx_gvB59SQIhXX2i
      fzRmqPoDdmJGaWZ3tnRyFWNnEogJDqGFCo2RHtk8fXkE5IEiBD0g-tN0GS_XnxlE"/>
    </form>
   </body>
  </html>

Client には HTTP POSTなリクエストが送られます。

POST /cb HTTP/1.1
  Host: client.example.org
  Content-Type: application/x-www-form-urlencoded

  response=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2
      FjY291bnRzLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsImV4cCI6MTM
      xMTI4MTk3MCwiYWNjZXNzX3Rva2VuIjoiMllvdG5GWkZFanIxekNzaWNNV3BBQSIs
      InN0YXRlIjoiUzhOSjd1cWs1Zlk0RWpOdlBfR19GdHlKdTZwVXN2SDlqc1luaTlkT
      UFKdyIsInRva2VuX3R5cGUiOiJiZWFyZXIiLCJleHBpcmVzX2luIjoiMzYwMCIsIn
      Njb3BlIjoiZXhhbXBsZSJ9.bgHLOu2dlDjtCnvTLK7hTN_JNwoZXEBnbXQx5vd9z1
      7v1HyzfMqz00Vi002T-SWf2JEs3IVSvAe1xWLIY0TeuaiegklJx_gvB59SQIhXX2i
      fzRmqPoDdmJGaWZ3tnRyFWNnEogJDqGFCo2RHtk8fXkE5IEiBD0g-tN0GS_XnxlE

Processing rules

Authorization Response を受け取った Client 側の検証処理について説明します。

  1. (OPTIONAL) JWT ヘッダパラメータの kid の値を用いてJWTをdecryptする
  2. state パラメータを取得して User-Agent との紐付けを検証(CSRF対策) state パラメータはワンタイムなCSRF対策トークンとして利用され、検証完了後破棄されます。
  3. iss の値を検証 (期待したものと一致するかどうか)
  4. aud の値を検証 (client_id と一致するかどうか)
  5. exp の値を検証
  6. iss, kid の組み合わせから署名を検証. 鍵周りの詳細はこの仕様の範囲外。

いずれの検証も失敗したらすぐにエラーを返します。 これらの検証が終わるまでは、Authorization Response の中身である codeaccess_token などの値を利用してはいけません。

Metadata

Client Metadata

Client 側で期待するJWTの署名や暗号化のアルゴリズムなどを定義できます。詳細は仕様みてください。

  • authorization_signed_response_alg
  • authorization_encrypted_response_alg
  • authorization_encrypted_response_enc

Authorization Server Metadata

Authozation Server 側でサポートしている値を定義できます。詳細は仕様みてください。

  • authorization_signing_alg_values_supported
  • authorization_encryption_alg_values_supported
  • authorization_encryption_enc_values_supported
  • response_modes_supported : サポートする response_mode パラメータ

OAuth Server Metadata についてはこちらのRFCを参照してください。

RFC 8414 - OAuth 2.0 Authorization Server Metadata

Security considerations

  • DoS using specially crafted JWTs : でかい JWK Set を処理する感じになってネットワークやCPUを圧迫するとか、JWK Set URL が DDos のターゲットみたいになるとか。鍵周りはよく考えて設計する必要がありますね。
  • Code Replay : 同一 Client, 別のユーザー向けの Authorization Code を含む攻撃は、state パラメータの検証で対応可能です。
  • Mix-Up : あるClientと複数のAuthorization Serverがやり取りする場合のMix-Up 攻撃については 図解:OAuth 2.0に潜む「5つの脆弱性」と解決法 (4/4):デジタルID最新動向(2) - @IT 見れば良いと思います。iss, aud の検証をしっかりやる必要があります。
  • Code Leakage : 暗号化すれば code の値も外から見えなくなります。

まとめ

  • Authorization Response に JWT を使う方法が定義されている
  • Response Type, Response Mode による挙動とJWTの中身の違いに注目せよ
  • JWT生成のところで鍵周りとかは仕様範囲外なので、よく考えて設計する必要がある

というところでしょうか。

もっと FAPI について知りたい?

ひたすら仕様を読む会ってのがあるみたいです。

peatix.com

今回紹介した JARM も入ってますが、既にあなたは「JARM完全に理解した」状態なので余裕ですね。 お時間とか色々と余裕がある方は是非参加してみてください。

ところで、"JARM" ってどう読むの??? ではまた。