← 記事一覧

RFC 9101のJARをKeycloakで試す、requestとrequest_uriによる認可リクエスト保護

JWT-Secured Authorization Request(JAR / RFC 9101)をKeycloakで検証します。通常の認可リクエストが抱える課題を確認し、署名付きRequest Objectをrequestまたはrequest_uriで渡す方法を再現します。

RFC 9101のJARをKeycloakで試す、requestとrequest_uriによる認可リクエスト保護JWT-Secured Authorization Request(JAR / RFC 9101)をKeycloakで検証します。通常の認可リクエストが抱える課題を確認し、署名付きRequest Objectをrequestまたはrequest_uriで渡す方法を再現します。

はじめに

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

この方式は実装しやすい一方、ブラウザや中継機器を通過する認可リクエストに対して、アプリケーション層の完全性、送信元認証、機密性を保証する仕組みを持ちません。
URLに含まれるパラメーターが増えると、URL長の制約やログへの残留も問題になります。

ここで扱う JWT-Secured Authorization Request(JAR) は、認可リクエストを署名付きのJWT(Request Object)として扱う仕様です。
Request Objectをrequestで直接渡す方法と、request_uriで参照渡しする方法があります。

この記事では、Keycloakを認可サーバー、GoアプリケーションをOAuthクライアントとして、次の内容を確認します。

  • RS256で署名したRequest Objectをrequestで渡す
  • Request Objectを一時公開し、request_uriで渡す
  • KeycloakでRequest Objectを必須化する
  • JWSJWErequestrequest_uriの効果を分けて整理する

PAR(Pushed Authorization Requests)はこの記事では扱わず、別の記事で扱います。

成果物

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

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

Authorization Code Flow

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

この例では、ClientがGoアプリケーション、Authorization ServerがKeycloak、User-Agentがブラウザです。

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

https://server.example.com/authorize?
response_type=code&
client_id=myapp&
redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
scope=openid&
state=...&
code_challenge=...

ブラウザ経由で発生する課題

RFC 9101は、認可リクエストがユーザーエージェントを通過することで、次の問題が起きると説明しています。RFC 9101 section 1

観点通常の認可リクエスト
完全性URLパラメーター自体にアプリケーション層の保護がない
送信元認証そのパラメーターを誰が作成したかを検証しにくい
機密性ブラウザや中継機器から内容を観測されうる
URL上の露出認可リクエストのパラメーターがURLに含まれる

TLSはブラウザと認可サーバーの間の通信を保護しますが、TLSセッションはブラウザで終端します。
ロードバランサーなどの中継機器でTLSが終端されることもあります。
そのため、TLSだけでは認可リクエストをブラウザから認可サーバーまで一貫して保護できません。

JARの基本

Request Object

JARでは、認可リクエストのパラメーターをJWTのクレームとして表現したRequest Objectを使います。
Request Objectには、response_typeclient_idredirect_uriscopestateなど、認可リクエストの処理に使うパラメーターを含めます。

Request Objectは次のいずれかでなければなりません。RFC 9101 section 5

  • JWSで署名されたJWT
  • JWSで署名した後、JWEで暗号化したNested JWT

JWEだけで署名のないRequest Objectにすることはできません。
署名と暗号化を組み合わせる場合は、先にJWS署名を行い、その結果をJWEで暗号化します。RFC 9101 section 4

JWSとJWEの役割

JWSとJWEが提供する性質は分けて考える必要があります。

  • JWSによる署名:Request Objectの完全性と送信元認証
  • JWEによる暗号化:Request Objectの機密性

したがって、JWSで署名したRequest Objectをrequestで送っても、JWTの内容自体はURL上に現れます。
内容を見えなくするには、JWEによる暗号化が別途必要です。

以前にJWEに関する記事を書きました。
KeycloakのIDトークンをJWEで検証する

Keycloakでサポートされているalgとenc
Terminal window
curl -s http://localhost:8080/realms/myrealm/.well-known/openid-configuration \
| jq '{
request_object_signing_alg_values_supported,
request_object_encryption_alg_values_supported,
request_object_encryption_enc_values_supported
}'
{
"request_object_signing_alg_values_supported": [
"PS384",
"RS384",
"EdDSA",
"ES384",
"HS256",
"HS512",
"ES256",
"RS256",
"HS384",
"ES512",
"PS256",
"PS512",
"RS512",
"none"
],
"request_object_encryption_alg_values_supported": [
"ECDH-ES+A256KW",
"ECDH-ES+A192KW",
"ECDH-ES+A128KW",
"RSA-OAEP",
"RSA-OAEP-256",
"RSA1_5",
"ECDH-ES"
],
"request_object_encryption_enc_values_supported": [
"A256GCM",
"A192GCM",
"A128GCM",
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512"
]
}

requestrequest_uri

