OAuth 2.0 PAR(Pushed Authorization Requests)を試してみた

# はじめに
OpenID Connect関連の仕様で、新しいユースケースや要望に対応するため、新しい仕様群の開発がされている。文章としては読んではいるが、理解が追いついていない感があるため、実際に試して、気になった点を備忘録として記載した内容です。
今回は、OAuth 2.0 PARについてcurlを利用して試した結果を記載します。

# PAR(Pushed Authorization Requests)とは
ブラウザやWebサーバーには、扱うURLの長さの制限があり、scope,redirect_uri,claims等のRequestパラメータ値が長すぎると、処理が中断されることがあります。これは、[IDA(OpenID Connect for Identity Assurance 1.0)](https://openid.net/specs/openid-connect-4-identity-assurance-1_0.html)や[RAR(OAuth 2.0 Rich Authorization Requests)](https://www.rfc-editor.org/rfc/rfc9396)や[JAR(JWT-Secured Authorization Request)](https://www.rfc-editor.org/rfc/rfc9101.html)でrequest_uriを使用せずにRequestオブジェクトとしてAuthorizationサーバーにパラメータを直接送信するケースが考えられます。
PARは、このような大きなAuthorization Requestのパラメータをブラウザ経由で送信する必要が無いように開発された仕様です。クライアントは最初にこれらのパラメータをAuthorizationサーバーにプッシュし、その後のAuthorization Requestでパラメータへの参照として使用されるRequest URIを提供します。

PARは、セキュリティ面でも従来のOAuth 2.0よりも良くなります。
従来のAuthorization Codeフローでは、Authorization Request時にはclient_idだけが含まれ、Token Request時にクライアント認証が行われます。PARは、最初にプッシュされるPAR Request時にクライアント認証が行われ、Authorization Requestを行う前に、Authorizationサーバーは、未承認または不正な形式のリクエストを早期に拒否する事が可能となります。
また、完全性や機密性の観点では、Authorization Requestは通常、ブラウザを介して平文で送信されるため、ユーザーや攻撃者(リソースオーナー)はリクエストを検査したり変更する事が出来ます。クライアントが処理結果(scope等)を適切にチェックすれば改ざんを後で検知できる可能性はありますが、Authorization Requestのデータを暗号化して保護する必要があります。PARは、このアプリケーションレベルでの暗号化を必要とせずに、クライアントとAuthorizationサーバー間でのHTTPSによる通信暗号化で保護する事が出来ます。

PARに関する詳細は、[RFC9126](https://datatracker.ietf.org/doc/html/rfc9126)や[この記事](https://qiita.com/TakahikoKawasaki/items/97563be46b62a0e02fe6)を参照。
その他、仕様書の著書であるTorsten Lodderstedtさんの[こちらの記事](https://medium.com/oauth-2/pushed-authorization-requests-draft-adopted-by-oauth-working-group-a1060007150f)も参考しました。

##### PARの処理フロー
![par-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247555/8c7cc5c0-57ca-2da0-ee3f-110ad82ec732.png)


# 検証
### PAR Request
curlでの実行確認のため、まずはStep2となるPAR EndpointにPAR Requestを送信する。
rfc9126に"application/x-www-form-urlencoded"フォーマットを使用すると記載されているので"Content-Type"を指定。送信パラメータとしては、「client_id」、「response_type」、「redirect_type」、「scope」、「state」、「code_challenge」、および「code_challenge_method」が含まれている場合があると記載されているが、今回は、下記のようなRequestを送信した。

~~~
curl -Ss -k \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "scope=openid profile" \
-d "response_type=code" \
-d "response_mode=query" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "redirect_uri=${REDIR_URI}" \
-d "code_challenge=${CC}" \
-d "code_challenge_method=${CCM}" \
-d "state=${STATE}" \
-d "nonce=${NONCE}" \
"https://${AUTHSRV_URL}/${PAR_EP}"
~~~

上記では、PAR Endpointのクライアント認証には、client_secret_postを使用しているため、client_secretも送信パラメータに含めている。クライアント認証については、[OpenID Connect Core(9. Client Authentication)](http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html#ClientAuthentication)や[OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://www.rfc-editor.org/rfc/rfc8705.html)や[この記事](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)を参照。また対応しているクライアント認証方式については、OpenID Provider Metadata(/.well-known/openid-configurationにアクセスして得られる内容)のtoken_endpoint_auth_methods_supported値を確認すると良い。

### PAR Response
上記Requestを送信すると、下記のようなResponseが得られる。
~~~
HTTP/2 201
content-type: application/json;charset=UTF-8
cache-control: no-store
pragma: no-cache

{
  "expires_in": 600,
  "request_uri": "urn:ietf:params:oauth:request_uri:MHh49pNGwlI2ZCTskERl1Lfz4ODyRLR5uDx3uZfots0"
}
~~~

参考までにエラーケースのメッセージ例も添付しておきます。
~~~
HTTP/2 400
content-type: application/json;charset=UTF-8
cache-control: no-store
pragma: no-cache

{
  "error": "unsupported_response_type",
  "error_description": "The client is not allowed to request response_type 'id_token'."
}
~~~

### Authorization Request
PARとしては、上記で終わりなのだが、Authorization EndpointへのアクセスやToken Endpintまでアクセスし、その結果を確認していきます。
Authorization Endpointへのアクセスは、PAR Responseで得られたrequest_uriとclient_idをRequestに含めれば良い。
また、このrequest_uriによるAuthorization Endpointへのアクセス方法は、JAR(JWT-Secured Authorization Request)として定義されている。JARについては、[RFC9101](https://www.rfc-editor.org/rfc/rfc9101.html)や[こちらの記事](https://qiita.com/TakahikoKawasaki/items/d52ce91866ae0dcc2ff7)を参照。

今回は、下記をAuthorization EndpointにRequestした。
~~~
curl -v -b cookieJar.txt -c cookieJar.txt -Ss -k \
-H "Content-Type: application/x-www-form-urlencoded" \
"https://${AUTHSRV_URL}/${AUTH_EP}?client_id=${CLIENT_ID}&request_uri=${REQUEST_URI}"
~~~

ここでcookieを指定しているのは、Authorization Endpointアクセス時には通常認証が求められるため、あらかじめAuthorizationサーバーでの認証を済ませた認証済みcookieを渡した。また、JARでclient_idは必須として定義されているが、残した理由については、参考記事を読んでなるほどと思った。

> 1. リクエストオブジェクトが対称鍵系アルゴリズムで暗号化されている場合、復号化のために共有秘密鍵が必要です。対称鍵系アルゴリズムでは、クライアントシークレットが鍵として使われるので (OIDC Core Section 10.1)、リクエストオブジェクトを復号化する前に、認可サーバーはクライアントアプリケーションを特定してデータベースからクライアントシークレットを取り出せなければなりません。暗号化されたリクエストオブジェクト内に client_id が含まれているとしても、認可サーバーは復号化の前にその値を知ることはできません。
> 2. リクエストオブジェクトが request_uri で指定されている場合、認可サーバーはその場所にリクエストオブジェクトを取りにいかねばなりません。しかし、セキュリティー上の理由から、認可サーバーは事前登録されていない場所にリクエストオブジェクトを取りにいくことを拒否してもよい (個人的には「すべき」と思っています) とされています (参考：OpenID Connect Discovery 1.0, Section 3, require_request_uri_registration)。提示されたリクエスト URI と当該クライアント用に事前登録されているリクエスト URI 群との照合をおこなうため、リクエストオブジェクトを取りにいく前に、認可サーバーはクライアントアプリケーションを特定できなければなりません。離れた場所にあるリクエストオブジェクト内に client_id が含まれているとしても、認可サーバーはリクエストオブジェクトを取りにいく前にその値を知ることはできません。

### Authorization Response
上記のResponseとして、Authorization Codeがresponse_modeに指定した形式で得られる。
今回は、response_mode=queryとしたため、locationヘッダーでAuthorization Codeが送られてきます。response_modeには、様々なタイプ(query,fragment,form_post,jwt,query.jwt,fragment.jwt,form_post.jwt)があり、対応しているタイプは、OpenID Provider Metadataのresponse_modes_supportedを確認すると良い。

response_mode関連仕様：
query,fragment: [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes)
form_post: [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode)
jwt,query.jwt,fragment.jwt,form_post.jwt: [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)](https://openid.net/specs/openid-financial-api-jarm.html)
※JARMについては、[こちらも参照](https://qiita.com/TakahikoKawasaki/items/83c47c9830097dba2744#jarm)

下記のようなlocationヘッダーでResponseが得られます。
~~~
HTTP/2 303

location: https://<redirect url>?code=1qvfS6OVWTx49popqBHTXHzQoyn0ZAuzkZuDeoMxMOw.NFNnfhGgCiwvncd-00Pc90b_6doxr1BoM3f7vhzl_EL8YrdZek_b34kv1elovDEONUXQuikb_FlK_Ss0gXuhrw&iss=<iss value>&state=<state value>
~~~

### Token Request
Authorization Codeが得られたので、Token Endpointにアクセスし、Access TokenやID Tokenを取得します。
このRequestは、OAuthやOpenID Connectで良くあるRequestのため特筆するところはありません。

今回は、下記をToken EndpointにRequestした。
~~~
curl -Ss -k \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=${CODE}" \
-d "grant_type=authorization_code" \
-d "state=${STATE}" \
-d "redirect_uri=${REDIR_URI}" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "code_verifier=${CV}" \
"https://${AUTHSRV_URL}/${TOKEN_EP}"
~~~


### Token Response
scopeにopenidを指定しているので、id_tokenも含めて、無事下記のようなResponseが得られた。
~~~
{
  "access_token": "7f2srPhZ-AqF1J~~~~~~~~~~",
  "expires_in": 3599,
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNlcnZlciJ9.eyJhY~~~~~~~~.wqTCc-uy~~~~~~~~~~",
  "scope": "openid profile",
  "token_type": "bearer"
}
~~~


# 補足
[PKCE(Proof Key for Code Exchange by OAuth Public Clients)](https://datatracker.ietf.org/doc/html/rfc7636)対応用にcode_challengeやcode_verifierの生成方法も記載しておきます。

code_verifierは下記のようにランダム値を生成しました。
※検証製品が43桁以上を要求していたため、43桁にしています。
~~~
CV=$(head /dev/urandom | tr -dc a-z0-9 | head -c 43)
~~~
code_challengeは、code_challenge_methodをS256とし、下記のように生成しました。
~~~
CC=$(echo -n $CV | sha256sum | cut -f 1 -d ' ' | xxd -r -p | base64 | sed 's/=//g' | sed 's/\//_/g' | sed 's/+/-/g');
~~~