fbpx

Keycloak (21.0.2) で クライアントポリシー + FAPI1 advanced を試す #keycloak #oauth #oidc

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

1. 目次

  1. 目次
  2. 概要
    2.1 本記事内での略称について
    2.2 今回の記事で説明する箇所
    2.3 今回の記事で説明しない箇所
  1. 環境の説明
    3.1 レルムの設定
    3.2 ユーザーの設定
    3.3 クライアントクライアントの設定
    3.4 クライアントポリシーの設定
  1. mTLS対応
    5.1 OAuth2 に関連するmTLSの仕様
    5.2 docker-compose の修正
    5.3 CA用の鍵と証明書の作成
    5.4 サーバーで用いるHTTPS用の鍵や証明書などの作成
    5.5 クライアント用の鍵や証明書などの作成
    5.6 Keycloak側での設定
  1. リクエストオブジェクト対応
    6.1 JWSに使うアルゴリズムの検討
    6.2 JWSに使う鍵情報とJWKSの作成
    6.3 KeycloakからJWKSの設定
    6.4 リクエストオブジェクトの作成
    6.5 JWS付きの認可リクエストの作成・送信
    6.6 Detached Signature
    6.7 トークンリクエスト
  1. 後書き

2. 概要


shiba チームの中村です。前回の記事では Keycloak でクライアントポリシーを設定した後で Financial-grade API Security Profile 1.0 - Part 1: Baseline の動きを確認していきました。しかし、Financial-grade API Security Profile 1.0 - Part 2: Advanced に対応する事は行っていませんでした。そこで今回の記事では Financial-grade API Security Profile 1.0 - Part 2: Advanced へ対応していきます。

また、今回も一緒に検証していくように発生しそうなエラーを踏み潰しながら試していきますので、 FAPI や Keycloak を既にお詳しい方や、結果だけ知りたい方は適時読み飛ばしながらご確認ください。

2.1 本記事内での略称について


本資料では以降の資料において、下記のように名称を省略して表記します。

名称 略称
Financial-grade API FAPI
Financial-grade API Security Profile 1.0 - Part 1: Baseline FAPI1 Baseline
Financial-grade API Security Profile 1.0 - Part 2: Advanced FAPI1 Advanced

2.2 当記事で説明する内容


今回の記事では主に下記の部分について解説していきます。

  • Keycloak 21.0.2 を用いてのクライアントポリシーの挙動
  • クライアントポリシーとして FAPI1 Advanced を設定した時のリクエストとレスポンスの例

2.3 当記事で説明しない内容


  • FAPI に関する詳細な説明
  • Keycloak の詳細な説明

3. 環境の説明


前回の環境は Keycloak の 16.1.1 を用いて試していましたが、今回は現時点で最新に近いKeycloak 21.0.1 用いて検証を進めていきます。

また、Keycloak 17.0.0 から Quarkus ベースのディストリビューションが使用されはじめましたが、Keycloak 20.0.0 から WildFly のディストリビューションが削除されました。

それに合わせて本資料も Quarkus ベースのKeycloakを前提とするにあたり、前回の資料(WildFlyベースのKeycloak)と比較して主に下記の変更点があります。

  1. Keycloak の構成が大幅に変更
  2. デフォルトのコンテキストパスから /auth が削除

その他の変更点についてはこちらをご確認ください。

まず1についてですが、docker-compose.yml は下記のように変更されます。

"前回までの" docker-compose.yml

version: '3.8'
services:
  keycloak:
    container_name: keycloak
    image: jboss/keycloak:16.1.1
    command: -b 0.0.0.0
    ports:
      - "8088:8080"
    environment:
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: password

 

"今回の" docker-compose.yml

services:
  keycloak:
    container_name: keycloak
    image: quay.io/keycloak/keycloak:21.0.2
    entrypoint: ["/opt/keycloak/bin/kc.sh","start-dev"]
    ports:
      - "8088:8080"
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: password
      KC_LOG_LEVEL: debug

※ 検証には必須ではありませんが、今回はJWSに署名してJWSを作成してリクエストに付与するケースなどが出てくるので、 Keycloak 側に指定したアルゴリズムと署名のアルゴリズムが違う場合などのエラーの詳細はログを確認するしかないため KC_LOG_LEVEL を debug として指定します。

次に2についてですが、ざっくり例を出して説明すると前回の記事と比較して本記事では下記のように変更になります。

対象 変更前 変更後
管理コンソール http://localhost:8088/auth/admin http://localhost:8088/admin
authorization_endpoint http://localhost:8088/auth/realms/sample-realm/protocol/openid-connect/auth http://localhost:8088/realms/sample-realm/protocol/openid-connect/auth
token_endpoint http://localhost:8088/auth/realms/sample-realm/protocol/openid-connect/token http://localhost:8088/realms/sample-realm/protocol/openid-connect/token

3.1 レルムの設定

また、レルム、ユーザー名、クライアントは前回の記事と全く同じデータを使っていきます。

レルムの作り方については公式ドキュメントをご参照ください。

フィールド
Realm name sample-realm

3.2 ユーザーの設定

ユーザーについては前回の記事と変わらず管理コンソールでも大きなUIの変更はされておりません。前回の記事と同様に下記のデータで作成します。

ユーザーの作り方については公式ドキュメントをご参照ください。

フィールド
Username cl-taro

また、ユーザーの Credentials については今回も下記のように作成します。

フィールド
Password password
Temporary Off

3.3 クライアントの設定

Keycloak の更新に合わせて、クライアントに設定画面なども少し変わっていますが作成するクライアントの内容は前回の記事と変わらず下記のようになります。

主な変更点としては、Access Type という設定項目でクライアントタイプについて、 public か confidential を選択していましたが、Client authentication という名前の設定項目に変更されています。

クライアントの作り方については公式ドキュメントをご参照ください。

フィールド
Client ID test-client
Client Type OpenID Connect (defaultの値)
Client authentication Off (defaultの値)
Authorization Off (defaultの値)
Standard flow ✓ (defaultの値)
Direct access grants ✓ をはずす
Valid redirect URIs https://client.example.com/test

また、PKCE の項目などを変更する Advanced もタブ分けされていますので、下記のようにタブを移動して確認する必要があります。

Advanced タブの Advanced Settings の中にある下記のPKCEのメソッドも変更しておきます。

フィールド
Proof Key for Code Exchange Code Challenge Method S256

3.4 クライアントポリシーの設定

クライアントポリシーの設定についても前回の記事と変わらず管理コンソールでも大きなUIの変更はされておりません。前回の記事と同様に下記のデータで作成します。

フィールド
Name test-policy
Conditions any-client
Client Profiles fapi1-advanced

Configureの位置が下記のように変更されているので、Realm Setting の位置も合わせてずれていますが、該当タブ内での変更方法などはさほど大きくは変わっておりません。設定のイメージについては、前回の記事をご確認ください。

4. 前回の記事の振り返り

前回の検証では作成したクライアントについて振り返ってみてみると、下記の条件でした。

フィールド
クライアントタイプ パブリック (public)
認可グラント 認可コード

という状況でクライアントポリシーの 'fapi-1-advanced' を適用してPKCEを試していました。簡易のイメージ図を出すと下記のようなイメージです。

では再度リクエストとレスポンスを送ってみましょう。

前回の記事と比較してKeycloakの更新に合わせて、authorization_endpoint のパスから /auth が無くなっていることに注意してください。

認可リクエストを再度送ってみましょう。

URL欄からレスポンスを確認すると下記のような値が確認できます。

https://client.example.com/test?error=invalid_client&error_description=invalid+client+access+type&state=abcdefghijk

URL のクエリパラメーターをデコードしてパラメータを見てみると、クライアントが無効であったことと、エラーの説明としてクライアントのアクセスタイプが無効という情報がわかります。

パラメータ名
error invalid_client
error_description invalid client access type

ここで言うクライアントアクセスタイプは OAuth2 で言うクライアントタイプを指しているので、public なところを confidential に変更してみます。

クライアントタイプを変更するために、クライアントの設定画面から、 Client authenticatio の値を On に変更して Save を押してみましょう。ですが現在は変更しようとすると、下記のエラーがポップアップで表示されます。

Client could not be updated: Invalid client metadata: token_endpoint_auth_method