Request Objectの渡し方には、次の2種類があります。

パラメーター渡し方動作
requestby value署名済みJWTを認可リクエストのパラメーターとして直接渡す
request_uriby referenceRequest Objectを参照するURIを渡し、認可サーバーが取得する

requestはJWTそのものがURLに載るため、通常のパラメーターよりURLが長くなることもあります。
request_uriを使うと、ブラウザに渡す認可リクエストを短いURIにできます。
ただし、request_uri自体がRequest Objectの機密性や完全性を提供するわけではありません。
取得したRequest ObjectのJWSまたはJWEによって、保護方式が決まります。

認可サーバーがrequest_uriを発行する場合

request_uriは、クライアントがあらかじめRequest Objectを認可サーバーへPOSTし、認可サーバーから受け取ったURIを使うこともできます。
RFC 9101 section 5.2.1では、認可サーバーがRequest ObjectをPOSTするURLをクライアントに提供し、クライアントがRequest URIを取得する例を示しています。

この場合も、認可リクエストのURLにrequest_uriを設定するのはクライアントです。
ただし、URIを用意しているのは認可サーバーなので、認可サーバーはそのURIに対応するRequest Objectを自身の保存領域などから取得します。
RFC 9101では、認可サーバーが提供するURIはURNであると説明されています。

RFC 9101 section 5.2.1

Request Objectのクレーム

このサンプルでは、主に次のクレームを設定します。

クレーム
issクライアントID
aud認可サーバーのissuer
client_idクライアントID
redirect_uriコールバックURL
response_typecode
scopeopenid
stateCSRF対策に使うランダム値
nonceIDトークンのリプレイ対策に使うランダム値
code_challengePKCEのチャレンジ

認可リクエストでは、次のクエリパラメーターの組み合わせが許可されます。

認可リクエストのクエリ扱い備考
client_id + request許可クエリのclient_idは、Request Object内のclient_idと一致しなければならない
client_id + request_uri許可request_uriが参照するRequest Object内のclient_idと一致しなければならない
client_id + request + request_uri禁止requestrequest_uriは同時に指定できない

RFC 9101 section 4–section 5

RFC 9101とOpenID Connect Coreの仕様差

OpenID Connect Core 1.0もRequest Objectを定義しており、RFC 9101のRequest Objectと互換性があります。
ただし、RFC 9101はOAuth 2.0全般を対象とするJARの仕様であり、OpenID Connect CoreはOAuth 2.0に認証機能を追加する仕様です。
そのため、Request Objectを使う場合の必須パラメーターや、外側の認可リクエストとRequest Objectの組み立て方には違いがあります。

以下では、この記事で扱うAuthorization Code Flowを前提に比較します。

必須パラメーターの違い

観点RFC 9101OpenID Connect Core根拠
request / request_uriどちらか一方が必須。両方は指定できないどちらも任意。OPがサポートしていない場合は対応するエラーを返すRFC 9101 section 5OIDC Core section 6
client_id外側の認可リクエストに必須。Request Object内の値と一致しなければならない外側の認可リクエストに必須。Request Object内にある場合は値が一致しなければならないRFC 9101 section 5OIDC Core section 6.1
response_type認可処理で使うパラメーターとしてRequest Objectに含める外側のOAuth 2.0形式にも必須。Request Object内にある場合は外側の値と一致しなければならないRFC 9101 section 4OIDC Core section 6.1
scope認可処理で使う場合はRequest Objectに含める外側に必須。openidを含めなければならない。Request Object内にあっても外側に必要RFC 9101 section 4OIDC Core section 6.1
redirect_uri認可処理で使う場合はRequest Objectに含めるAuthorization Code Flowでは必須。事前登録済みのURIと完全一致しなければならないRFC 9101 section 4OIDC Core section 3.1.2.1
state使用する場合はRequest Objectに含めるOPTIONALだが推奨。毎回変わる値として外側に置くこともできるRFC 9101 section 4OIDC Core section 3.1.2.1
nonce使用する場合はRequest Objectに含めるOPTIONAL。Request Objectまたは外側に置けるRFC 9101 section 4OIDC Core section 3.1.2.1
code_challengeOAuth 2.0の拡張パラメーターとして使用する場合はRequest Objectに含めるOIDC Coreでは定義されていない。PKCE(RFC 7636)のパラメーターとして扱うRFC 9101 section 4RFC 7636

RFC 9101では、外側の認可リクエストに必要なのはrequestまたはrequest_uriのどちらか一方とclient_idです。
response_typescoperedirect_uriなど、認可処理で使うパラメーターはRequest Objectに含めます。

一方、OpenID Connect Coreでは、Request Objectを使っても外側のOAuth 2.0形式にresponse_typeclient_idscope=openidを含めなければなりません。
これはRequest Object内に同じ値が存在する場合にも適用されます。

