← 記事一覧

RFC 9126 PAR で認可リクエストを保護する(Keycloak で試す)

Pushed Authorization Requests(PAR / RFC 9126)を Keycloak で試します。認可リクエストをバックチャネルで事前登録し、ブラウザや中継機器への露出を減らしながら、クライアント認証と早期検証を行う仕組みを確認します。

RFC 9126 PAR で認可リクエストを保護する(Keycloak で試す)Pushed Authorization Requests(PAR / RFC 9126)を Keycloak で試します。認可リクエストをバックチャネルで事前登録し、ブラウザや中継機器への露出を減らしながら、クライアント認証と早期検証を行う仕組みを確認します。

はじめに

OAuth 2.0 の認可コードフローでは、クライアントがブラウザを介して認可サーバーへ認可リクエストを送ります。
通常の認可リクエストでは、client_idscoperedirect_uristatecode_challenge などが URL のクエリに含まれます。

クライアントと認可サーバーが直接通信する経路をバックチャネル、ブラウザを介して通信する経路をフロントチャネルと呼びます。

この方式では、認可リクエストの内容がブラウザや中継機器を通過します。
また、認可サーバーがクライアントを認証し、redirect_uriscope などを検証できるのは、ブラウザから認可リクエストを受け取った後です。

Pushed Authorization Requests(PAR) は、認可リクエストをバックチャネルで認可サーバーへ事前登録し、後続の認可リクエストで使用する request_uri を受け取る仕様です。
認可サーバーはユーザー操作を始める前に、認証情報を持つクライアントを認証し、認可リクエストを検証できます。

Keycloak を認可サーバーに設定し、通常の認可リクエストと PAR の通信フローを比較します。
RFC 9126 の要件を確認したうえで、成功例と失敗例を curl と mise で検証します。
最後に、PAR が保護する範囲と JAR(JWT-Secured Authorization Request) との責務の違いを整理します。

サンプルの Go Web アプリは、Terraform で作成する機密クライアント myapp(Client A)を使います。
/loginstatenonce、PKCE の code_verifier を生成して PAR を実行し、成功したときだけブラウザを認可エンドポイントへリダイレクトします。
Client B(myapp-binding)は、発行済み request_uri のクライアント束縛を確認する curl 検証専用です。

Financial-grade API(FAPI)のプロファイル全体、OAuth 2.0 のプライバシー問題全般、JAR における署名と暗号化の詳細は扱いません。
JAR の仕組みは、RFC 9101 の JAR を Keycloak で試した記事で扱っています。

成果物

https://github.com/kntks/blog-code/tree/main/2026/09/oauth-par

通常の認可リクエストと課題

認可コードフロー

RFC 6749 の Authorization Code Grant に OpenID ConnectPKCE(RFC 7636) を組み合わせた認可コードフローは、次のようになります。

この例では、クライアントが Go アプリケーション、認可サーバーが Keycloak、ユーザーエージェントがブラウザです。

通常の認可リクエストでは、パラメーターを URL のクエリに設定します。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
response_type=code&
client_id=myapp&
redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fcallback&
scope=openid&
state=...&
nonce=...&
code_challenge=...&
code_challenge_method=S256

ブラウザを経由することで生じる課題

通常の認可リクエストでは、URL に含まれるパラメーターがブラウザの履歴やリバースプロキシのアクセスログに残ります。
TLS は通信経路上の盗聴を防ぎますが、URL を記録または転送するコンポーネントからパラメーターが見える可能性は残ります。

もう一つの課題は、認可サーバーがブラウザからリクエストを受け取るまで、認可リクエストを検証できないことです。
また、通常の認可エンドポイントではクライアント認証を行わないため、機密クライアントの認証は後続のトークンリクエストで行われます。
不正な redirect_uri や許可されていない scope を含むリクエストでも、最初にフロントチャネルへ送信されます。

課題通常の認可リクエストPAR を使う場合
URL への露出認可パラメーターをクエリに含めるブラウザに渡す値を client_idrequest_uri に短縮できる
クライアント認証認可エンドポイントでは通常行わない。機密クライアントは後続のトークンリクエストで認証するPAR エンドポイントでトークンエンドポイントと同じ規則により認証する
リクエスト検証ブラウザから認可エンドポイントへ到達した後に行うユーザー操作を始める前に検証できる
URL の長さパラメーターが増えるほど長くなる認可エンドポイントへ送る URL を短くできる