現在はクライアントポリシーで制限されており、条件を満たさないクライアントは作成しづらいので一旦作成済みのクライアントポリシーである test-policy のクライアントプロファイルである fapi1-advanced について Status の欄のトグルスイッチを押してポリシーを一度無効化します。

変更前のイメージ:
変更後のイメージ:
また、変更中に下記のようなポップアップが出た場合は Disable を選んでください。

このプロファイルを無効化した状態で、先程と同じようにクライアントの設定画面から、 Client authentication の値を On に変更して、Save を押してクライアントの情報を保存してください。そして先程無効化した fapi1-advanced の test-policy を再度有効にしてください。

また、クライアントの Client authentication の値を On に変更することで、Credentials のタブが新しく表示されます。

では FAPI1 Advanced にクライアント認証に関する記述がないかを確認してみます。

認可サーバーに関する記述に下記のような記述があります。

14. shall authenticate the confidential client using one of the following methods (this overrides FAPI Security Profile 1.0 - Part 1: Baseline clause 5.2.2-4):  
    1. tls_client_auth or self_signed_tls_client_auth as specified in section 2 of MTLS, or  
    2. private_key_jwt as specified in section 9 of OIDC;

上記のように、 OAuth2 に関連するMTLS の仕様のセクション2にでてくる tls_client_auth または、self_signed_tls_client_auth ないしは、OIDC Core のセクション9にでてくる private_key_jwt を用いてクライアントを認証する必要があります。

今回は tls_client_auth の形で Client 認証をしていくことにしましょう。

5. mTLS対応

OAuth2 に関連するMTLSの前に、mTLSについて簡単に確認します。 Google Cloudさんのドキュメントの表現を借りると、下記です。

相互 TLS(mTLS)は、クライアントとサーバー間の相互認証のための業界標準プロトコルです。mTLS プロトコルは、ネットワーク接続の両端で、双方がクライアント証明書に関連付けられた秘密鍵を保持していることを確認することで、クライアントとサーバーの両方が自身であると主張することを保証します。

上記引用部については、相互で自身を証明書で保証するイメージさえできれば今は問題ありません。

5.1 OAuth2 に関連するmTLSの仕様

このようなmTLSの振る舞いとアクセストークンを紐付けることで、アクセストークンが横取りされてしまった際に、悪用されることを軽減できるというのが OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens という仕様です。

この仕様は主に下記2点の方法を提供する仕様だとイメージしてください。

  1. 認可サーバーがアクセストークンにクライアントのmTLSの証明書を紐付ける方法
  2. リソースサーバーがトークンリクエスト時に送信されたアクセストークンについて、トークンを送信したクライアントに対して発行されたかものかを確認する方法

Keycloak で試す場合は下記の3つの流れになります。

① クライアントはトークンリクエスト時にKeycloakとmTLS通信し、クライアントの証明書をKeycloakにわたす(証明書の情報はKeycloak側で確認)

② KeycloakはアクセストークンのJWTの中の cnf の中にある x5t#S256 という値にクライアントの証明書のハッシュ情報を格納してアクセストークンなどをクライアントに返却。

{
  "exp": 1677754428,
  "iat": 1677754128,
  "auth_time": 1677754095,
  "jti": "e36385c1-97b2-4723-8f8a-2e2d5784d489",
  "iss": "https://localhost:8443/realms/sample-realm",
  "sub": "ab767b12-e843-4a19-8e78-099f795ce8aa",
  "typ": "Bearer",
  "azp": "test-client",
  "nonce": "abcdefghijk",
  "session_state": "b5187cfc-08ee-46d6-8f6b-ad354016cfd6",
  "acr": "1",
  "allowed-origins": [
    "https://client.example.com"
  ],
  "cnf": {
    "x5t#S256": "AW3AakVy486N8hMv_ERHTTCPDYC6Zvw-RcKzUkdAijQ"
  },
  "scope": "openid email profile",
  "sid": "b5187cfc-08ee-46d6-8f6b-ad354016cfd6",
  "email_verified": false,
  "preferred_username": "cl-taro",
  "given_name": "",
  "family_name": ""
}

③ クライアントはリソースサーバーへのアクセス時にmTLS通信し、リソースサーバーはアクセストークンの中の cnf の中にある x5t#S256 の情報と、mTLSで渡す証明書の情報が一致するかを確認する。

イメージとしては下記の様な流れで、①で渡すクライアント証明書はトークンリクエストを行ったクライアントしか所持していないはずなので、③のリクエストは、トークンリクエストを行ったクライアントだと判断できます。

5.2 docker-compose の修正

tls_client_auth のためにMTLSを行いたいので、まずはdocker-compose.ymlをを下記のように修正します。

services:
  keycloak:
    container_name: keycloak
    image: quay.io/keycloak/keycloak:21.0.2
    entrypoint: ["/opt/keycloak/bin/kc.sh","start-dev"]
    ports:
      - "8088:8080"
      - "8443:8443"
    volumes:
      - ./certs/keycloak-server.crt:/opt/keycloak/conf/tls.crt
      - ./certs/keycloak-server.key:/opt/keycloak/conf/tls.key
      - ./certs/client.jks:/opt/keycloak/conf/client.jks
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: password
      KC_LOG_LEVEL: debug
      KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/tls.crt
      KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/tls.key
      KC_HTTPS_TRUST_STORE_FILE: /opt/keycloak/conf/client.jks
      KC_HTTPS_TRUST_STORE_PASSWORD: changeit
      KC_HTTPS_CLIENT_AUTH: request

今回の修正ではHTTPSに対応するため、tls.crt, tls.key の設定を、 またmTLSに対応するため、client.jks の設定を追加しています。

用途 environment value
HTTPS KC_HTTPS_CERTIFICATE_FILE /opt/keycloak/conf/tls.crt
HTTPS KC_HTTPS_CERTIFICATE_KEY_FILE /opt/keycloak/conf/tls.key
mTLS KC_HTTPS_TRUST_STORE_FILE /opt/keycloak/conf/client.jks
mTLS KC_HTTPS_TRUST_STORE_PASSWORD changeit
mTLS KC_HTTPS_CLIENT_AUTH request

ではそれぞれのファイルを作成していきます。 コンソールを開いて、今回の docker-compose.yml を作成したリポジトリまで移動します。

まずは現在の位置を変数に格納します。

$ BASE=`pwd`

 

次に証明書を保存するディレクトリの作成しておきましょう。

$ mkdir -p $BASE/certs

 

5.3 CA用の鍵と証明書の作成

今回使うCA用の鍵と証明書を作成します。

$ openssl req -new -x509 -nodes -sha256 -days 365 -subj "/CN=test-ca" -keyout $BASE/certs/ca.key -out $BASE/certs/ca.crt

 

5.4 サーバーで用いるHTTPS用の鍵や証明書などの作成

HTTPSに使う秘密鍵を作成します。

$ openssl genrsa -out $BASE/certs/keycloak-server.key

 

次に上記の秘密鍵を用いて署名リクエスト(CSR)を作成します。

$ openssl req -new -key $BASE/certs/keycloak-server.key -sha256 -out $BASE/certs/keycloak-server.csr -subj "/CN=localhost"

 

最後に作成したCSRファイルと、CA用の証明書と鍵を用いて、証明書を作成します。

$ openssl x509 -req -days 365 -sha256 -in $BASE/certs/keycloak-s erver.csr -CA $BASE/certs/ca.crt -CAkey $BASE/certs/ca.key -set_serial 1 -out $BASE/certs/keycloak-server.crt

5.5 クライアント用の鍵や証明書などの作成

先程のサーバー用の証明書と同じようにクライアント用の鍵、署名リクエスト、証明書を作成していきます。

$ openssl genrsa -out $BASE/certs/client.key
$ openssl req -new -key $BASE/certs/client.key -out $BASE/certs/client.csr -subj "/CN=client.example.com"
$ openssl x509 -req -days 365 -sha256 -in $BASE/certs/client.csr -CA $BASE/certs/ca.crt -CAkey $BASE/certs/ca.key -set_serial 2 -out $BASE/certs/client.crt

 

前回の記事まで使っていた、Wildfly ベースのKeycloakのコンテナでは mTLS に対応するための、X509_CA_BUNDLE という環境変数には 証明書ファイルを指定するとよしなにやってくれるという黒魔術のようなスクリプトがありました。