Request Objectの組み立てと値の優先順位

観点RFC 9101OpenID Connect Core根拠
Request Objectに含める範囲requestrequest_uriを除き、認可処理で使うすべてのパラメーター(拡張パラメーターを含む)を含める固定パラメーターをRequest Objectに、statenonceなどの可変パラメーターを外側に分けられるRFC 9101 section 4OIDC Core section 6.1
外側との重複クライアントは重複して送信してもよいが、認可サーバーはRequest Object内の値だけを使う両方に存在する場合はRequest Object内の値を使うRFC 9101 section 5OIDC Core section 6.3.3
外側に必須の値client_idrequestまたはrequest_uriresponse_typeclient_idscope=openid。Authorization Code Flowでは、組み立て後のリクエストにredirect_uriも必要RFC 9101 section 5OIDC Core section 6.1
client_idの不一致外側とRequest Object内の値が異なる場合はエラーRequest Object内にある場合、外側の値と一致しなければならないRFC 9101 section 6.3OIDC Core section 6.1
request / request_uriのJWT内指定Request Objectに含めてはならないRequest Objectに含めてはならないRFC 9101 section 4OIDC Core section 6.1

両仕様で論理的なパラメーターは同じでも、その値を外側の認可リクエストに置くか、Request Objectに置くかという規則が異なります。
特にOpenID Connectでは、response_typeclient_idを外側に含め、scopeにはopenidを含める必要があります。

署名・暗号化要件の違い

観点RFC 9101OpenID Connect Core根拠
最小限の形式JWS署名済み、またはJWS署名後にJWE暗号化したNested JWT署名済みまたは署名なしのRequest ObjectRFC 9101 section 5OIDC Core section 6.1
JWEのみ不可。暗号化する場合も先にJWS署名が必要署名なしでJWE暗号化することも仕様上は可能RFC 9101 section 4OIDC Core section 6.1
署名検証認可サーバーは署名を検証しなければならない署名付きの場合、登録済みのアルゴリズムと鍵で検証するRFC 9101 section 6.2OIDC Core section 6.3.2
署名の強制require_signed_request_objecttrueにすると、署名なしやalg=noneを拒否する。省略時の既定値はfalseCoreのRequest Object規定では署名なしも許容するRFC 9101 section 10.5OIDC Core section 6.1

したがって、次の説明はRFC 9101について述べる場合には正しいですが、OpenID Connect Coreにもそのまま適用することはできません。

RFC 9101では、JWEだけで署名のないRequest Objectにすることはできません。JWEを使う場合も、先にJWS署名を行わなければなりません。

OpenID Connect Coreでは、署名なしRequest Objectや、署名なしのJWTをJWEで暗号化したRequest Objectも許容されています。

request_uriの扱い

観点RFC 9101OpenID Connect Core根拠
渡し方URIが参照するリソースからRequest Objectを取得する同じRFC 9101 section 5.2OIDC Core section 6.2
URI長Request URI全体は512 ASCII文字を超えないことが望ましいRequest URI全体は512 ASCII文字を超えないことが望ましいRFC 9101 section 5.2OIDC Core section 6.2
クライアントが用意するURIhttps URIが必須原則https。Request ObjectをOPが検証可能な署名で保護している場合は例外RFC 9101 section 5.2OIDC Core section 6.2
認可サーバーが用意するURI認可サーバーから提供された場合はURNRequest Objectを含むリソースを参照するURLとして扱うRFC 9101 section 5.2OIDC Core section 6.2
Request Objectの取得原則HTTP GET。安全な別の取得方法も許容原則HTTP GET。認可サーバーは取得結果をキャッシュできるRFC 9101 section 5.2.3OIDC Core section 6.2.3
URIの推測対策十分なエントロピーが必要。適切なタイムアウト後の削除を推奨同じRFC 9101 section 5.2.1OIDC Core section 6.2.1
URIの事前登録本文ではrequest_urisの事前登録を定義しないDynamic Client Registrationのrequest_urisで事前登録でき、OPは事前登録を要求できるOIDC Core section 6.2

代表的なリクエスト例

RFC 9101のJARでは、外側の認可リクエストにclient_idrequestを指定し、その他の認可リクエストパラメーターをRequest Objectに含められます。

https://server.example.com/authorize?
client_id=myapp&
request=<JWS-signed Request Object>

Request Objectには、たとえばresponse_typeredirect_uriscopestatenoncecode_challengeを含めます。

OpenID ConnectでJARを使う場合は、OIDC Coreの要件により、response_typeclient_idscope=openidを外側に指定します。
Authorization Code Flowで必要なredirect_uriは、この例では外側に指定していますが、Request Objectに含めてリクエストを組み立てることもできます。

https://server.example.com/authorize?
response_type=code&
client_id=myapp&
redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
scope=openid&
request=<Request Object>