URL を短くできることは PAR の利点の一つです。
PAR は認可リクエストのペイロードをバックチャネルで送信するため、クライアント認証と早期検証が可能になります。

PAR の仕組み

PAR エンドポイントの役割

RFC 9126 は、認可リクエストのペイロードをクライアントから認可サーバーへ直接送信し、後続の認可リクエストで使う request_uri と交換する方法を定義しています。

クライアントが認可パラメーターを送る先は、認可エンドポイントではなく PAR エンドポイントです。
認可サーバーは認可リクエストを保持し、そのリクエストを参照するための有効期間が短い request_uri を返します。

PAR を使った認可コードフロー

この例でブラウザを経由する認可リクエストは、次の形になります。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
client_id=myapp&
request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A...

個々の認可パラメーターは URL のクエリに現れなくなります。
一方で、request_uri 自体はブラウザを経由するため、PAR を使っても認可フローに関するすべての情報がログに残らなくなるわけではありません。

Go アプリの /login は、PAR エンドポイントへ application/x-www-form-urlencoded 形式で POST し、Client A の client_idclient_secretclient_secret_basic(HTTP Basic 認証)で送ります。
レスポンスが 201 Created で、空でない request_uri と正の expires_in を含む場合だけ、認可エンドポイントへ client_idrequest_uri だけを付けて 302 Found を返します。
PAR が失敗した場合、通常の認可リクエストへフォールバックせず、詳細をサーバーログに記録して、ブラウザには 502 Bad Gateway を返します。

コールバックでは statenonce、PKCE の code_verifier を Cookie から取り出し、認可コードをトークンエンドポイントで交換します。
取得した ID Token は、JWKS で署名を検証し、issuer、Client A の audience、nonce、有効期限を確認してからセッション Cookie を設定します。

RFC 9126 の要件

PAR エンドポイントへのリクエスト

PAR エンドポイントは、UTF-8 の application/x-www-form-urlencoded 形式を使う HTTP POST を受け付けます。
本番環境のエンドポイントは https スキームでなければなりません。RFC 9126 Section 2

PAR エンドポイントには、通常の認可エンドポイントで使えるパラメーターと、PKCE や OpenID Connect などの拡張パラメーターを送信できます。
ただし、認可サーバーが返す request_uri を PAR リクエストへ含めてはなりません。

PAR エンドポイントのクライアント認証には、トークンエンドポイントと同じ規則が適用されます。
クライアントは、登録された認証方式を使って認証します。
client_id は PAR リクエストにも必須です。RFC 9126 Section 2.1

認可サーバーによる検証

認可サーバーは、PAR リクエストに対して次の確認を行います。

  • トークンエンドポイントと同じ方法でクライアントを認証する
  • リクエストに request_uri が含まれていた場合は拒否する
  • 通常の認可リクエストと同様に、redirect_uriscope などを検証する

この処理により、不正なリクエストをユーザー操作の前に拒否できます。
認可サーバーが PAR の時点で実施した検証を認可エンドポイントで省略するには、リクエストとポリシーに変更がなく、検証結果が変わらないことを確認できなければなりません。

成功レスポンスと request_uri

検証に成功すると、認可サーバーは HTTP 201 Created と JSON レスポンスを返します。RFC 9126 Section 2.2

項目RFC 9126 の要件
HTTP メソッドPOST
Content-TypeUTF-8 の application/x-www-form-urlencoded
エンドポイント本番環境では https が必須
client_idPAR リクエストでも必須
クライアント認証トークンエンドポイントと同じ規則を適用
事前検証通常の認可リクエストと同様に検証
成功ステータス201 Created
成功レスポンスrequest_uriexpires_in
推測耐性request_uri に暗号学的に推測困難な部分を含める
クライアント束縛発行した request_uri を投稿元クライアントへ束縛する
有効期限期限切れの request_uri を拒否する
利用回数クライアントは一度だけ使用する。認可サーバーも原則として一回利用として扱う

request_uri の形式と有効期間は認可サーバーが決めます。
RFC 9126 は一般的な有効期間の例として 5 秒から 600 秒を挙げていますが、特定の値を要求していません。

クライアントは request_uri を一度だけ使用しなければなりません。
認可サーバーは request_uri を一回利用として扱うことが推奨されていますが、ブラウザの再読み込みによる重複リクエストを許容できます。RFC 9126 Section 4