それらが有効な場合は下記のような指定でした。

  • X509_CA_BUNDLE: /etc/x509/https/client.crt

ですが、 Quarkus ベースのKeycloakでは KC_HTTPS_TRUST_STORE_FILE の環境変数にはトラストストアファイルを指定する必要があるので、".jks形式" のファイルを作成していきます。

下記のコマンドでPKCS12形式に一度変換してください。また、処理中の Export Password は changeit で行ってください。

$ openssl pkcs12 -export -out $BASE/certs/client.p12 -name "certificate" -inkey $BASE/certs/client.key -in $BASE/certs/client.crt

次にkeytoolを使用して jks形式 に変換します。ですが keytool を動かすためにはJDKを入れるため、まずはdockerで適当な ubuntu のコンテナを立ち上げましょう。 コンテナを動かす際には、/certs をマウントしてください。

$ docker run -it --rm -v $BASE/certs:/tmp/crt ubuntu:23.04

まずはパッケージのリストを更新します。

$ apt -y update

 

その後パッケージを更新します。

$ apt upgrade

 

では準備が整ったのでOpenJDKを入れましょう。

$ apt -y install openjdk-19-jdk

 

次にkeytoolを使用して jks形式 に変換します。処理中の出力先キーストアのパスワードやソース・キーストアのパスワードは共に changeit で行ってください。

$ keytool -importkeystore -srckeystore /tmp/crt/client.p12 -srcstoretype PKCS12 -destkeystore /tmp/crt/client.jks -deststoretype JKS

 

では先程のdocker-compose.ymlをみて、 マウントするファイルを全て持っているかを確認してみましょう。

services:
  keycloak:
    container_name: keycloak
    image: quay.io/keycloak/keycloak:21.0.2
    entrypoint: ["/opt/keycloak/bin/kc.sh","start-dev"]
    ports:
      - "8088:8080"
      - "8443:8443"
    volumes:
      - ./certs/keycloak-server.crt:/opt/keycloak/conf/tls.crt
      - ./certs/keycloak-server.key:/opt/keycloak/conf/tls.key
      - ./certs/client.jks:/opt/keycloak/conf/client.jks
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: password
      KC_LOG_LEVEL: debug
      KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/tls.crt
      KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/tls.key
      KC_HTTPS_TRUST_STORE_FILE: /opt/keycloak/conf/client.jks
      KC_HTTPS_TRUST_STORE_PASSWORD: changeit
      KC_HTTPS_CLIENT_AUTH: request

 

確認を終えたら、一度docker環境を作り直してください。

$ docker-compose down
$ docker-compose up

 

dockerの起動を確認したら、下記コマンドを実行してMTLSが成功しているかを確認してください。

$ curl -v https://localhost:8443 --cacert $BASE/certs/ca.crt --key $BASE/certs/client.key --cert ./certs/client.crt

 

正しく設定できていれば、下記のようなレスポンスが返ります。

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /Users/k-nakamura/Desktop/test/certs/ca.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=localhost
*  start date: Mar 2 10:07:58 2023 GMT
*  expire date: Mar 1 10:07:58 2024 GMT
*  common name: localhost (matched)
*  issuer: CN=test-ca
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fd512011c00)
> GET / HTTP/2
> Host: localhost:8443
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200
--- 以下略 ---

また、docker-compseの環境を作り直したので、HTTPSで管理画面を開き、下記の情報を再度設定しておいてください。

  • レルム
  • ユーザー
  • クライアント
  • クライアントポリシー

5.6 Keycloak側での設定

ではMTLSでのクライアント認証に必要な残りの作業として、Keycloak側の設定していきます。作業前にクライアントポリシーの test-policy を無効化しておきましょう。無効化後に clients タブから test-client の設定に移動して、 Credentials のタブに移動します。

そして Client Authenticator のフィールドから X509 Certificate を選択してください。次に、Subject DN のフィールドを CN=client.example.com に変更して、Save を押してください。

下記のような確認のポップアップが出たら、Yes を選択してください。

変更後に クライアントポリシーの test-policy を再度有効に変更してください。

では再度認可リクエストを送ってみましょう。また、この際にHTTPからHTTPSに、またdocker-composeに記載したportと合わせて、8088から8443に変更して送信してみましょう。

URL欄からレスポンスを確認すると下記のような値が確認できます。

https://client.example.com/test?error=invalid_request&error_description=Missing+parameter%3A+%27request%27+or+%27request_uri%27&state=abcdefghijk

URL のクエリパラメーターをデコードしてパラメータを見てみると、 パラメーターの request ないしは request_uri のどちらかが不足していることがわかります。

パラメータ名
error invalid_client
error_description Missing parameter: 'request' or 'request_uri'

では FAPI1 Advanced に関連する記述がないかを確認してみます。

認可サーバーに関する記述に下記のような記述があります。

1. shall require a JWS signed JWT request object passed by value with the request parameter or by reference with the request_uri parameter;

つまり認可リクエストには request パラメーターないしは request_uri パラメーターを用いて、JWS (署名付き JWT )を渡す必要があります。

6. リクエストオブジェクト対応

リクエストオブジェクトは、認可リクエストの Claim がリクエストパラメータとなる JWT です。request パラメータないしは request_uri パラメータを使うことで認可リクエストにのせることができ、JWTに署名・暗号化することが可能です。

それぞれのパラメータはオプショナルであり、request の場合はJWTの値を示します。例示すると、署名する前のJWTでは下記のような値です。