Request Object内にもresponse_typeclient_idを含める場合は、外側の値と一致させなければなりません。

RFC 9101とOpenID Connect CoreはRequest Objectを共有しますが、同じ必須条件を定めているわけではありません。
RFC 9101はJARとしてRequest Objectへのパラメーター集約と署名を要求します。
OpenID Connect Coreは、OIDCの認証リクエストとして処理するため、外側のリクエストにも追加の必須パラメーターを要求します。

検証環境とKeycloak設定

この記事で使用するアプリケーションの簡単な構成図です。

前提条件

  • DockerとDocker Composeを使用できる
  • Terraform、Go、jq、miseを使用できる
  • http://localhost:8080http://localhost:8081にブラウザでアクセスできる

バージョン

mise.tomlで指定しているバージョンを記載しています。

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

RSA鍵の生成

Request Objectを署名するためのRSA鍵ペアを生成します。

Terminal window
cd blog-code/oauth-jar
# 秘密鍵の生成(PKCS#8形式)
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
# 公開鍵の抽出
openssl rsa -pubout -in private.pem -out public.pem

サンプルリポジトリに含まれる鍵はローカル検証専用です。
公開リポジトリにある秘密鍵を本番環境で使わないでください。

構築手順

KeycloakのClient設定で公開鍵を使うため、Terraformを実行する前に鍵を生成します。

Keycloakを起動します。

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

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

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

サンプルアプリケーションを起動します。

Terminal window
air

8クライアントの設定