PAR の必須化

認可サーバーは、サーバー全体またはクライアント単位で PAR を必須にできます。
PAR が必須の場合、PAR エンドポイントから発行された request_uri を持たない認可リクエストは invalid_request で拒否されます。

メタデータ意味
pushed_authorization_request_endpointPAR リクエストを送信するエンドポイント
require_pushed_authorization_requestsPAR を唯一の認可リクエスト開始方法とするかどうか。省略時は false

これらのメタデータは RFC 9126 Sections 5–6 で定義されています。

Keycloak で PAR を試す

検証環境

検証環境は、ブラウザ、Go アプリケーション、Keycloak で構成します。
基本フローを試すクライアントに加え、request_uri のクライアント束縛を確認するための別クライアントを使用します。

前提条件

  • Docker と Docker Compose が使用できる
  • mise が使用できる
  • mise install で、このリポジトリの mise.toml に定義された terraformgogoplsairjq を導入する
  • http://localhost:8080http://localhost:8081 にブラウザでアクセスできる

バージョン

項目バージョン
macOS26.6.2
Keycloak26.7.3
Go1.27.1
gopls0.23.0
air1.67.4
Terraform1.16.1
jq1.8.2
Terraform Provider(Keycloak)>= 5.6.0

構築手順

Keycloak を起動します。

Terminal window
cd blog-code/oauth-par
docker compose up -d

ツールは OS へ個別にインストールせず、mise.toml の定義から導入します。

Terminal window
mise install

Terraform で Realm、User、Client を作成します。

Terminal window
terraform -chdir=terraform init
terraform -chdir=terraform plan
terraform -chdir=terraform apply

サンプルアプリを起動します。

Terminal window
air

検証環境はローカルのため http を使用しますが、RFC 9126 が定める PAR エンドポイントは https が必須です。

Keycloak の起動と Terraform の apply が完了した後、検証タスクを個別に実行します。

PAR エンドポイントを確認する

Keycloak の OpenID Provider Configuration から、pushed_authorization_request_endpointrequire_pushed_authorization_requests を取得します。

mise の検証タスクを使うと、レスポンス本文と自動判定を同時に確認できます。

Terminal window
mise run kc:par:discover
Terminal window
$ curl -s http://localhost:8080/realms/myrealm/.well-known/openid-configuration | jq '{ "pushed_authorization_request_endpoint": .pushed_authorization_request_endpoint, "require_pushed_authorization_requests": .require_pushed_authorization_requests}'
{
"pushed_authorization_request_endpoint": "http://localhost:8080/realms/myrealm/protocol/openid-connect/ext/par/request",
"require_pushed_authorization_requests": false
}

pushed_authorization_request_endpoint が存在すれば、クライアントは認可サーバーが PAR を利用できると判断できます。
Keycloak 26.7.3 では、この構成の Discovery に含まれる require_pushed_authorization_requestsfalse です。
この値は Client A の PAR 必須設定とは別であり、Client A を個別に必須にしても変わりません。
したがって、Discovery の値から Client A の設定を判断することはできません。
Client A の PAR 必須化は、extra_config で設定します。

認可リクエストを事前登録する

認可パラメーターを PAR エンドポイントへ送信します。
この例では HTTP Basic 認証によってクライアントを認証します。

Terminal window
CLIENT_ID=myapp
CLIENT_SECRET=your-client-secret
REDIRECT_URI=http://localhost:8081/callback
ISSUER_URL=http://localhost:8080/realms/myrealm
PAR_ENDPOINT=$(curl -sS "$ISSUER_URL/.well-known/openid-configuration" \
| jq -er .pushed_authorization_request_endpoint)
BASIC_CLIENT_ID=$(jq -nr --arg value "$CLIENT_ID" '$value | @uri')
BASIC_CLIENT_SECRET=$(jq -nr --arg value "$CLIENT_SECRET" '$value | @uri')
STATE=$(openssl rand -hex 32)
NONCE=$(openssl rand -hex 32)
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | head -c 64)
CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 -A \
| tr '+/' '-_' \
| tr -d '=')
PAR_RESPONSE=$(curl -s -u "$BASIC_CLIENT_ID:$BASIC_CLIENT_SECRET" \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "response_type=code" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "scope=openid" \
--data-urlencode "state=$STATE" \
--data-urlencode "nonce=$NONCE" \
--data-urlencode "code_challenge=$CODE_CHALLENGE" \
--data-urlencode "code_challenge_method=S256" \
"$PAR_ENDPOINT")
echo "$PAR_RESPONSE" | jq