{
  "iss": "test-client",
  "aud": "https://localhost:8443/realms/sample-realm",
  "response_type": "code",
  "scope": "openid email",
  "client_id": "test-client",
  "nonce": "abcdefghijk",
  "state": "abcdefghijk",
  "redirect_uri": "https://client.example.com/test",
  "code_challenge": "x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE",
  "code_challenge_method": "S256"
}
```

request_uri の場合はJWTの参照先を示します。例示すると下記のような値です

https%3A%2F%2Fclient.example.com%2Frequest.jwt%23GkurKxf5T0Y-mnPFCHqWOMiZi4VS138cQO_V7PZHAdM

今回の検証では認可リクエストの request パラメータに、JWTに署名したJWSを用い進めます。簡易のイメージ図を出すと下記のようなイメージです。

6.1 JWSに使うアルゴリズムの検討

ここから JWS の署名のための鍵情報などを作成していきましょう。まずは署名のアルゴリズムを指定する必要があります。

では、リクエストオブジェクト( JWS )の署名に使うアルゴリズムについて、FAPI1 Advanced の中で JWS のアルゴリズムに関連する記述がないかを確認してみます。

すると 8. Security considerations の中の、8.6. Algorithm considerationsに下記の記述があります。

8.6.  Algorithm considerations
    For JWS, both clients and authorization servers
        1. shall use PS256 or ES256 algorithms;
        2. should not use algorithms that use RSASSA-PKCS1-v1_5 (e.g. RS256); and
        3. shall not use none.

つまり RS256 と none は使うべきでなく、PS256 又は ES256 を使うべきだということがわかりますので、今回はPS256を選択してみます。

6.2 JWSに使う鍵情報とJWKSの作成

次に、鍵情報と JWKS を作成する必要があるのですが、今回はmkjwk.org というサービスを使ってみましょう。まずは画面右上から日本語に言語を変更してください。

次にタブから "RSA" を選択して、パラメーターは下記を指定した後に、"生成する" を押してください。

フィールド名
鍵のサイズ 2048
鍵の用途 署名
鍵のアルゴリズム PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
鍵のID test ※デフォルトのSpecifyの状態のまま test を指定してください
Show X.509 Yes

下記のように 公開鍵・秘密鍵・ JWKS が生成されます。

今回の検証での公開鍵は、一例として下記のものを使います。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgoDU7p/7uzESHYCDGN1E
DCszYShRL8woEb6HKaGjUJDsBwD79IvedvzPbdy/is5AIWPWfdyEaNmNixodEUQm
XhvPabn7AFnrwXluGTraABTDimuSZWgLq0pkVXDiNA7ki3IYw/dHqu3/YkesjkOS
5svSHVA5C/yqegnkv2H9Ij8ETIyno9vzr4HKjpqVRI1CmuBRd1fgg8D2ju0pX5cc
c6o9AoOgpDaoEThcYpcodgGrxZhmRJZazvoXkizMGAXZ64Zry4RJoUjwJVTVdTjx
jIw9KeI2S8/uoxC74iUDOiBmi2SHokX8nCTDTLsCIMoCzZM9BR1cwDLvFfg4by0n
kQIDAQAB
-----END PUBLIC KEY-----

 

今回の検証での秘密鍵は、一例として下記のものを使います。

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCCgNTun/u7MRId
gIMY3UQMKzNhKFEvzCgRvocpoaNQkOwHAPv0i952/M9t3L+KzkAhY9Z93IRo2Y2L
Gh0RRCZeG89pufsAWevBeW4ZOtoAFMOKa5JlaAurSmRVcOI0DuSLchjD90eq7f9i
R6yOQ5Lmy9IdUDkL/Kp6CeS/Yf0iPwRMjKej2/OvgcqOmpVEjUKa4FF3V+CDwPaO
7Slflxxzqj0Cg6CkNqgROFxilyh2AavFmGZEllrO+heSLMwYBdnrhmvLhEmhSPAl
VNV1OPGMjD0p4jZLz+6jELviJQM6IGaLZIeiRfycJMNMuwIgygLNkz0FHVzAMu8V
+DhvLSeRAgMBAAECggEAe3lXffsCWwc/o4gvAXyAYJ8TOs7Bmd6o3rkM+1fCxHyJ
xMqqmKMpthzWSZT96V/hj3X9wBG/edC0ujLX47k+L/ZSFS9xC9EIXYL9p4NmNYNv
y2yiE64QtF1rdueaLjUVCdbHFcrGFTSfWCaGXggTWqjnwPJhNzU1OshXlLgqn5Xx
Tm4Go1Pr4LqjyaMzh5wPNnTP4ElNSwCBTI35b8BDwCqSNiooWrR2VmDo0IsXLCSh
X9l22kbcBMcjqhWRn3E6GP7knYprEffpEBMW+XVlog73zAAha7WC6LZd9Xpv4djN
gTMzepPeo9kd3oPakHfoZMnUkOVzbjy1JtDR2UfSLQKBgQDPuUkjeznpdxELJymQ
1muXDqtLeiXBA3r+MgQhL8y2W8BBaLrePd3zcFpwkVZw5MVdVM3xX9tJPmbdsG5B
tCTvGGrjzZll6hPQq296EDdSbGca/pfibfdvVJgJT5PrFlLc/FGqq6Zj3QlUsXIe
Bp5r/SEcbKOGQIKIEUqqJuc9EwKBgQCg1T2wyG5SDgm0bNEhL7rdi+89sgSbewwh
wGY7FKWDGmK7ymrNrwjokg6DeniB7qH/xd1f+72MdQdWCJjm0xcjv5Z07bEvp6fA
oXN6g2+79BYuLvC3IFDeAbQqsZypOpgHmII4W70jyQ/oASRIXxL3iAJaatSXNpm6
CqTTRCARSwKBgF2MuZakKXmuaNuYAI09M+ks7xIn6ZbahWqzhc6YY16BRb1veDEc
tbesEt79ZWukbApTZghdvjlnRBZ1HcKzaarQWVtMvdf7Kn9gpezYHsIdFfY/UJHm
KnhWJb6Tuy81t43UiMcPVPlGk6wz2gwRuQkzT9UoTCDrLp4vA2xL5vpHAoGAY46c
eWoYoEKAT2dsrRZWnf2ZQp+Hqpcok1v97GSDb/xNUeGi61+GLDD9OvX80rFdJm7c
8iVq2B85Q1BfFcNld4OJJyhbnhwyA1Ptn9DswXP+puf3qeQfKs3zMNpxF3Bl243U
Tf67vgMgDYVnaEUyAHf4vO+UWWY7Eqa0EPMCxrECgYEAq0DCJvjKeBUZ1XODvQ1n
7W2ZNjmVRztJZwvfssWWOX2I3e8X9krYTRin/EnRJhtQsF8LNq17H01izW8hZ0uX
ET3KUr5F91bVP2Jxis2QHWt9l586cDmd9X1XeXWCQv1+vNI+9v0ULQ4sNxsN6u2q
siZWb2FpceQgn0qPRFuegaY=
-----END PRIVATE KEY-----

 

そして JWKS は、一例として下記のものを使います。

JWKSは任意のテキストファイル(例: jwks.txt)に保存しておいてください。

{
    "keys": [
        {
            "p": "z7lJI3s56XcRCycpkNZrlw6rS3olwQN6_jIEIS_MtlvAQWi63j3d83BacJFWcOTFXVTN8V_bST5m3bBuQbQk7xhq482ZZeoT0KtvehA3UmxnGv6X4m33b1SYCU-T6xZS3PxRqqumY90JVLFyHgaea_0hHGyjhkCCiBFKqibnPRM",
            "kty": "RSA",
            "q": "oNU9sMhuUg4JtGzRIS-63YvvPbIEm3sMIcBmOxSlgxpiu8pqza8I6JIOg3p4ge6h_8XdX_u9jHUHVgiY5tMXI7-WdO2xL6enwKFzeoNvu_QWLi7wtyBQ3gG0KrGcqTqYB5iCOFu9I8kP6AEkSF8S94gCWmrUlzaZugqk00QgEUs",
            "d": "e3lXffsCWwc_o4gvAXyAYJ8TOs7Bmd6o3rkM-1fCxHyJxMqqmKMpthzWSZT96V_hj3X9wBG_edC0ujLX47k-L_ZSFS9xC9EIXYL9p4NmNYNvy2yiE64QtF1rdueaLjUVCdbHFcrGFTSfWCaGXggTWqjnwPJhNzU1OshXlLgqn5XxTm4Go1Pr4LqjyaMzh5wPNnTP4ElNSwCBTI35b8BDwCqSNiooWrR2VmDo0IsXLCShX9l22kbcBMcjqhWRn3E6GP7knYprEffpEBMW-XVlog73zAAha7WC6LZd9Xpv4djNgTMzepPeo9kd3oPakHfoZMnUkOVzbjy1JtDR2UfSLQ",
            "e": "AQAB",
            "use": "sig",
            "kid": "test",
            "qi": "q0DCJvjKeBUZ1XODvQ1n7W2ZNjmVRztJZwvfssWWOX2I3e8X9krYTRin_EnRJhtQsF8LNq17H01izW8hZ0uXET3KUr5F91bVP2Jxis2QHWt9l586cDmd9X1XeXWCQv1-vNI-9v0ULQ4sNxsN6u2qsiZWb2FpceQgn0qPRFuegaY",
            "dp": "XYy5lqQpea5o25gAjT0z6SzvEifpltqFarOFzphjXoFFvW94MRy1t6wS3v1la6RsClNmCF2-OWdEFnUdwrNpqtBZW0y91_sqf2Cl7Ngewh0V9j9QkeYqeFYlvpO7LzW3jdSIxw9U-UaTrDPaDBG5CTNP1ShMIOsuni8DbEvm-kc",
            "alg": "PS256",
            "dq": "Y46ceWoYoEKAT2dsrRZWnf2ZQp-Hqpcok1v97GSDb_xNUeGi61-GLDD9OvX80rFdJm7c8iVq2B85Q1BfFcNld4OJJyhbnhwyA1Ptn9DswXP-puf3qeQfKs3zMNpxF3Bl243UTf67vgMgDYVnaEUyAHf4vO-UWWY7Eqa0EPMCxrE",
            "n": "goDU7p_7uzESHYCDGN1EDCszYShRL8woEb6HKaGjUJDsBwD79IvedvzPbdy_is5AIWPWfdyEaNmNixodEUQmXhvPabn7AFnrwXluGTraABTDimuSZWgLq0pkVXDiNA7ki3IYw_dHqu3_YkesjkOS5svSHVA5C_yqegnkv2H9Ij8ETIyno9vzr4HKjpqVRI1CmuBRd1fgg8D2ju0pX5ccc6o9AoOgpDaoEThcYpcodgGrxZhmRJZazvoXkizMGAXZ64Zry4RJoUjwJVTVdTjxjIw9KeI2S8_uoxC74iUDOiBmi2SHokX8nCTDTLsCIMoCzZM9BR1cwDLvFfg4by0nkQ"
        }
    ]
}

JWKS は任意のテキストファイル(例: jwks.txt)に保存しておいてください。

6.3 KeycloakからJWKSの設定

次に Keycloak の設定ですが、clients タブから test-client の設定に移動して、 Keys のタブに移動します。

JWKSを読み込ませるために、下記のimportを押してください。

Archive format の値を JSON Web Key Set に変更します。その後 Import file で先程作成した jwks を含んだファイルを指定して Import を押してください。

下記のような表示がでたら完了です。

6.4 リクエストオブジェクトの作成

では、次にリクエストオブジェクトである JWS (署名付き JWT)を作成していきます。

今回は Ruby で JWS を作成していきます。Ruby の実行環境については Docker で適当な Ruby コンテナを作成し、 irb (Ruby コンソール)で作業していきます。

まずはコンテナを立ち上げて接続します。イメージはdocker公式のものを使っていきます。

$ docker run -it --rm ruby:3.0 bash

 

コマンドを実行すると、コマンドを受け付ける状態になっていると思いますので引き続きこの環境で作業を続けます。

JWSの作成には JSON::JWT という gem を使っていきます。下記のコマンドで gem を install しましょう。

$ gem install json-jwt

 

grep コマンドで gem が存在することを確認します。

$ gem list | grep json-jwt

 

ではここで irb ( Ruby コンソール)を開いていきます。

$ irb

 

※ 以降 irb 内のプログラムは > 記号の後ろに続けて記載していきます。

では JWS (署名付き JWT)を作成していきます。 まずは gem を読み込みます。

> require 'json/jwt'
=> true

次に秘密鍵を変数に保存します。

> private_key = OpenSSL::PKey::RSA.new <<-PEM
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCCgNTun/u7MRId
gIMY3UQMKzNhKFEvzCgRvocpoaNQkOwHAPv0i952/M9t3L+KzkAhY9Z93IRo2Y2L
Gh0RRCZeG89pufsAWevBeW4ZOtoAFMOKa5JlaAurSmRVcOI0DuSLchjD90eq7f9i
R6yOQ5Lmy9IdUDkL/Kp6CeS/Yf0iPwRMjKej2/OvgcqOmpVEjUKa4FF3V+CDwPaO
7Slflxxzqj0Cg6CkNqgROFxilyh2AavFmGZEllrO+heSLMwYBdnrhmvLhEmhSPAl
VNV1OPGMjD0p4jZLz+6jELviJQM6IGaLZIeiRfycJMNMuwIgygLNkz0FHVzAMu8V
+DhvLSeRAgMBAAECggEAe3lXffsCWwc/o4gvAXyAYJ8TOs7Bmd6o3rkM+1fCxHyJ
xMqqmKMpthzWSZT96V/hj3X9wBG/edC0ujLX47k+L/ZSFS9xC9EIXYL9p4NmNYNv
y2yiE64QtF1rdueaLjUVCdbHFcrGFTSfWCaGXggTWqjnwPJhNzU1OshXlLgqn5Xx
Tm4Go1Pr4LqjyaMzh5wPNnTP4ElNSwCBTI35b8BDwCqSNiooWrR2VmDo0IsXLCSh
X9l22kbcBMcjqhWRn3E6GP7knYprEffpEBMW+XVlog73zAAha7WC6LZd9Xpv4djN
gTMzepPeo9kd3oPakHfoZMnUkOVzbjy1JtDR2UfSLQKBgQDPuUkjeznpdxELJymQ
1muXDqtLeiXBA3r+MgQhL8y2W8BBaLrePd3zcFpwkVZw5MVdVM3xX9tJPmbdsG5B
tCTvGGrjzZll6hPQq296EDdSbGca/pfibfdvVJgJT5PrFlLc/FGqq6Zj3QlUsXIe
Bp5r/SEcbKOGQIKIEUqqJuc9EwKBgQCg1T2wyG5SDgm0bNEhL7rdi+89sgSbewwh
wGY7FKWDGmK7ymrNrwjokg6DeniB7qH/xd1f+72MdQdWCJjm0xcjv5Z07bEvp6fA
oXN6g2+79BYuLvC3IFDeAbQqsZypOpgHmII4W70jyQ/oASRIXxL3iAJaatSXNpm6
CqTTRCARSwKBgF2MuZakKXmuaNuYAI09M+ks7xIn6ZbahWqzhc6YY16BRb1veDEc
tbesEt79ZWukbApTZghdvjlnRBZ1HcKzaarQWVtMvdf7Kn9gpezYHsIdFfY/UJHm
KnhWJb6Tuy81t43UiMcPVPlGk6wz2gwRuQkzT9UoTCDrLp4vA2xL5vpHAoGAY46c
eWoYoEKAT2dsrRZWnf2ZQp+Hqpcok1v97GSDb/xNUeGi61+GLDD9OvX80rFdJm7c
8iVq2B85Q1BfFcNld4OJJyhbnhwyA1Ptn9DswXP+puf3qeQfKs3zMNpxF3Bl243U
Tf67vgMgDYVnaEUyAHf4vO+UWWY7Eqa0EPMCxrECgYEAq0DCJvjKeBUZ1XODvQ1n
7W2ZNjmVRztJZwvfssWWOX2I3e8X9krYTRin/EnRJhtQsF8LNq17H01izW8hZ0uX
ET3KUr5F91bVP2Jxis2QHWt9l586cDmd9X1XeXWCQv1+vNI+9v0ULQ4sNxsN6u2q
siZWb2FpceQgn0qPRFuegaY=
-----END PRIVATE KEY-----
PEM
=> #<OpenSSL::PKey::RSA:0x000055be5c356c48 oid=rsaEncryption>

次にペイロード用の変数を準備します。基本的には認可リクエストに用いたパラメータを列挙していますが、OpenID Connect Coreに則り JWS なので、 iss 及び aud のパラメータをリクエストオブジェクトに含めるため、payloadに含んでいきます。

> payload = {
  'iss': 'test-client',
  'aud': 'https://localhost:8443/realms/sample-realm',
  'response_type': 'code',
  'scope': 'openid email',
  'client_id': 'test-client',
  'nonce': 'abcdefghijk',
  'state': 'abcdefghijk',
  'redirect_uri': 'https://client.example.com/test',
  'code_challenge': 'x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE',
  'code_challenge_method': 'S256'
}
=>
{:iss=>"test-client",
...

ではペイロードをJWT化しましょう。

> jwt = JSON::JWT.new payload
=>
{"iss"=>"test-client",
...

また、kid を "test" としたのでそれを含めていきます。

jwt.kid = "test"
=> "test"

JWTに署名して、JWS を作成します。

> signed_jwt = jwt.sign(private_key, :PS256)
=>
{"iss"=>"test-client",
...

作成した JWS を確認してみましょう。

> signed_jwt.to_s
=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzI1NiIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJ0ZXN0LWNsaWVudCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvcmVhbG1zL3NhbXBsZS1yZWFsbSIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJjbGllbnRfaWQiOiJ0ZXN0LWNsaWVudCIsIm5vbmNlIjoiYWJjZGVmZ2hpamsiLCJzdGF0ZSI6ImFiY2RlZmdoaWprIiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6Ly9jbGllbnQuZXhhbXBsZS5jb20vdGVzdCIsImNvZGVfY2hhbGxlbmdlIjoieDVUelk3RjczcHd1cE4yTW14Vl9wNjVwYVJjN3ZKck43YjFjUkwyQ0lHRSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYifQ.IVS25OPyr58_1gXzTrQIMEIud98Ie4zYu5iJts7tnpWO0d3JggRy8IV3c1d31fXiSTh_qccY8n5jYS-lDxqSfPYRMaSs_8WMIj55qC9A30kNPL14ywwgkQR04J9Jqv0EbEI5JmSt4JU-dPisLE3XcDCQCh_ezLftjorjIstE9e3FixD7YuFmYMeFxY33LGRUYR0DuzXqps6Aju-rtJ_kLBwyt5jE5sVstz433Ogi81kEwpJgHO-Plz7fQuWY52a9KfToY_qT8hq7BAy8xAvzmvj8L8gDFZjh_Ew9woWQsFZ9egY5jGJsqdeGrA-6pIjkWqQqeJ3ONHDAbvluXliFqA"

環境ごとに値は変わりますが、上記のような JWS 形式の文字列を取得できるので、これをリクエストオブジェクトとして使っていきます。

6.5 JWS付きの認可リクエストの作成・送信

では次に認可リクエストを作成していきます。

先程作ったリクエストオブジェクトを今まで送信していた認可リクエストに付与するのですが、リクエストオブジェクト内に存在するので今回は、 state, nonce, redirect_uri のパラメータを省略します。

URL欄からレスポンスを確認すると下記のような値が確認できます。

https://client.example.com/test?error=invalid_request_object&error_description=Missing+parameter+in+the+%27request%27+object%3A+exp&state=abcdefghijk

URL のクエリパラメーターをデコードしてパラメータを見てみると、リクエストオブジェクトにexpクレームが足りないとわかります。

パラメータ名
error invalid_request_object
error_description Missing parameter in the 'request' object: exp

では FAPI1 Advanced にexpクレームについて関連する記述がないかを確認してみます。認可サーバーに関する記述に下記のような記述があります。

13. shall require the request object to contain an exp claim that has a lifetime of no longer than 60 minutes after the nbf claim;

 

FAPI1 Advanced では OpenID Connect Core の中では必須とされていない exp クレームを含む必要があります。またこの文章で nbf クレーム後の生存期間が60分以内であることも記載されているので、 nbf クレームも必要であることがわかります。

では irb でリクエストオブジェクトを作成する際に exp クレームと nbf クレームを追加してみましょう。下記では変更部分だけ抜粋して記載しますが、同じようにリクエストオブジェクトの JWS を作成してください。

> payload = {
  'iss': 'test-client',
  'aud': 'https://localhost:8443/realms/sample-realm',
  'response_type': 'code',
  'scope': 'openid email',
  'client_id': 'test-client',
  'nonce': 'abcdefghijk',
  'state': 'abcdefghijk',
  'redirect_uri': 'https://client.example.com/test',
  'code_challenge': 'x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE',
  'code_challenge_method': 'S256'
}

この情報で作り出したリクエストオブジェクトの例が下記です。

eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzI1NiIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJ0ZXN0LWNsaWVudCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvcmVhbG1zL3NhbXBsZS1yZWFsbSIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJjbGllbnRfaWQiOiJ0ZXN0LWNsaWVudCIsIm5vbmNlIjoiYWJjZGVmZ2hpamsiLCJzdGF0ZSI6ImFiY2RlZmdoaWprIiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6Ly9jbGllbnQuZXhhbXBsZS5jb20vdGVzdCIsImNvZGVfY2hhbGxlbmdlIjoieDVUelk3RjczcHd1cE4yTW14Vl9wNjVwYVJjN3ZKck43YjFjUkwyQ0lHRSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYiLCJuYmYiOjE2ODExNjkyMzIsImV4cCI6MTY4MTE3MjgzMn0.ARyVgGffpejqpJAHeScOQXZLHPprvoxnqbjWNVMica6NoghLbfIin09W0YRYnRczUDZvpMgXWwbMi8hGmRdXgw1pofOBZ-K-xK9VE5aaTgmkh2ArSW8P2W4IeyZaRS83iD6KiZnM-8qalcy7eB9imOnd8A1LPg0bFcA-eU5_7BCjed47_Pb7HyJcu0EraSJh4ESOQrvMRxMfu6HsrIS6CiJZjp-jfbDTZMcTyTlqMemcCQAFWQrLTZypwbIrs7W2WaYUZWjpg_oAzX53Fdy0r3o9uL3nvGZ1JdJ8U4ppPDnCYzJwHEdlO-Hhyil5lPAjd8UU5FgcM5QqHWLxufOw_A

では認可リクエストを作成して開いてみましょう。

URL欄からレスポンスを確認すると下記のような値が確認できます。

https://client.example.com/test?error=invalid_request&error_description=invalid+response_type&state=abcdefghijk

URL のクエリパラメーターをデコードしてパラメータを見てみると、指定された response_type でブラウザログインを開始できないことがわかります。

パラメータ名
error invalid_request
error_description invalid response_type

では FAPI1 Advanced の中で response_type に関連する記述がないかを確認してみます。

認可サーバーに関する記述に下記のような記述があります。

2. shall require
  1. the response_type value code id_token, or
  2. the response_type value code in conjunction with the response_mode value jwt;

また、ID Tokenに関する記述に下記のような記述があります。

In addition, if the response_type value code id_token is used, the authorization server

  4. shall return ID Token as a detached signature to the authorization response;

つまりID Tokenを detached signature として用いるため、response_type の値として code id_token を指定するか、もしくは response_type に code を指定しながら response_mode に jwt を指定する必要があります。response_mode に jwt を指定する JARM を用いた方法もありますが、今回はできる限りシンプルにするため、 response_type を code id_token を指定する方向で進めてみましょう。

ではリクエストオブジェクトの response_type クレームを変更して作り直してみましょう。 下記では変更部分だけ抜粋して記載しますが、同じようにリクエストオブジェクトの JWS を作成してください。

> payload = {
  'iss': 'test-client',
  'aud': 'https://localhost:8443/realms/sample-realm',
  'response_type': 'code id_token',
  'scope': 'openid email',
  'client_id': 'test-client',
  'nonce': 'abcdefghijk',
  'state': 'abcdefghijk',
  'redirect_uri': 'https://client.example.com/test',
  'code_challenge': 'x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE',
  'code_challenge_method': 'S256',
  'nbf': Time.now.to_i,
  'exp': (Time.now + 1.hour).to_i
}

この情報で作り出したリクエストオブジェクトの例が下記です

eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzI1NiIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJ0ZXN0LWNsaWVudCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvcmVhbG1zL3NhbXBsZS1yZWFsbSIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIGlkX3Rva2VuIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJjbGllbnRfaWQiOiJ0ZXN0LWNsaWVudCIsIm5vbmNlIjoiYWJjZGVmZ2hpamsiLCJzdGF0ZSI6ImFiY2RlZmdoaWprIiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6Ly9jbGllbnQuZXhhbXBsZS5jb20vdGVzdCIsImNvZGVfY2hhbGxlbmdlIjoieDVUelk3RjczcHd1cE4yTW14Vl9wNjVwYVJjN3ZKck43YjFjUkwyQ0lHRSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYiLCJuYmYiOjE2ODExNjkzOTksImV4cCI6MTY4MTE3Mjk5OX0.UapN3S0QQ6TMJWK23MKC1K9lc5JNFCdmlYfac4Y4DZHWvacQomZ6IytiwUkira25t4vuJreJNwCgpgi0yP319XqoQdalEcudGsM69SSnvIJRd4cdO10axXQwDyiOYzV2wRcoGdB6zUAFMxaqCVdgXXpesJR9xHHIH4o71Q_3Hdm6rjPZAGBW6WAhu8CM-5Rb-9HM9huyu6C0tahTbJo8cBVa5vxhtMAFbVfazw4zlT8fbbJkdXTe2Wy0Mm_kMxmJ5AOW_xpS70DnRVtKeXQaGwfAUzkazHR1ViffQIdXKnt47Wh1yBnTkoCFEnSh81QFEG7qyRvbIKWda0nUxoXokA

では認可リクエストの response_type 書き換えて再度開いてみましょう。

URL欄からレスポンスを確認すると下記のような値が確認できます。

https://client.example.com/test#error=unauthorized_client&error_description=Client+is+not+allowed+to+initiate+browser+login+with+given+response_type.+Implicit+flow+is+disabled+for+the+client.&state=abcdefghijk

URL のクエリパラメーターをデコードしてパラメータを見てみると、implicit flow が許可されていないというエラーのように見えます。

パラメータ名
error unauthorized_client
error_description Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.

FAPIに準拠するため response_type を code id_token と指定したので、これはハイブリッドフローとなります。Keycloakのドキュメントを確認してみると、ハイブリッドフローを有効にするためには、下記2つのフラグを有効にする必要があります。

  • Standard flow
  • Implicit flow

ではKeycloak の設定を変更してみましょう。Keycloak の設定ですが、clients タブから test-client の設定に移動して、 Settings のタブに移動します。

そして Implicit Flow Enabled のチェックボックス を押して Save から設定を保存しましょう。 すると下記のエラーがポップアップで表示されます。

Client could not be updated: Invalid rootUrl

ここまでの設定時は一旦無効化して進めましたが、今回はこれらを全て解決してから進めてみましょう。このエラーは Root URL が入力されていないことに起因します。 今回は検証目的なので Settings タブにいるはずなのでそのタブのままで、暫定で下記のような値を入力してみましょう。

フィールド
Root URL https://client.example.com

入力後に Save を押すと下記のエラーがポップアップで表示されます。

Client could not be updated: Invalid adminUrl

このエラーは Admin URL が入力されていないことに起因します。 暫定で下記のような値を入力してみましょう。

フィールド
Admin URL https://client.example.com/admin

入力後に Save を押すと下記のエラーがポップアップで表示されます。

Client could not be updated: Invalid baseUrl

このエラーは Home URL が入力されていないことに起因します。 暫定で下記のような値を入力してみましょう。

フィールド
Home URL https://client.example.com

入力後に Save を押すと下記のエラーがポップアップで表示されます。

Client could not be updated: Invalid logoutUrl

このエラーは Backchannel logout URL が入力されていないことに起因します。 暫定で下記のような 値を入力してみましょう。

フィールド
Backchannel logout URL https://client.example.com/logout

入力後に Save を押すと下記のエラーがポップアップで表示されます。

Client could not be updated: not allowed signature algorithm.

このエラーは署名のアルゴリズムとして設定している一部が、許可されていない値となっていることです。 それでは署名周りのアルゴリズムの設定を確認してみましょう。Advanced タブの下記の項目を変更します。

フィールド
Access token signature algorithm PS256
ID token signature algorithm PS256
User info signed response algorithm PS256
Request object signature algorithm PS256

変更後に Save を押してください。下記のポップアップが出て保存が成功しているとわかります。

Client successfully updated

では再度認可リクエストを送信してみます。

https://localhost:8443/realms/sample-realm/protocol/openid-connect/auth?response_type=code id_token&scope=openid email&client_id=test-client&code_challenge=x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE&code_challenge_method=S256&request=eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzI1NiIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJ0ZXN0LWNsaWVudCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvcmVhbG1zL3NhbXBsZS1yZWFsbSIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIGlkX3Rva2VuIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJjbGllbnRfaWQiOiJ0ZXN0LWNsaWVudCIsIm5vbmNlIjoiYWJjZGVmZ2hpamsiLCJzdGF0ZSI6ImFiY2RlZmdoaWprIiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6Ly9jbGllbnQuZXhhbXBsZS5jb20vdGVzdCIsImNvZGVfY2hhbGxlbmdlIjoieDVUelk3RjczcHd1cE4yTW14Vl9wNjVwYVJjN3ZKck43YjFjUkwyQ0lHRSIsImNvZGVfY2hhbGxlbmdlX21ldGhvZCI6IlMyNTYiLCJuYmYiOjE2ODExNzAyOTcsImV4cCI6MTY4MTE3Mzg5N30.gPd79ZtTVVLTCyew2_yClXGmu-AG1rtF-tz_A2UqeIJgJFFG09aDOVPkFWKyfufnC4YJ5UK_rVxsTTXRpqFysddutvSHRGVYXywtYKbVLQPH6SoakppY5YlbDz_1Gr0IyIFta3qpnIpew691G2idBI7PnL9vRZPWms5cG2Uw0kJX5VvOuDDayCgegxHrUiA4cQE1tpPj7vw2RauTOOJzDVQbENiDQt_CJrvsKvzU8S3lcDrWqMquxKzjcM7HNDb0PPprFRM3OLWRq4Hjhxlqn5rU_EzUwE7HohOsll3fLLD5b0yvg9Z_jphVUyyMoFXFmTklKolwfWfqmjhEbfqyUQ

下記のようなログイン画面が確認できます。最初に作成した cl-taro さんでログインしてみましょう。

フィールド
Username cl-taro
Password password

また、下記のような認可画面がでたら、Yesで許可してください。

ログインすると、レスポンスとしては下記のような値が返ってくることを確認できます。

https://client.example.com/test#state=abcdefghijk&session_state=3d5d6617-1b29-4ba2-90e2-f15cd09e5419&code=2cd4f024-3c77-41db-8274-57f535b59254.3d5d6617-1b29-4ba2-90e2-f15cd09e5419.acbfc69e-c9ca-416e-8c60-2d66ce39e389&id_token=eyJhbGciOiJQUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxYlFualA1YVpnMmZGUVlWZnBHcGw5YlI1Zi1wdDNRTS13TmotdmllWjhJIn0.eyJleHAiOjE2ODExNzEwMDksImlhdCI6MTY4MTE3MDcwOSwiYXV0aF90aW1lIjoxNjgxMTcwNDA5LCJqdGkiOiI2NzIzYjkzNi1kMGFkLTQxMjktOTJhYy05ZDg5YTliYTA0ZDUiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6IjExZWI5NDAzLWYyNjQtNDBkOC1hZjJmLWE2NDUwOWY1ODU2MSIsInR5cCI6IklEIiwiYXpwIjoidGVzdC1jbGllbnQiLCJub25jZSI6ImFiY2RlZmdoaWprIiwic2Vzc2lvbl9zdGF0ZSI6IjNkNWQ2NjE3LTFiMjktNGJhMi05MGUyLWYxNWNkMDllNTQxOSIsImNfaGFzaCI6Ik5Mc29WbENPRmp4V0ZINGlBenRtcEEiLCJzX2hhc2giOiJ5aThnYWVvTWJrWllJaTRHLU4xamxnIiwic2lkIjoiM2Q1ZDY2MTctMWIyOS00YmEyLTkwZTItZjE1Y2QwOWU1NDE5In0.TDPkLYssza3hBrqQ6k7D6l9-kPpfDL33CQecPHQ5XGDOGpZB_MKBUY4m5fyCLoyd1KnU0ZKpE45jbbZqp5GtP78CeB31ylWw984vTXLIOJ_oLmdiMxL7rXjFraAzVC16EwsWWbaP6_fbpKDs6QZL_kuCd8baQfvjPW8zcPx9d3xW7Te0RTqjd3I01tk9PAmZGaEGi5ONvX1yqT59DycXBVxOvjYG08-Qyemi5_M_sblX2VUC0ywG7Nw9Z8y3urRAhL8-0MSjXK1wKee9DDc_KWNjZA_0_louFyTxaR4aqeUR5MYJJRvgBGsbLsu3ZSVnE_9pipZ927QqU_hs3BCzhA

6.6 Detached Signature

認可リクエストのレスポンスからID Tokenを取り出して、jwt.io 等でJWTの中身を見てみましょう。すると下記のPayloadを確認できます。すると、c_hash 及び s_hash という値が確認できます。

{
  "exp": 1681171009,
  "iat": 1681170709,
  "auth_time": 1681170409,
  "jti": "6723b936-d0ad-4129-92ac-9d89a9ba04d5",
  "iss": "https://localhost:8443/realms/sample-realm",
  "aud": "test-client",
  "sub": "11eb9403-f264-40d8-af2f-a64509f58561",
  "typ": "ID",
  "azp": "test-client",
  "nonce": "abcdefghijk",
  "session_state": "3d5d6617-1b29-4ba2-90e2-f15cd09e5419",
  "c_hash": "NLsoVlCOFjxWFH4iAztmpA",
  "s_hash": "yi8gaeoMbkZYIi4G-N1jlg",
  "sid": "3d5d6617-1b29-4ba2-90e2-f15cd09e5419"
}

 

これは今回の検証ではJARMを用いずに、response_typeにid_tokenを含めており、ID TokenをDetached Signatureとして用いているからです。

ID token 自体は認可サーバーによって署名されますが、ID Token以外のその他のデータ(code, stateなど)についてはなんらかの対策をしていない限りは改ざんを検知できません。そこでそれぞれのハッシュ値をID tokenに含めることで改ざんを検知できるという仕組みです。

では FAPI1 Advanced にDetached Signatureに関する記述がないか確認すると、下記のような記述があります。

5.1.1.  ID Token as Detached Signature
While the name ID Token (as used in the OpenID Connect Hybrid Flow) suggests that it is something that provides the identity of the resource owner (subject), it is not necessarily so. While it does identify the authorization server by including the issuer identifier, it is perfectly fine to have an ephemeral subject identifier. In this case, the ID Token acts as a detached signature of the issuer to the authorization response and it was an explicit design decision of OpenID Connect Core to make the ID Token act as a detached signature.

This document leverages this fact and protects the authorization response by including the hash of all of the unprotected response parameters, e.g. code and state, in the ID Token.

また、FAPI1 Advanced の資料では下記のようにs_hashの定義も行っています。

While the hash of the code is defined in OIDC, the hash of the state is not defined. Thus this document defines it as follows.

s_hash

State hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the state value, where the hash algorithm used is the hash algorithm used in the alg header parameter of the ID Token's JOSE header. For instance, if the alg is HS512, hash the state value with SHA-512, then take the left-most 256 bits and base64url encode them. The s_hash value is a case sensitive string.

また、簡易のイメージ図を出すと下記のようなイメージです

6.7 トークンリクエスト

では返ってきた情報の code の値を使い、トークンのリクエストを行ってみましょう

curl -i -X POST \
   -H "Content-Type:application/x-www-form-urlencoded" \
   -d "client_id=test-client" \
   -d "grant_type=authorization_code" \
   -d "code=2cd4f024-3c77-41db-8274-57f535b59254.3d5d6617-1b29-4ba2-90e2-f15cd09e5419.acbfc69e-c9ca-416e-8c60-2d66ce39e389" \
   -d "code_verifier=09CgSwrVcqat4ZE6JXzWkIW9Ox61aX8rDY_oWcBWgkA" \
   -d "redirect_uri=https://client.example.com/test" \
   --cacert ./certs/ca.crt  \
   --key ./certs/client.key  \
   --cert ./certs/client.crt \
 'https://localhost:8443/realms/sample-realm/protocol/openid-connect/token'

下記のようにアクセストークンが取得できたら処理としては成功しています

{"access_token":"eyJhbGciOiJQUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxYlFualA1YVpnMmZGUVlWZnBHcGw5YlI1Zi1wdDNRTS13TmotdmllWjhJIn0.eyJleHAiOjE2ODExNzExODksImlhdCI6MTY4MTE3MDg4OSwiYXV0aF90aW1lIjoxNjgxMTcwNDA5LCJqdGkiOiIxNGY5ODFlYS0wYjQ0LTRkNTgtOTg5ZC0yMWEzOWNhYmVhZTAiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJzdWIiOiIxMWViOTQwMy1mMjY0LTQwZDgtYWYyZi1hNjQ1MDlmNTg1NjEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0ZXN0LWNsaWVudCIsIm5vbmNlIjoiYWJjZGVmZ2hpamsiLCJzZXNzaW9uX3N0YXRlIjoiM2Q1ZDY2MTctMWIyOS00YmEyLTkwZTItZjE1Y2QwOWU1NDE5IiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2NsaWVudC5leGFtcGxlLmNvbSJdLCJjbmYiOnsieDV0I1MyNTYiOiJBVzNBYWtWeTQ4Nk44aE12X0VSSFRUQ1BEWUM2WnZ3LVJjS3pVa2RBaWpRIn0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiIzZDVkNjYxNy0xYjI5LTRiYTItOTBlMi1mMTVjZDA5ZTU0MTkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImNsLXRhcm8iLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIifQ.jWa79RfJcbrjIseduwwelHBpzzTofb8UW32vJ6Ee40G8mmhjo7PaztBCRLPVxUT_3pSQTd3WqKymroCTVT68HzEG21ki7E4cLsT5oUt-tCsnCj7y_FG0YJrHkQ9SYMwzijoyssYvBN1q_y0jsrTshZ9K5ZMrOud59aXLj6VJolBzLE_aimdeqXQyDP_Ts3HuzMQ3hUJtnoQ0bWcOOrrtZXZsjCo01ThsIMPnF8SkZZ1AIKQsPRfvY51oIPmiYQTIRyjIX69k6Xsz9wnIvUqfKc0opbN0KiZQ9GQGWi1G454oJQlB4uCU4DQ9mR64c8_uwOrvqpgk5HwLr4Krcr_OTw","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyYmZmMGZmMC0wM2U5LTRlODgtYTYwZC1lM2IxODAzNzcxNWEifQ.eyJleHAiOjE2ODExNzI2ODksImlhdCI6MTY4MTE3MDg4OSwianRpIjoiZGU1NzcxNTEtZGE4Zi00OTA1LWI4ZjktMTc1NjQ3Mzg0YzNiIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiMTFlYjk0MDMtZjI2NC00MGQ4LWFmMmYtYTY0NTA5ZjU4NTYxIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6InRlc3QtY2xpZW50Iiwibm9uY2UiOiJhYmNkZWZnaGlqayIsInNlc3Npb25fc3RhdGUiOiIzZDVkNjYxNy0xYjI5LTRiYTItOTBlMi1mMTVjZDA5ZTU0MTkiLCJjbmYiOnsieDV0I1MyNTYiOiJBVzNBYWtWeTQ4Nk44aE12X0VSSFRUQ1BEWUM2WnZ3LVJjS3pVa2RBaWpRIn0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiIzZDVkNjYxNy0xYjI5LTRiYTItOTBlMi1mMTVjZDA5ZTU0MTkifQ.YsmsL4k91BRZcWowWixrpuFpzHFQ36h0Dt3svPTph7o","token_type":"Bearer","id_token":"eyJhbGciOiJQUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxYlFualA1YVpnMmZGUVlWZnBHcGw5YlI1Zi1wdDNRTS13TmotdmllWjhJIn0.eyJleHAiOjE2ODExNzExODksImlhdCI6MTY4MTE3MDg4OSwiYXV0aF90aW1lIjoxNjgxMTcwNDA5LCJqdGkiOiJiNmQ5YzNjMS05MjViLTRkNTYtODdhMS1hZDVmMjUyMWNkMzciLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6IjExZWI5NDAzLWYyNjQtNDBkOC1hZjJmLWE2NDUwOWY1ODU2MSIsInR5cCI6IklEIiwiYXpwIjoidGVzdC1jbGllbnQiLCJub25jZSI6ImFiY2RlZmdoaWprIiwic2Vzc2lvbl9zdGF0ZSI6IjNkNWQ2NjE3LTFiMjktNGJhMi05MGUyLWYxNWNkMDllNTQxOSIsImF0X2hhc2giOiJ6WW5aR2txMENxdEN5cTh0WHByTjlRIiwiYWNyIjoiMCIsInNpZCI6IjNkNWQ2NjE3LTFiMjktNGJhMi05MGUyLWYxNWNkMDllNTQxOSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2wtdGFybyIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiJ9.H4j68Rw_hZax815nrtp36SFKlwBEV8T-8DgCUrOMRMRBPG3eHopxV0OSRMzPTw474CDhYsgc_epe7eFMYBqxFBbj4neQe6a2W_3l6oLPSJsdI3YYwW1EVpVJimoMHeWfz82VPV6a5CMpbVwMOduvgCkWvTeOHIjhlVilQtPV2SQKO4KAzHHFPpjxg9VTPeu6wxffWX37D1rEYivQGaYzUUUp9RW4B7Z8cupUVbNLrqe5hddS1WgUgiY8x822MYG8iuhPWZpkSwM6YXDT5wbmc0fSGTx6M6V2Ho99tSFePn5cjhNMx9MbzkXjPhqLWAU-mvRlXFoPkWVIqFCLMUY3gw","not-before-policy":0,"session_state":"3d5d6617-1b29-4ba2-90e2-f15cd09e5419","scope":"openid profile email"}

 

7. 後書き

この記事と前回の記事で Keycloak のクライアントポリシーと、FAPI を簡単にふれていく過程で
  • PKCE を用いて認可コードの横取り対策
  • mTLS を用いてクライアント認証、及びトークンの横取り対策
  • リクエストオブジェクト を用いて認可リクエストの改ざん対策
  • ID Token を Detached Signature として用いることで認可コードなどの改ざん対策
などを簡単に見たり・試してみたりしました。この記事を読むことで、Keycloak のクライアントポリシーやそれによって FAPI に準拠していく過程のイメージやリクエストのやりとりを掴むことに対して少しでも参考になれば幸いです。
新規CTA