このサンプルでは、Terraformで8つのKeycloakクライアントを作成します。
アプリケーションのトップページ(http://localhost:8081/)には、各クライアントでログインするリンクが表示されます。

keycloak-clients

クライアントID、クライアントシークレット、コールバックURLには、環境変数を元にしたプロファイル名が付加されます。
たとえば、KC_CLIENT_ID=myappの場合、request-rs256-importのクライアントIDはmyapp-request-rs256-import、コールバックURLはhttp://localhost:8081/callback/request-rs256-importです。

プロファイルログインURLKeycloakのRequest object required署名鍵の検証方法送信されるパラメーター
plain/login/plainNot requiredなしなし通常の認可パラメーター
request-rs256-import/login/request-rs256-importRequest onlyRS256公開鍵のImportclient_id + request
request-rs256-jwks/login/request-rs256-jwksRequest onlyRS256jwks.urlclient_id + request
request-hs256/login/request-hs256Request onlyHS256クライアントシークレットclient_id + request
request-uri-rs256-import/login/request-uri-rs256-importRequest URI onlyRS256公開鍵のImportclient_id + request_uri
request-uri-rs256-jwks/login/request-uri-rs256-jwksRequest URI onlyRS256jwks.urlclient_id + request_uri
request-uri-hs256/login/request-uri-hs256Request URI onlyHS256クライアントシークレットclient_id + request_uri
both-rs256-import/login/both-rs256-importRequest or Request URIRS256公開鍵のImportclient_id + request + request_uri

Keycloak管理画面の表示名と、Terraformのextra_configで指定する値は次のように対応します。

Keycloak管理画面Terraformの値意味
Not requirednot requiredRequest Objectは任意
Request or Request URIrequest or request_urirequestまたはrequest_uriが必須
Request onlyrequest onlyrequestが必須
Request URI onlyrequest_uri onlyrequest_uriが必須

RS256の公開鍵Import方式では、jwt.credential.public.keypublic.pemを設定します。
JWKS方式では、use.jwks.url=truejwks.urlを設定し、Keycloakがアプリケーションの/jwksエンドポイントから公開鍵を取得します。
request_uriを使うクライアントでは、Keycloakが取得先として許可するURLパターンをrequest.urisに設定します。

構築とログイン

まだ環境を構築していない場合は、上の構築手順でKeycloak、Terraform、アプリケーションを起動します。

サンプルアプリケーションではブラウザで http://localhost:8081 にアクセスすると以下のようなそれぞれのKeyclaokクライアントに対応したログインボタンが画面に表示されます。

app-home

次のコマンドで指定したプロファイルが生成する認可リクエストを確認できます。

Terminal window
$ curl -sIL http://localhost:8081/login | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-plain&code_challenge=BVmHu4Ok2VsghoqnLdq4s8UBf5GymyOVhYO9AKsrBPg&code_challenge_method=S256&nonce=wzqHjdxKqHt8fy_if-J_5Zt5Bu27MTr9eq2urPur0ls&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fcallback%2Fplain&response_type=code&scope=openid&state=jblIL5SBKDavpF-ZB4a3l9di_bk6MxMtEKPmK640uIA
HTTP/1.1 200 OK
$ curl -sIL http://localhost:8081/login/request-rs256-import | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-request-rs256-import&request=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteWFwcC1yZXF1ZXN0LXJzMjU2LWltcG9ydCIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL215cmVhbG0iXSwiZXhwIjoxNzg4NzM5OTUwLCJpYXQiOjE3ODg3Mzk2NTAsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiY2xpZW50X2lkIjoibXlhcHAtcmVxdWVzdC1yczI1Ni1pbXBvcnQiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjgwODEvY2FsbGJhY2svcmVxdWVzdC1yczI1Ni1pbXBvcnQiLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoiR3Z5dlJaajBINEd0cE9jTExrY2xDYzJQNk81WHo2ZW5EREJYN1Q1MTlpSSIsIm5vbmNlIjoiOUMzTlZFbHdDNWdHMWowYXByYlRjMkZOc1RqNElmZmJvVG5ZalNyYk4wNCIsImNvZGVfY2hhbGxlbmdlIjoia19lN2VWQnF3SUhxYmdTaHBZd0FoX0dvaUFRSEFqRFpiQ3psV01yWXJuSSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYifQ.bPITkplYKh6HMcY7PoZ_hmhhn5DUtL8JWkVW4l1bRhc-NhS2skxPzclJ4FcsMyyzwGsoVJKWsB6hcREril2EbgFcP8_KjsggclvlpK7hnqaeVhI-NUb3DF-1ZXym8RZ1b206I46AMTiiDm_IUXQWHWSY7qF11dIflBAlfSNP8aad-5-1qRgNnlxnDJIf7nnughTBivMROh8k27JawuuY-Y_8moTQvN1Hqp09J9W4NadGsDLJHVZB2NmIgLx1MUAnehbd9vrebVGhNHdIeFN21YKTMk_uuMj82QmMoDaGvel39lTZ61m1aP1BrgWMQ0_rygZFz7oGo2mIRkWiSYo2Lw
HTTP/1.1 200 OK
$ curl -sIL http://localhost:8081/login/request-rs256-jwks | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-request-rs256-jwks&request=eyJhbGciOiJSUzI1NiIsImtpZCI6ImNsaWVudC1rZXktaG9nZSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteWFwcC1yZXF1ZXN0LXJzMjU2LWp3a3MiLCJhdWQiOlsiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teXJlYWxtIl0sImV4cCI6MTc4ODc0Mzg4MSwiaWF0IjoxNzg4NzQzNTgxLCJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF9pZCI6Im15YXBwLXJlcXVlc3QtcnMyNTYtandrcyIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MS9jYWxsYmFjay9yZXF1ZXN0LXJzMjU2LWp3a3MiLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoibDhSY1hIOC1FR0dPaXNhNGZmYTVvcUk2VjZVZUMtaHluQ2dLU2RTUk9INCIsIm5vbmNlIjoiRGFURlhKZUUwcXlFcGZjcHYweFFWYWVINkFVYzN1M01oZlFXM0tnTjBKcyIsImNvZGVfY2hhbGxlbmdlIjoiUU5HVE9BTzVTSHZrcnRkbVg5Tkw4S3hNVENVNUR0ZVpLQlo2M012YUVGRSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYifQ.H3ghw_T36k_VdQDDBjQZgB-SZGctt7q02quKbeyEA8iuRc5KCnrctSSCzajAW3lX9ZzNGGNDZ3tO5xJeJHDFQhXFpC_27RQ_l9uUq6UbRMhG4osZRl8RBZF5d10l1yAcDf_mReUUFYuaKekVarPSSJiBVeYaU5VmMboafEzhyr7RFaqqH3H443U_K1cvFtToC7GCZ_G6kxHC84d1VeQBRo_1J6DMeJ9Qs07taCONlmQcFmploP04I7gqexYYFxj0Fy6Jdm5hM796vw9ulFHzd6mpctzj_4SoVa79ioVIXs5V2IAKl5nSZgFxLtHnowMLDtPfFFZZjmGrV7TJ9b6-Xw
HTTP/1.1 200 OK
$ curl -sIL http://localhost:8081/login/request-hs256 | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-request-hs256&request=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteWFwcC1yZXF1ZXN0LWhzMjU2IiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9yZWFsbXMvbXlyZWFsbSJdLCJleHAiOjE3ODg3Mzk5OTksImlhdCI6MTc4ODczOTY5OSwicmVzcG9uc2VfdHlwZSI6ImNvZGUiLCJjbGllbnRfaWQiOiJteWFwcC1yZXF1ZXN0LWhzMjU2IiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL2NhbGxiYWNrL3JlcXVlc3QtaHMyNTYiLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoiMFB1dkRaMklJX2h4c2lzX1ZWRk9mRUFabVFCeHRZbDdvaGhra2Z5QlpLayIsIm5vbmNlIjoiTWl1UkdkU2xqWEpLQkxpNm1sajdBaS10SHFmREZkdVRUU2pPWjR5VVVsOCIsImNvZGVfY2hhbGxlbmdlIjoic3huS1hadkhpVG9UOE9fNDlYREc2b1RKT3NNQTFHanZUT1RHMFZfaDJ3SSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYifQ.UE6TB7EeZkyl6nGUyo0pEM_ucdQsX2Hoy8WzF9FjoSo
HTTP/1.1 200 OK
$ curl -sIL http://localhost:8081/login/request-uri-rs256-import | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-request-uri-rs256-import&request_uri=http%3A%2F%2Fhost.docker.internal%3A8081%2Frequest-objects%2FghYR1CJqcPwWfTDVcW2SesU3LicvO7RoNK_NlfMtWuc
HTTP/1.1 200 OK
$ curl -sIL http://localhost:8081/login/request-uri-rs256-jwks | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-request-uri-rs256-jwks&request_uri=http%3A%2F%2Fhost.docker.internal%3A8081%2Frequest-objects%2F2zDbJx6YRiHe0dery0IejjWHlVxecnSqa0hCXZSQBYE
HTTP/1.1 200 OK
$ curl -sIL http://localhost:8081/login/request-uri-hs256 | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-request-uri-hs256&request_uri=http%3A%2F%2Fhost.docker.internal%3A8081%2Frequest-objects%2F4f7YpCs4ZqiKSPNlon8bUA0nWi8bKJbbrtoNsEW_vsE
HTTP/1.1 200 OK
# request, request_uri同時指定はリダイレクト先で拒否される。
$ curl -sIL http://localhost:8081/login/both-rs256-import | grep -E 'Location|HTTP'
HTTP/1.1 302 Found
Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myapp-both-rs256-import&request=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteWFwcC1ib3RoLXJzMjU2LWltcG9ydCIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL215cmVhbG0iXSwiZXhwIjoxNzg4NzM5MTM5LCJpYXQiOjE3ODg3Mzg4MzksInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiY2xpZW50X2lkIjoibXlhcHAtYm90aC1yczI1Ni1pbXBvcnQiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjgwODEvY2FsbGJhY2svYm90aC1yczI1Ni1pbXBvcnQiLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoiTG1HRGNrNXBKMlptRzJZaUZEVzJieE00QWsxRTIxYmY5Y2FmY2xzNjFkZyIsIm5vbmNlIjoiRl9xUGtLamh0QmRlampIY2E5OVRvLVZ0dkViYnFCSEp6VzhiaW9CQkJZUSIsImNvZGVfY2hhbGxlbmdlIjoiSlE0WFBaSWNwSzFXTXBPakZtV2JuUVJMNUVtbEc2OWpFUlpNWXF5b2dDQSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYifQ.QBMufyTLTVA8cIyji_8UgkbbUwH2fLDP0QlzijVk0fRm3ESDsf6qe8MFdaADu6ZhvD6ImTyZMp0qxDpRRrZRmzdnLkOFD9zy2O19lnZYZvjUJs0LSdHACjENnD33vcT1y87P8T2JNmTJMW0M_QPV7GNghSbrIWQ7HYqmTI_EPf8cvCDohYsFhSQec3wp_EdFnSHh3FxfDMeaW8uTTL7DHatXrbCtUJIlgtutxSgAO33VyU9yFpCtTZe54ZIpsHmNirXcP-wVl5bxswV5wg6NxNfjE248LTj_2g4uvlewDSN52lrESd_N4Cuja0UeHUbVTCuj_TF7nnGCVI0iSuoKWg&request_uri=http%3A%2F%2Fhost.docker.internal%3A8081%2Frequest-objects%2FIb78niX7PYOW5CKtu6Sr_55BmpJ5mTdNdK2o1ZQQpPg
HTTP/1.1 400 Bad Request

/login/{profile}{profile}を、検証したいプロファイル名に置き換えます。
ブラウザで同じURLを開くと、Keycloakのログイン画面を表示できます。

プロファイルごとに、Locationヘッダーで確認する内容を示します。

プロファイルLocationで確認する内容
plainscoperesponse_typeredirect_uriなど
request-rs256-importrequestのJWTヘッダーにあるalg=RS256
request-rs256-jwksrequestのJWTヘッダーにあるalg=RS256kid
request-hs256requestのJWTヘッダーにあるalg=HS256
request-uri-rs256-importrequest_uriだけが含まれる
request-uri-rs256-jwksrequest_uriだけが含まれる
request-uri-hs256request_uriだけが含まれる
both-rs256-importrequestrequest_uriが同時に含まれる

requestの場合は、LocationヘッダーにJWT全体が含まれます。
JWSは改ざんを検出できますが、署名済みJWTのクレームを隠すものではありません。
一方、request_uriの場合にブラウザへ渡るのは参照URIで、Request Object本体はKeycloakからアプリケーションへのGETで取得されます。

署名鍵の別方式

requestrequest_uriは、Request Objectの署名方式とは独立した軸です。
Keycloakでは、次のような方式も選択できます。

署名方式Keycloakが使う鍵Keycloakでの設定
RS256クライアントの公開鍵Import、またはjwks_uriでJWKSを指定
HS256クライアントのclient_secretRequest Object signature algorithmをHS256に設定

request_uriはRequest Objectの渡し方、jwks_uriは署名検証用の公開鍵集合を取得するURLです。
両者は別のパラメーターです。
このサンプルでは、これらの組み合わせを8つのクライアントに分けて比較します。

requestによる検証

Request Objectの生成

Goアプリケーションは、認可サーバーのissuerをaudに設定したRequest Objectを作成します。

oidc.go
now := time.Now()
claims := RequestObjectClaims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: clientID,
Audience: jwt.ClaimStrings{issuerURL},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)),
},
ResponseType: "code",
ClientID: clientID,
RedirectURI: redirectURI,
Scope: "openid",
State: state,
Nonce: nonce,
CodeChallenge: challenge,
CodeChallengeMethod: "S256",
}
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(embeddedPrivateKey)
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signed, err := token.SignedString(privateKey)