client_secret_basic では、RFC 6749 に従い Client ID と Client Secret をそれぞれ application/x-www-form-urlencoded 形式へ変換してから HTTP Basic 認証へ渡します。
Base64 で生成した値には +/ が現れることがあり、このサンプルの固定 Client Secret にも + が含まれます。
未変換のまま curl -u "$CLIENT_ID:$CLIENT_SECRET" とすると Keycloak のクライアント認証に失敗するため、Go アプリと mise タスクも同じ変換を行います。

成功すると、Keycloak は request_uriexpires_in を返します。
RFC 9126 上の成功ステータスは 201 Created です。

{
"request_uri": "urn:ietf:params:oauth:request_uri:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"expires_in": 300
}

この環境では、Terraform の Realm 属性 parRequestUriLifespan = "300" により expires_in300 になります。
RFC 9126 自体はこの値を固定していません。

request_uri で認可を開始する

レスポンスから request_uri を取り出します。

Terminal window
REQUEST_URI=$(echo "$PAR_RESPONSE" | jq -r .request_uri)

認可エンドポイントへ送るのは client_idrequest_uri です。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
client_id=myapp&
request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

statenoncecode_challenge などは、PAR エンドポイントへ事前に送信したリクエストから復元されます。

これを mise のタスクとして実行すると以下のような結果になります。

Terminal window
$ mise run kc:par:request
説明:Client A Basic 認証付き PAR を実行し、生レスポンスを表示する。HTTP 201、request_uri、expires_in=300 を確認し、認可 URL(client_id + request_uri)を表示する
PAR HTTP status: 201
Raw PAR response:
{
"request_uri": "urn:ietf:params:oauth:request_uri:77b43ada-50ab-bab3-8ed1-fa62a9e78980",
"expires_in": 300
}
Authorization URL: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A77b43ada-50ab-bab3-8ed1-fa62a9e78980

不正な redirect_uri を事前に拒否する

PAR エンドポイントへ、クライアントに登録されていない redirect_uri を送ります。

Terminal window
curl -i -u "$BASIC_CLIENT_ID:$BASIC_CLIENT_SECRET" \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "response_type=code" \
-d "client_id=$CLIENT_ID" \
-d "redirect_uri=https://invalid.example/callback" \
-d "scope=openid" \
"$PAR_ENDPOINT"

ここでは、ログインと同意の画面へ進む前にリクエストが拒否されることを確認します。
RFC 9126 では、不正または不一致の redirect_uri に対して invalid_request を既定のエラーコードとして使用できます。RFC 9126 Section 2.3

これを mise のタスクとして実行すると以下のような結果になります。

Terminal window
$ mise run kc:par:reject-redirect
説明:未登録の redirect_uri を含む Pushed Authorization Request PAR エンドポイントへ送る。生レスポンスを表示し、HTTP 400 OAuth エラーの手掛かりを確認する
Unregistered redirect PAR HTTP status: 400
Raw response:
{"error":"invalid_request","error_description":"Invalid parameter: redirect_uri"}

PAR を必須にする

Terraform の keycloak_openid_client.main.extra_config["require.pushed.authorization.requests"] = "true" で、Client A(myapp)だけに Pushed authorization request required を設定します。
Client B(myapp-binding)の extra_config は空で、PAR 必須ではありません。
Realm レベルで設定しているのは parRequestUriLifespan = "300" だけです。

設定後、PAR を使わずに認可エンドポイントへ通常の認可パラメーターを送ります。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
response_type=code&
client_id=myapp&
redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fcallback&
scope=openid

Keycloak 26.7.3 はこのリクエストを、error=invalid_requesterror_description=Pushed+Authorization+Request+is+only+allowed. を付けた登録済みコールバックへの HTTP 302 リダイレクトとして拒否します。
PAR エンドポイントで発行された request_uri を使うリクエストだけが受け付けられることを確認します。
この挙動は Client A 単位の設定によるものです。
Discovery に表示される require_pushed_authorization_requests: false は変わらず、Client A が個別に PAR 必須であることと矛盾しません。