issにはクライアントID、audにはDiscoveryで取得したKeycloakのissuerを設定します。
署名にはクライアントの秘密鍵を使い、Keycloakは登録済みの公開鍵で検証します。

認可リクエスト

request-rs256-importrequest-rs256-jwksrequest-hs256でログインを開始します。
レスポンスのLocationヘッダーには、client_idと署名済みJWTを含むrequestが設定されます。

Location: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
client_id=myapp-request-rs256-import&request=eyJ...<省略>

ここで、JWT全体がブラウザのURLに載っていることを確認できます。
JWSは改ざんを検出できますが、署名済みJWTのクレームを隠すものではありません。

request_uriによる検証

Request Objectを参照渡しする

request_uriを使う場合、クライアントはRequest Objectを一時的にホストし、そのURIを認可サーバーへ渡します。
この構成では、KeycloakがURIへGETリクエストを送り、取得したJWTを検証してから認可処理を続けます。

request-uri-rs256-importrequest-uri-rs256-jwksrequest-uri-hs256でログインを開始します。
KeycloakのRequest object requiredRequest URI onlyに設定済みで、アプリケーションはプロファイルに応じてrequest_uriを生成します。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?
client_id=myapp-request-uri-rs256-import&
request_uri=http%3A%2F%2Fhost.docker.internal%3A8081%2Frequest-objects%2F...

ブラウザに渡るのはclient_idrequest_uriだけです。
Request Objectの内容は、KeycloakからGoアプリケーションへのGETで取得されます。

request_uriの有効期限と一度きりの利用

request_uriは推測困難なランダム値にし、短い有効期限を設け、取得後に再利用できないようにします。
このサンプルでは、Request Objectを1分間保持し、Keycloakから取得された時点で削除します。

これにより、同じrequest_uriを繰り返し使う動作を防ぎます。
実運用では、複数インスタンス間で共有できる一時ストレージと、適切な有効期限も検討します。

参考:RFC 9101 section 11.2.1

検証するケースと失敗ケース

JARなしのリクエスト

この構成のplainプロファイルは、Request Objectを使わない通常の認可リクエストを確認するためのクライアントです。

Keycloakクライアントの設定でRequest object requiredNot requiredにしているため、Request Objectを含まない認可リクエストでも拒否されずに処理されます。
未認証の場合は、その後にログイン画面が表示されます。

Request Objectを必須にしたクライアントへRequest Objectを含まない認可リクエストを送ると拒否されます。

このサンプルでは、plainをRequest Objectを必須にするクライアントとは別に設定しています。

これは、JARを必須化しないことで保護を回避されるdowngrade attackへの対策です。RFC 9101 section 10.5

requestrequest_uriの同時指定

RFC 9101では、requestrequest_uriを同じ認可リクエストに含めることは禁止されています。RFC 9101 section 5

both-rs256-importを使うと、両方を送信できます。

Terminal window
curl -sI http://localhost:8081/login/both-rs256-import | grep 'Location'

Keycloakは次のようなエラーを記録し、ログイン画面を表示しません。

KC-SERVICES0097: Invalid request: java.lang.RuntimeException:
Illegal to use both 'request' and 'request_uri' parameters together

セキュリティ特性と注意点

JARの方式ごとの効果を整理すると、次のようになります。

方式完全性と送信元認証機密性ブラウザに載る情報
通常の認可リクエストなしなし認可パラメーター
JWS + requestなし署名済みJWT
JWS + request_uriなしrequest_uri
JWS + JWE + request暗号化JWT
JWS + JWE + request_urirequest_uri