これを mise のタスクとして実行すると以下のような結果になります。

Terminal window
$ mise run kc:par:reject-nonpar
説明:Client A でrequest_uri なしの通常認可リクエストを送る。生レスポンスヘッダーを表示し、HTTP 302 Locationの error=invalid_request、PAR 必須を示す説明を確認する
Non-PAR authorization HTTP status: 302
Raw response headers:
HTTP/1.1 302 Found
Cache-Control: no-store, must-revalidate, max-age=0
Location: http://localhost:8081/callback?error=invalid_request&error_description=Pushed+Authorization+Request+is+only+allowed.&state=7f1195ff935139f4e8ef26f3aefde893&iss=http%3A%2F%2Flocalhost%3A8080%2Frealms%2Fmyrealm
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Robots-Tag: none
content-length: 0
Raw response body:

request_uri のクライアント束縛を確認する

RFC 9126 は、request_uri を PAR リクエストの投稿元クライアントへ束縛することを認可サーバーへ要求しています。

Client A が取得した request_uri と、Client B の client_id を組み合わせて認可エンドポイントへ送ります。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
client_id=myapp-binding&
request_uri=<Client A が取得した request_uri>

この組み合わせが拒否されれば、別のクライアントが取得済みの request_uri を流用できないことを確認できます。

期限切れと再利用への対応では、Keycloak の実装固有の挙動と RFC 9126 の要件を区別します。
RFC 9126 は、期限切れの値を拒否すること、クライアントが一度だけ使用すること、認可サーバーが原則として一回利用として扱うことを定めています。

これを mise のタスクとして実行すると以下のような結果になります。

Terminal window
$ mise run kc:par:reject-binding
説明:Client A が取得した request_uri Client B で使う。両方の生レスポンスを表示し、HTTP 400 とクライアント束縛エラーを確認する
Client A PAR HTTP status: 201
Raw client A PAR response:
{"request_uri":"urn:ietf:params:oauth:request_uri:601aad6f-67af-7272-63b7-91a0c9e9c641","expires_in":300}
Client B using client A request_uri HTTP status: 400
Raw binding response:
<!DOCTYPE html>
<html class="login-pf" lang="en">
<head>
# 省略
</head>
<body class="" data-page-id="login-error">
# 省略
</body>
</html>
Checked client B=myapp-binding against client A request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A601aad6f-67af-7272-63b7-91a0c9e9c641

mise による検証タスク

検証タスクは、環境準備や Terraform の apply とは分離しています。
Keycloak の起動と terraform apply が完了した後、各タスクを個別に実行できます。
各タスクは生レスポンスを表示し、終了ステータスと内容も自動判定します。

タスク確認内容と自動判定
mise run kc:par:discoverDiscovery の生 JSON を表示し、HTTP 200、PAR エンドポイント、認可エンドポイントの存在と require_pushed_authorization_requests=false を確認する
mise run kc:par:requestClient A で Basic 認証付き PAR を実行し、生レスポンスを表示する。HTTP 201、request_uriexpires_in=300 を確認し、認可 URL(client_id + request_uri)を表示する
mise run kc:par:reject-redirect未登録の redirect_uri を含む Pushed Authorization Request を PAR エンドポイントへ送る。生レスポンスを表示し、HTTP 400 と OAuth エラーの手掛かりを確認する
mise run kc:par:reject-nonparClient A で request_uri なしの通常認可リクエストを送る。生レスポンスヘッダーを表示し、HTTP 302 と Locationerror=invalid_request、PAR 必須を示す説明を確認する
mise run kc:par:reject-bindingClient A が取得した request_uri を Client B で使う。両方の生レスポンスを表示し、HTTP 400 とクライアント束縛エラーを確認する
mise run kc:par:verify上記 5 タスクを discoverrequestreject-redirectreject-nonparreject-binding の順に実行する

正常な PAR の検証では、Client A(myapp)と Go アプリが共有する Client Secret を使います。
Client B(myapp-binding)は request_uri の束縛検証にだけ使い、PAR を必須にしていません。

PAR が保護するものと限界

PAR が変えるのは、認可リクエストの送信経路と処理のタイミングです。
PAR によって得られる保護と、PAR だけでは解決しない問題を整理します。

観点PAR による効果限界と追加対策
認可パラメーターの露出個々のパラメーターをバックチャネルで送るrequest_uri 自体はブラウザを経由する
クライアント認証認証可能なクライアントをユーザー操作前に認証できるクライアント種別と登録済み認証方式に依存する
早期検証redirect_uriscope などを事前検証できるPAR 時点で実行できない検証は認可時に必要
推測とリプレイ推測困難で短命な参照を使い、クライアントに束縛する認可サーバーによる一回利用の扱いが重要
プライバシーURL クエリによる意図しない情報開示を減らせるOAuth 2.0 全体のプライバシー問題を解決するものではない

RFC 9126 は、request_uri の差し替えに備えて、PKCE、一意な state、OpenID Connect の nonce のいずれかを使用することを推奨しています。RFC 9126 Section 7.5
今回の Go アプリは、これら三つを使用します。

認可リクエストを PAR エンドポイントへ登録してから、ブラウザが認可エンドポイントへ到達するまでにクライアントポリシーが変わる可能性もあります。
認可サーバーは、認可エンドポイントでも現在のポリシーに基づいてリクエストを評価します。

PAR と JAR の責務分担

今回のリポジトリには、JAR の Request Object を生成、署名、検証する実装はありません。
mise の検証タスクも PAR 単体だけを対象にします。

PAR と JAR は、認可リクエストの異なる側面を扱います。

仕様主な役割ブラウザへ渡すもの
PAR認可リクエストをバックチャネルで事前登録し、認証と早期検証を行うclient_id と認可サーバーが発行した request_uri
JAR認可リクエストを Request Object にまとめ、JWS で完全性と送信元認証を保護するrequest、または Request Object を参照する request_uri

JAR の JWE を使う場合は Request Object の機密性も保護できます。
PAR は署名や暗号化そのものを提供する仕様ではないため、JAR の代替ではありません。

両者は組み合わせて利用できます。
PAR エンドポイントへ通常の認可パラメーターの代わりに JAR の Request Object を送信し、認可サーバーが発行した request_uri を後続の認可リクエストで使います。

Terminal window
# 概念例: REQUEST_OBJECT の生成、署名、検証は今回の実装に含まれません。
CLIENT_ID=myapp
CLIENT_SECRET=your-client-secret
REQUEST_OBJECT=eyJhbGci...
ISSUER_URL=http://localhost:8080/realms/myrealm
PAR_ENDPOINT=$(curl -sS "$ISSUER_URL/.well-known/openid-configuration" \
| jq -er .pushed_authorization_request_endpoint)
BASIC_CLIENT_ID=$(jq -nr --arg value "$CLIENT_ID" '$value | @uri')
BASIC_CLIENT_SECRET=$(jq -nr --arg value "$CLIENT_SECRET" '$value | @uri')
PAR_RESPONSE=$(curl -s -u "$BASIC_CLIENT_ID:$BASIC_CLIENT_SECRET" \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "request=$REQUEST_OBJECT" \
"$PAR_ENDPOINT")
REQUEST_URI=$(echo "$PAR_RESPONSE" | jq -r .request_uri)

この構成では、PAR によるバックチャネル送信、クライアント認証、早期検証と、JAR による Request Object の完全性、送信元認証を組み合わせます。

JAR の by-reference 方式と PAR は、同じ request_uri という名前を使いますが、参照の作り方が異なります。

方式request_uri の作り方と取得経路
JAR by-referenceクライアントが Request Object を参照する URI を用意し、認可サーバーが取得する
PARクライアントが認可リクエストを POST し、認可サーバーが管理する request_uri を発行する

まとめ

PAR は、認可リクエストを認可サーバーへ事前登録し、後続のフロントチャネルでは request_uri で参照する仕組みです。

  • 個々の認可パラメーターをバックチャネルで送り、ブラウザ上の露出を減らす
  • 認証可能なクライアントを認証し、redirect_uriscope などをユーザー操作前に検証できる
  • 認可サーバーは request_uri を短命で推測困難な値として発行し、投稿元クライアントに束縛する
  • Keycloak では PAR を必須化し、通常の認可リクエストを拒否できる
  • PAR は送信経路と事前処理を扱い、JAR は Request Object の完全性と送信元認証を扱う

PAR は、認可リクエストに含まれる情報を一切見えなくする仕組みではありません。
request_uri の有効期限、一回利用、クライアント束縛、PKCE、statenonce を組み合わせて運用します。

参考

Pagefind UI mount↑↓ select · enter open