この表のとおり、それぞれの手段が解決する問題は異なります。

  • JWSはRequest Objectの改ざん検知と送信元認証を提供する
  • JWEを組み合わせるとRequest Objectの機密性も得られる
  • request_uriはブラウザへ渡すURL上の情報量を減らす
  • request_uriそのものは、Request Objectの署名や暗号化の代わりにはならない

RFC 9101では、クエリパラメーターとRequest Objectのクレームが重複する場合でも、認可サーバーはRequest Object内の値を使うことが求められています。RFC 9101 section 5

これは、クエリとRequest Objectの値が異なる場合に、保護されていないクエリ側の値へ切り替えられることを防ぐためです。RFC 9101 section 10.7

RFC 9101 section 10.4: request_uriに関連するリスク

request_uriを使うと、認可サーバーはクライアントが指定したURIへアクセスしてRequest Objectを取得します。
つまり、署名を検証する前に、認可サーバーがクライアント指定のURIへアクセスする段階があります。

この取得処理には、通常の認可リクエストにはないリスクがあります。

予期しない取得先とDDoS

悪意のあるクライアントが、認可サーバーから到達可能な任意のURIをrequest_uriに指定できると、内部ネットワークへのアクセスを誘発するSSRF攻撃につながる可能性があります。
また、巨大なレスポンスや応答に時間のかかるURIを指定することで、認可サーバーのリソースを消費させるDDoSも考えられます。

RFC 9101は、request_uriが予期しない場所を指していないこと、レスポンスのメディアタイプがapplication/oauth-authz-req+jwtであること、取得にタイムアウトを設けること、再帰的なGETを実行しないことを原則として求めています。RFC 9101 section 10.4.1

Keycloakでは、クライアントのValid Request URIsに、取得を許可するRequest ObjectのURIを設定します。
これは、RFC 9101が挙げる「予期しない場所を指していないこと」を確認するための許可リストです。
Keycloakの公式ドキュメントも、この設定の目的をSSRF攻撃の回避と説明しています。 Keycloak Upgrading Guide

このサンプルのように、Request ObjectのURIにランダムなIDを含める場合は、次のようにパスを限定します。

Valid Request URIs:
https://client.example.com/request-objects/*

ホスト全体を許可する設定や、*だけの設定は避けます。

Valid Request URIsは取得先を制限する設定です。
これだけで、レスポンスサイズの制限、取得タイムアウト、メディアタイプの検証、再帰的な取得の禁止まで保証されるわけではありません。
これらは、認可サーバーの実装やネットワークの出口制御も含めて確認する必要があります。

request_uriの書き換え

request_uri自体には署名がないため、ブラウザ上で別のURIに書き換えられる可能性があります。
書き換え後のURIが攻撃者のRequest Objectや別のクライアントのURIを指すと、意図しない認可リクエストを取得する可能性があります。RFC 9101 section 10.4.2

このリスクに対して、Valid Request URIsは書き換え後の取得先が許可範囲外であれば拒否するために使います。
一方、取得されたRequest Objectの内容が改ざんされていないことは、JWSの署名検証によって確認します。
許可範囲内の別のRequest Objectに書き換えられた場合に備えて、クエリのclient_idとRequest Object内のclient_idが一致することも確認します。
URIの宛先の保護と、取得したRequest Objectの内容の保護は別の問題です。

設定の境界は、次のように確認できます。

ケース期待する結果
許可済みの/request-objects/<ランダム値>KeycloakがRequest Objectを取得し、認可処理を続行する
許可外のパスRequest Objectの取得前に拒否する
許可外のホストRequest Objectの取得前に拒否する

なお、今回のサンプルはクライアントがRequest Objectをホストし、KeycloakがそのURIへGETする構成です。
PAR(RFC 9126)では、クライアントが認可リクエストを認可サーバーへ直接送信し、認可サーバーが発行したrequest_uriを後続の認可リクエストで使用します。
そのため、クライアントが外部URIを指定して認可サーバーに取得させる今回の構成とは、request_uriの取得経路とリスクが異なります。RFC 9126

方式request_uriの取得経路
JAR by-referenceクライアントがホストする外部URIを認可サーバーがGETする
PARクライアントが認可サーバーへリクエストを送信し、認可サーバーが管理するURIを返す

まとめ

JARは、認可リクエストをRequest ObjectというJWTにまとめて送る仕組みです。

  • JWS署名により、認可リクエストの完全性と送信元認証を保護できる
  • JWEを組み合わせると、Request Objectの機密性も保護できる
  • requestはRequest Objectを直接渡し、request_uriはURIで参照渡しする
  • request_uriはURL上の情報量を減らすが、署名や暗号化そのものではない
  • KeycloakのRequest object requiredで、クライアントごとにJARを必須化できる

参考

Pagefind UI mount↑↓ select · enter open