fbpx

CL LAB

HOME > CL LAB > Keycloak で クライアントポリシー + FAPI を試す with PKCE

Keycloak で クライアントポリシー + FAPI を試す with PKCE

 ★ 28

1. 目次

  1. 目次
  2. 概要
    - 2.1 今回の記事で説明する箇所
    - 2.2 今回の記事で説明しない箇所
  3. クライアントポリシーとは何か
  4. FAPI とは何か
  5.  検証環境について
    - 5.1 レルムの作成
    - 5.2 ユーザーの作成
    - 5.3 クライアントの作成
  6. 検証
    - 6.1. クライアントポリシーの適用前に、認可コードフローでアクセストークンの取得してみる
    - 6.2. クライアントポリシーで FAPI1 Baseline の適用
    - 6.3. "6.1" と同じ条件でアクセストークンを取得してみる
    - 6.4. FAPI1 Baseline に対応させて認可コードフローでアクセストークンの取得
    - 6.5. クライアントポリシーで FAPI1 Advanced の適用
    - 6.6. "6.4" と同じ条件でアクセストークンを取得してみる
  7. 後書き

2. 概要

shiba チームの中村です。今回は Keycloak でクライアントポリシーが実際にどう動くのかということが気になったので、簡単に確認していきます。

Keycloak 側での設定や実際のリクエストとレスポンスの変化を見るために、本資料では Financial-grade API Security Profile 1.0 の Part 1: Baseline と Part 2: Advanced を Keycloak 側でクライアントポリシーとして設定して検証してみました。

本資料では以降下記のように省略して表記します。

名称 略称
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

今回は例として、よりシンプルに下記のケースを想定して試していきます。

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

2.1 今回の記事で説明する箇所

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

  • Keycloak でのクライアントポリシーの設定方法
  • Keycloak での PKCE の設定方法
  • Keycloak でクライアントポリシーとして FAPI1 Baseline 及び FAPI1 Advanced を設定した時のパブリッククライアントのリクエスト例とレスポンス例

2.2 今回の記事で説明しない箇所

  • FAPI に関する詳細または全体的な説明
  • Keycloak のドキュメントで明確な手順とその詳細な解説

3. クライアントポリシーとは何か

Keycloak14.0.0のリリースで公式にサポートされるようになった(以前はプレビュー)、クライアントのアプリケーションのセキュリティに関するプロファイルへの準拠などを容易にするための機能です。よりわかりやすくシンプルに表現すると下記のようなことができます。

  • クライアントの構成に関するポリシーの設定ができる。
  • ポリシーによるクライアント構成のチェックができる。
  • Financial-grade API(FAPI) などのプロファイルへの準拠ができる。
    • ポリシーによるクライアントのリクエストなどのチェックができる。
本資料では、FAPI1 Baseline と FAPI1 Advanced をポリシーとして設定して、リクエストのチェックの部分を主に見ていきます。

また、参考になりそうな資料として OSSセキュリティ技術の会 第九回勉強会 で田畑さんが話されていた、最近のKeycloakのご紹介 ~クライアントポリシーとFAPI~ という資料がありますので、より興味がある方はこちらをご確認ください。

4. FAPI とは何か

FAPI(Financial-grade API)は OpenID Foundation の Financial-grade API ワーキンググループが策定している技術仕様の集まりを指します。

このワーキンググループは、標準的な OAuth や OpenID Connect で提供されるよりも高いレベルのセキュリティを必要とする、セキュリティと相互運用性に関する具体的な実装ガイドラインを提供することを目的としています。

元々はオープンバンキング関連のシナリオでの使用を目的としていましたが、現在は他のユースケースにも拡大しており、各種のセキュリティプロファイルは幅広く使えるものとなっています。

多くの仕様はまだドラフト状態なのですが、OpenIDFoundation の標準に向けたドラフトとしては下記の2つがあります。

その他の仕様とステータスについてはこちらをご確認ください。

5. 検証環境について

今回の環境は下記のような Docker Compose の設定を用いて、Keycloak の 16.1.1 のバージョンで試していきます。

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

検証に必要なデータは下記の通りです。管理画面を開いて設定していきましょう。

5.1 レルムの作成

まずは任意のレルムの作成をしてください。今回の記事ではレルムの名前は sample-realm で作成します。今後の文章ではレルムの Name を sample-realm として説明していくので、必要があれば適宜環境に合わせて読み替えてください。

5.2 ユーザーの作成

次にユーザーを作成します。今回の記事ではユーザーの Username は cl-taro  で作成します。

またパスワードとして Temporary の設定を OFF にして、パスワードの値としてpassword を設定します。

5.3 クライアントの作成

次にクライアントを作成します。今回の記事では Client ID を test-client とし Client Protocol を openid-connect であることを確認して作成します。

また、クライアントの作成後にクライアントの設定として、仮の値として Valid Redirect URIs に https://client.example.com/test を登録し、認可画面を出すためにConsent Required のオプションを ON に変更しておいてください。

6. 検証

今回は下記のような順番で振る舞いを見ていきましょう。

  • 6.1. クライアントポリシーの適用前に、認可コードフローでアクセストークンの取得してみる

  • 6.2. クライアントポリシーで FAPI1 Baseline の適用

  • 6.3. "6.1" と同じ条件でアクセストークンを取得してみる

  • 6.4. FAPI1 Baseline に対応させて認可コードフローでアクセストークンの取得

  • 6.5. クライアントポリシーで FAPI1 Advanced の適用

  • 6.6. "6.4" と同じ条件でアクセストークンを取得してみる

6.1 クライアントポリシーの適用前に、認可コードフローでアクセストークンの取得してみる

まず、クライアントポリシーを適用する前に、パブリッククライアントで、認可コードフローでアクセストーンを取得してみましょう。

6.1.1 まずは認可エンドポイントに認可リクエストを送信

例示した Docker Compose の設定を使っている場合は下記のようなリクエストになります。ブラウザでこちらのリンクを開いてください。

※ パラメーターを見やすいように改行すると下記になります。

http://localhost:8088/auth/realms/samplerealm/protocol/openid-connect/auth?
    response_type=code
    &scope=openid email
    &client_id=test-client
    &state=abcdefghijk
    &redirect_uri=https://client.example.com/test

6.1.2 ログインページへのリダイレクト

上記のリンクをブラウザで開くと下記のようなログインページがリダイレクトされて表示されますので、ここで作成したユーザーの情報を入力します。

フィールド名
ユーザー名 cl-taro
パスワード password

6.1.3 認可画面の表示

ログイン情報で入力してログインすることで認可情報を確認する画面が表示されます。 Yes を押下して認可情報を許可することとします。

keycloak-grant-access-view

6.1.4 リダイレクトページへ移動

認可を許可すると下記のような Redirect URI へ登録したページに遷移します。今回のケースでは仮で作成した client.example.com というサイトは実在しないので下記のような エラー画面は出ますが無視してください。

※ 下記画像は Google Chrome での例です。

トークンエンドポイントへのリクエストに使うため、 リダイレクト後のブラウザの URL の欄から、code の値をメモしておきましょう。

  • https://client.example.com/test?state=abcdefghijk&session_state=43d5774a-3708-4e7d-af12-95ae7089f345&code=0d588770-5fed-4f9f-819e-a585b73883f1.43d5774a-3708-4e7d-af12-95ae7089f345.7adc7336-b65c-48cf-95b4-8505bec3741a
上記の例では 0d588770-5fed-4f9f-819e-a585b73883f1.43d5774a-3708-4e7d-af12-95ae7089f345.7adc7336-b65c-48cf-95b4-8505bec3741a という値になります。

6.1.5 トークンエンドポイントに認可コードを提示してアクセストークンの取得

では取得した認可コードをパラメーターに含み、下記のような CURL を叩いてアクセストークンを取得しましょう。

curl -i -X POST \
   -H "Content-Type:application/x-www-form-urlencoded" \
   -d "client_id=test-client" \
   -d "grant_type=authorization_code" \
   -d "code=0d588770-5fed-4f9f-819e-a585b73883f1.43d5774a-3708-4e7d-af12-95ae7089f345.7adc7336-b65c-48cf-95b4-8505bec3741a" \
   -d "redirect_uri=https://client.example.com/test" \
 'http://localhost:8088/auth/realms/sample-realm/protocol/openid-connect/token'

下記のようにアクセストークンが取得できます。

{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4OWNPbWVhQmh5emZONHF5bk9veUlLdlM3WHJuOE1JMXA4M1RGa2hUTkVjIn0.eyJleHAiOjE2Mzc2NzMxMDIsImlhdCI6MTYzNzY3MjgwMiwiYXV0aF90aW1lIjoxNjM3NjcyNDg2LCJqdGkiOiJiYmI3NzljZS1lNTljLTRjNjMtYmUxNS1mNTQ5NTMzMGE0OGMiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODgvYXV0aC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjQ0NTZlMDc3LTkwMmItNDMxZi1hMTIzLTM0ZWQ2OGZiNDk5OCIsInR5cCI6IkJlYXJlciIsImF6cCI6InRlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjQzZDU3NzRhLTM3MDgtNGU3ZC1hZjEyLTk1YWU3MDg5ZjM0NSIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1zYW1wbGUtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInNpZCI6IjQzZDU3NzRhLTM3MDgtNGU3ZC1hZjEyLTk1YWU3MDg5ZjM0NSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2wtdGFybyJ9.ZdL4TmhNFCT5SlSZ2T4d0_vziKYDdppApczEm8WczryHVzcLcIi2qH-wGl-vEW192dImoh_DMaKdzy6TUSY7Mg-z-LWLyeDpOBXuRZwzM6MbtzG1p3qB2j-NwsHeQgrnojwsSJTBqi0dr51odpFTYIqxvRD3-UT_WsmQqMux4LZBW9C9G_QKMo0jbple6ZjizK2hv4tUwT9a1adh-ROQO2s0oc9Xhi-N_Gf0Hmi5tgs7mvM8GN9IHzPx6MpbDG8PmVgHhXC_O_qe5jQQ56pmnJxvnUBICxkpdTxWfUbMSKv-v7R8KTa-uLP-56nRKIm02fHzfazTc6vAiu1EfAenbw","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlOGE2ODM2NC0wMzIyLTRkZTMtYTQ4ZC0zMDE0NzRiZjliNmUifQ.eyJleHAiOjE2Mzc2NzQ2MDIsImlhdCI6MTYzNzY3MjgwMiwianRpIjoiMTgzNGJlNzQtZTk2NC00MWI4LWI5N2ItZGNkZWRjMWE5YTA2IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg4L2F1dGgvcmVhbG1zL3NhbXBsZS1yZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4OC9hdXRoL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJzdWIiOiI0NDU2ZTA3Ny05MDJiLTQzMWYtYTEyMy0zNGVkNjhmYjQ5OTgiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoidGVzdC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiNDNkNTc3NGEtMzcwOC00ZTdkLWFmMTItOTVhZTcwODlmMzQ1Iiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInNpZCI6IjQzZDU3NzRhLTM3MDgtNGU3ZC1hZjEyLTk1YWU3MDg5ZjM0NSJ9.DMpNbmmn88mtWIl-v9Sm8NFS359fMsgAtWnehmN_NGY","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4OWNPbWVhQmh5emZONHF5bk9veUlLdlM3WHJuOE1JMXA4M1RGa2hUTkVjIn0.eyJleHAiOjE2Mzc2NzMxMDIsImlhdCI6MTYzNzY3MjgwMiwiYXV0aF90aW1lIjoxNjM3NjcyNDg2LCJqdGkiOiJlZjQxMzgyNC0yYzNlLTRjNjctYjMwNS03ODc1ZTM4ZTk4NjQiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODgvYXV0aC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoidGVzdC1jbGllbnQiLCJzdWIiOiI0NDU2ZTA3Ny05MDJiLTQzMWYtYTEyMy0zNGVkNjhmYjQ5OTgiLCJ0eXAiOiJJRCIsImF6cCI6InRlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjQzZDU3NzRhLTM3MDgtNGU3ZC1hZjEyLTk1YWU3MDg5ZjM0NSIsImF0X2hhc2giOiJfYmVHcTFUaWJ2bEduUE1xbExneFRnIiwiYWNyIjoiMCIsInNpZCI6IjQzZDU3NzRhLTM3MDgtNGU3ZC1hZjEyLTk1YWU3MDg5ZjM0NSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2wtdGFybyJ9.IhDVslkvWMRscvFY5D2xJrrO2YkPF6pAqeaFuuJgp7vwUWft_4hIx6f3qUWtAnIqTT1eU-bOy7nG_jBVTMR9ZHRQ91yIht95efYMqSscjT4xApO2h-JjpRbE31YoHG3jZDfEh5V9UNdqM1gsy9uSChvcFIKG0CnW7Uq3LBWJ8-HPcneUI-dz3C1OTQMEuIyC-19igPMT7w_WJD3-r91F3WdD9pZeoX0M7o8_yU9Y3wz7UWywpKEMs0pL3GvEKX3NvdCQoyhzw6W6jcJ81aeJU6YN1mN0G9xIYllsGzKQDdow-o1fOG8LWxn7nLv5cbjRSgOs0WCtmEGLnB0InuRevw","not-before-policy":0,"session_state":"43d5774a-3708-4e7d-af12-95ae7089f345","scope":"openid profile email"}

6.2. クライアントポリシーで FAPI1 Baseline の適用

次にクライアントポリシーを設定してみます。 今回は FAPI1 Baseline を設定していきましょう。

Keycloak の管理画面の左側のメニュー項目の Realm Settings に移動し、Client Policies のタブを開きます。例示した Docker Compose の設定を使っている場合は下記のようなURLになります。

この画面には Profiles と Policies の2つがあります。

Profiles はクライアントの作成やクライアントの認証などの振る舞いに強制的に適用されるエグゼキューターの集まりを設定する項目です。 Profiles にはデフォルトでは下記の3つがあります。

  • fapi-1-baseline
  • fapi-1-advanced
  • fapi-ciba
次に Policies のタブへ移動します。Policies というタブに切り替えると下記のような画面になります。

Policies は Profiles にあるようなクライアントプロファイルを指定して、さまざまな条件と紐付ける設定する項目です。まずは Create を押すと下記のような画面になります。

 test-policy という名前を入力して Save を押してポリシーを作成してみましょう。

ポリシーを作成した時点では Conditions と Client Profiles のどちらも設定されておりません。

ではまず Condition から設定していきます。 Condition はどのクライアントに採用するかや、ポリシーをいつ(クライアントの作成・更新時 or 認可リクエスト時など)採用するかを決定するための設定です。まずは Condition の Create を押してください。

最初から any-client が選択されていますので、いまはそのまま Save を押します。これでこのレルムに存在するすべてのクライアントに対して、このポリシーが有効になりました。

保存されると、下記の画像のように Condition の欄に情報が追加されます。

では次に、Client Profiles の設定をしていきます。

Add client profile... という表示を押すと、適用する Profile の一覧が表示されるので、今回は fapi-1-baseline を選択してください。

追加後の画面は下記のようになります。

6.3."6.1" と同じ条件でアクセストークンを取得してみる

6.2 の設定ですべてのクライアントに、FAPI1 Baseline が適用されるようになりました。では確認のために 6.1 で行ったように認可コードフローでアクセストークンを取得してみます。

6.3.1 まずは認可エンドポイントに認可リクエストを送信

例示した Docker Compose の設定を使っている場合は下記のようなリクエストになります。ブラウザで下記のURLを開いてください。

6.3.2 リダイレクト先の確認

6.1.2 の時は、ログインページへのリダイレクトしていました。ですが、今回は下記のようなエラーを示す情報を含んで Redirect URI へ登録したページにリダイレクトしてきているはずです。

  • https://client.example.com/test?error=invalid_request&error_description=Missing+parameter%3A+nonce&state=abcdefghijk

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

パラメータ名
error invalid_request
error_description Missing parameter: nonce

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

認可サーバーに対する要求事項の 5.2.2.2. Client requesting openid scopeを見ると、クライアントが openid というスコープを要求した場合、認可サーバーは nonce パラメーターを要求しなければなりません

If the client requests the openid scope, the authorization server
1. shall require the nonce parameter defined in Section 3.1.2.1 of OIDC in the authentication request.

また、public なクライアントに関する要求事項の 5.2.3. Public clientを見ると、nonce をパラメーターに含めることが必要とあります。

8. shall include the nonce parameter defined in Section 3.1.2.1 of OIDC in the authentication request.

6.4. FAPI1 Baseline に対応させて認可コードフローでアクセストークンの取得

では先程のケースをベースとして FAPI1 Baseline に合わせたリクエストを作成してみます。

6.4.1 nonceを含み認可エンドポイントに認可リクエストを送信

先程のエラーでは nonce が足りないというエラーメッセージだったので、認可リクエストのパラメータに nonce を追加してみましょう。例示した Docker Compose の設定を使っている場合は下記のようなURLになります。

6.4.2 リダイレクト先の確認

今回は下記のようなエラーを示す情報を含んで Redirect URI へ登録したページにリダイレクトしてきているはずです。

  • https://client.example.com/test?error=invalid_request&error_description=Missing+parameter%3A+code_challenge_method&state=abcdefghijk

このURLのクエリパラメーターをデコードしてパラメータを見てみると、リクエストが無効であったこととエラーの説明として先程とは違いパラメーターの code_challenge_method が足りないという情報がわかります。

パラメータ名
error invalid_request
error_description Missing parameter: code_challenge_method

では code_challenge_method は何かというと、このパラメータは RFC 7636の PKCE という仕様で使うパラメータです。

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

認可サーバーに対する要求事項の5.2.2. Authorization serverを見ると、code challenge method として S256 を指定した RFC7636 への対応が必要だとわかります。

shall require RFC 7636 with S256 as the code challenge method;

また、public なクライアントに関する要求事項の 5.2.3. Public clientを見ると、RFC 7636 = PKCE の対応と、PKCE の code challenge method として S256 を使用することが要求されています。

1. shall support RFC7636;
2. shall use S256 as the code challenge method for the RFC7636;

コラム: PKCEとは

PKCE(Proof Key for Code Exchange) はピクシーとも呼ばれ、 OAuth 2.0 の public クライアントにおける認可コードの横取り攻撃(authorization code interception attack)を軽減するための仕様です。

この攻撃は主にスマートフォンが対象で、クライアントのリダイレクトエンドポイントにカスタムURIスキームのようにアプリケーションを起動する設定を行っている場合、正規のアプリと全く同じ設定をした悪意あるアプリをスマートフォンが意図せず開いてしまうことで、認可コードが悪意あるアプリに渡ってしまうという攻撃です。

図示すると下記の部分になります。
また、この仕様は認可コードを送信したクライアントと、トークンリクエストを送信したクライアントが同じであることを確認するための仕様で、認可コードを送信したのが "誰か" という情報は一切確認しない点にご注意ください。

認可コードの横取り攻撃が発生する条件

また、認可コードの横取り攻撃はすべてのケースで発生するものではありません。下記の前提となる条件を満たす場合に発生する可能性があります。

  1. 攻撃する人がクライアントの端末に悪意のあるアプリケーションを登録し、他のアプリケーションで使用されているカスタムURIスキームを登録できている。
  1. OAuth 2.0 の認可コードフローを使っている。

  2. 攻撃する人が正当なクライアントの client_id と client_secret を知ることができる、または正当なクライアントがパブリッククライアントで、 攻撃する人が client_id を知ることができる。

  3. 攻撃する人が登録した悪意あるアプリケーションは認可リクエストのレスポンスを見る(知る)ことができる。

    また、認可リクエストのリクエストログを見る(知る)ことができる場合は、code challenge method が plane の場合はリスクを軽減できないことに注意が必要です。code challenge method として S256 の採用を検討が必要です。

用語

PKCE を理解するために下記の用語を確認してください。

名前 用途
code verifier 認可リクエストごとに動的に生成するランダムな文字列。認可リクエストとトークンリクエストを関連づけるために使用
code challenge code_verifierをcode challenge methodで変換した値。認可リクエストに含まれ、後にトークンリクエスト時に検証に使用。
code challenge method code challengeを生成するために使用された方法。plain(=平文)かS256(=sha256)のどちらか。RFC 7636 でもある通り、使用できる場合はS256を使用しなければならず、なんらかの技術的な理由でS256がサポートされていないことを知っている場合に限りplaneの使用が許可される。

PKCEの処理の流れ

 PKCE のふるまいについて、簡易なシーケンスで表現すると下記のようになります。
  1. クライアントは code verifier と呼ばれるランダムな文字列を認可リクエストごとに作成します。

  2. code verifier を code challenge method で変換して、code challenge を準備しておきます。

  3. 認可リクエストには code challenge と code challenge method を含めて認可サーバーに送信します。

  4. 認可サーバーは code challenge と code challenge method を認可コードに紐付けて任意の場所に保存し、認可コードを返却します。

  5. クライアントはトークンリクエスト時に、認可コードと共に code verifier を送信します。

  6. 認可サーバーはリクエストできた code verifier を認可コードに紐づけて保存していた、code challenge method で変換して、その値が code challenge と一致するかを確認します。ここで一致しない場合は認可リクエストを送信したクライアントと、アクセストークンを送信したクラアントが別だと判断してエラーを返します。

では PKCE の流れをイメージできたところで、元の処理に戻りましょう。

6.4.3 クライアントをPKCEに対応させる設定を行う

PKCE に対応した認可リクエストを送信してみます。 しかし Keycloak のクライアントはデフォルトでは PKCE が有効ではありませんのでまずは設定から行っていきましょう。

Keycloak の管理画面の左側のメニュー項目 Client の移動し、作成した test-clientを押下してクライアントの設定を開きます。

Setting タブが開かれている状態で、そのまま下にドラッグして、Advanced Settings を開きます。

Proof Key for Code Exchange Code Challenge Method という項目を確認してください。この値はデフォルトでは画像のように空白になっています。

このフィールドは下記のような3種類の値を指定できますので、S256 を選択して Save を押下してください。

意味
(blank) クライアントがPKCEのパラメーターをKeycloakの認可エンドポイントに適切に送信しない限り、KeycloakはPKCEを適用しません。これがデフォルト設定です。
S256 コード・チャレンジ・メソッドをS256としてPKCEを適用します
plain コード・チャレンジ・メソッドをplainとしてPKCEを適用します

6.4.4 PKCEに対応した認可リクエストを送る準備

では元の処理に戻り、PKCE に対応した認可リクエストを送信する準備をしましょう。 まずは任意の code verifier を作成します。

RFC7636によると code verifier は下記の条件を満たすものです。

  • 長さは43~128文字
  • 使用可能文字は、[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"

また、RFC7636には下記のような補足情報も記載されています。

NOTE: The code verifier SHOULD have enough entropy to make it
impractical to guess the value. It is RECOMMENDED that the output of
a suitable random number generator be used to create a 32-octet
sequence. The octet sequence is then base64url-encoded to produce a
43-octet URL safe string to use as the code verifier.

今回はこれに従って、適切な乱数発生器の出力を使用して、ランダムな32文字の文字列を作成し、base64URL でエンコードを行い code verifier を作成します。

今回は下記のコマンドで作成します。

$ openssl rand -base64 32 | tr '/+' '_-' | tr -d '='
09CgSwrVcqat4ZE6JXzWkIW9Ox61aX8rDY_oWcBWgkA

では次に code challenge を作成します。code challenge を作成するための code challenge method は FAPI1 Baseline の要求事項として S256 を指定に従い sha256 とします。RFC 7636どおりに sha256 でハッシュ化した後、base64URL でエンコードを行います。

code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

コマンドで行う例を提示すると下記のようになります。

$ echo -n '09CgSwrVcqat4ZE6JXzWkIW9Ox61aX8rDY_oWcBWgkA' | openssl dgst -sha256 -binary | openssl base64 | tr '/+' '_-' | tr -d '='

x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE

これで code verifier, code challenge, code challenge method の各値が揃いました。今回は下記の値を用いて検証していきます。

名前
code verifier 09CgSwrVcqat4ZE6JXzWkIW9Ox61aX8rDY_oWcBWgkA
code challenge x5TzY7F73pwupN2MmxV_p65paRc7vJrN7b1cRL2CIGE
code challenge method S256

6.4.5 PKCEに対応した認可リクエストを送る

では PKCE に対応した認可リクエストを送ってみましょう。認可リクエストのパラメータに code_challenge と、 code_challenge_method を追加してみましょう。例示した Docker Compose の設定を使っている場合は下記のようなリクエストになります。

6.4.6 リダイレクトページへ移動

認可は既に 6.1.3 の時点で許可されており、ユーザーの認証についてもセッションが残っているので、リダイレクトした先へ移動します。

  • https://client.example.com/test?state=abcdefghijk&session_state=b53f3214-c5bf-4a8b-b12c-c29b51f7d685&code=a5f2b9cf-e9d3-41de-9f9f-93a15fde68a4.b53f3214-c5bf-4a8b-b12c-c29b51f7d685.3bd898d3-bb26-48fc-af88-44c6e2bcde57

6.4.7 トークンエンドポイントに認可コードを提示してアクセストークンの取得

では取得した認可コードをパラメーターに含み、下記のような CURL コマンドを叩いてアクセストークンを取得してみましょう。

curl -i -X POST \
   -H "Content-Type:application/x-www-form-urlencoded" \
   -d "client_id=test-client" \
   -d "grant_type=authorization_code" \
   -d "code=a5f2b9cf-e9d3-41de-9f9f-93a15fde68a4.b53f3214-c5bf-4a8b-b12c-c29b51f7d685.3bd898d3-bb26-48fc-af88-44c6e2bcde57" \
   -d "redirect_uri=https://client.example.com/test" \
 'http://localhost:8088/auth/realms/sample-realm/protocol/openid-connect/token'

レスポンスを見ると下記のようなエラー(下記は一部省略)が返ってきます。

HTTP/1.1 400 Bad Request {"error":"invalid_grant","error_description":"PKCE code verifier not specified"}

これは PKCE の code verifier が含まれていないことを示しています。

フィールド名
error invalid_grant
error_description PKCE code verifier not specified

では code verifier を含んで再送信する前に、再度認可エンドポイントにリクエストを送りリダイレクトした先から認可コードを取得してください

今回も認可は既に 6.1.3 の時点で許可されており、ユーザーの認証についてもセッションが残っているので、下記のようにリダイレクトした先へ移動します。

  • https://client.example.com/test?state=abcdefghijk&session_state=b53f3214-c5bf-4a8b-b12c-c29b51f7d685&code=48980fbe-e45e-4332-873a-e9da28f36787.b53f3214-c5bf-4a8b-b12c-c29b51f7d685.3bd898d3-bb26-48fc-af88-44c6e2bcde57

では取得した認可コードと、PKCE の code verifier を含んで下記のような CURL コマンドを叩いてアクセストークンを取得してみましょう。

curl -i -X POST \
   -H "Content-Type:application/x-www-form-urlencoded" \
   -d "client_id=test-client" \
   -d "grant_type=authorization_code" \
   -d "code=48980fbe-e45e-4332-873a-e9da28f36787.b53f3214-c5bf-4a8b-b12c-c29b51f7d685.3bd898d3-bb26-48fc-af88-44c6e2bcde57" \
   -d "code_verifier=09CgSwrVcqat4ZE6JXzWkIW9Ox61aX8rDY_oWcBWgkA" \
   -d "redirect_uri=https://client.example.com/test" \
 'http://localhost:8088/auth/realms/sample-realm/protocol/openid-connect/token'

下記のようなレスポンスが返り、アクセストークンが取得できます。これによりパブリッククライアントで認可コードを用いたケースにおいて、FAPI1 Baseline に準拠したリクエストでアクセストークンを取得できるところまでは確認できました。

{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHSHJ1c1NhU25SMmFrY2x2bG1lTTdXTzdXTUUwZlBveVYtSlpwb1NYT2hZIn0.eyJleHAiOjE2MzY4OTQ0OTksImlhdCI6MTYzNjg5NDE5OSwiYXV0aF90aW1lIjoxNjM2ODkyODI3LCJqdGkiOiI1ODdmZjM4Ny1iNjJiLTQ2ZDAtYWRmYS0xNWMyODBlMDYxYTMiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODgvYXV0aC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjBiNDVlNWQ0LTk2MTEtNGM4NS04NjNmLTAyYjU2NWRmY2IwMiIsInR5cCI6IkJlYXJlciIsImF6cCI6InRlc3QtY2xpZW50Iiwibm9uY2UiOiJhYmNkZWZnaGlqayIsInNlc3Npb25fc3RhdGUiOiJiNTNmMzIxNC1jNWJmLTRhOGItYjEyYy1jMjliNTFmN2Q2ODUiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLXNhbXBsZXJlYWxtIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInNpZCI6ImI1M2YzMjE0LWM1YmYtNGE4Yi1iMTJjLWMyOWI1MWY3ZDY4NSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2wtdGFybyJ9.XCWLiwz9flPUAKJE6qXrGxiNNitsXhbLD-NiGj-AJK2eOkRX1M2lEsha1u4YdxCxxYjWRF0u6vVx1pwejjiUrI2OiVpDGKJlNj22zgLz8H-hCysnsc1Uv4xMipu3dbz8BOhAphHoBKQgpOPHQ-HYw3vi2ypcn2ypgphVKPUcdxc8T0XlqhhuZNAX9xxs9MRvt05AeZEX0RV6V9UajwRDL66O2pmXdS6GNTlBlabRnmjEbw5-Mxeyifo7NNYWiIs810ZLXTp25-n8VRoWK3DZHsLo-XkEdHYNuVstepgbcN-KrSfL-ai6FZpX0bBGa1Y3y1izuwLDd2Ohl18UVtCcxg","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxZDViZDI5OC1hZWQ4LTRmZmItOTc2NC00ZTNkNTdhMTJjNTMifQ.eyJleHAiOjE2MzY4OTU5OTksImlhdCI6MTYzNjg5NDE5OSwianRpIjoiYmM1ZmZiZmUtMzExMy00MjIzLTgxZjAtZDgxYzBkZGE2ZDE1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg4L2F1dGgvcmVhbG1zL3NhbXBsZS1yZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4OC9hdXRoL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJzdWIiOiIwYjQ1ZTVkNC05NjExLTRjODUtODYzZi0wMmI1NjVkZmNiMDIiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoidGVzdC1jbGllbnQiLCJub25jZSI6ImFiY2RlZmdoaWprIiwic2Vzc2lvbl9zdGF0ZSI6ImI1M2YzMjE0LWM1YmYtNGE4Yi1iMTJjLWMyOWI1MWY3ZDY4NSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJiNTNmMzIxNC1jNWJmLTRhOGItYjEyYy1jMjliNTFmN2Q2ODUifQ.X2Nzir5MjspbJLAmsIB2TesDJLAfzsWxmyps1fgHc-Q","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHSHJ1c1NhU25SMmFrY2x2bG1lTTdXTzdXTUUwZlBveVYtSlpwb1NYT2hZIn0.eyJleHAiOjE2MzY4OTQ0OTksImlhdCI6MTYzNjg5NDE5OSwiYXV0aF90aW1lIjoxNjM2ODkyODI3LCJqdGkiOiI4ZDgwZmEwZC1jMmE0LTQ1ZWUtODdkMS1iYWVmMmI2OGNjNDYiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODgvYXV0aC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoidGVzdC1jbGllbnQiLCJzdWIiOiIwYjQ1ZTVkNC05NjExLTRjODUtODYzZi0wMmI1NjVkZmNiMDIiLCJ0eXAiOiJJRCIsImF6cCI6InRlc3QtY2xpZW50Iiwibm9uY2UiOiJhYmNkZWZnaGlqayIsInNlc3Npb25fc3RhdGUiOiJiNTNmMzIxNC1jNWJmLTRhOGItYjEyYy1jMjliNTFmN2Q2ODUiLCJhdF9oYXNoIjoiVGthTjBXbkpydUc2MlAtdnZkLVgxdyIsImFjciI6IjAiLCJzaWQiOiJiNTNmMzIxNC1jNWJmLTRhOGItYjEyYy1jMjliNTFmN2Q2ODUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImNsLXRhcm8ifQ.Fbz_fhGRutbJRqKSOHq1GptKdTPOMW5DmG-vVxvlrByrOcxMABN6CAd9s9fuC_IuKkqZyFY0IbUZdBapTq7XANvmtI9BbolfGi43Ws4OwHZrGAm0meMfQScBricBRxMSZmVGwCQBWjLrIcCCUSRTVn22OCjrlqj6gXRwe6lhXwbeRoWdfbr27Qc-0RQJI62QdhS6ZGYcD20_S7c7PimHATyr80cQVIkG2hn-Ej9jmYn-xTHBjdnPWDDioC1Eqedi6UBuwpfKOEQ60CzEn5bbzuovekR-RS2_WeS5lqxkTFDQcglZrrcjLdE_PLeNKyminaqKTbWpw6h0dxoX2O4iLA","not-before-policy":0,"session_state":"b53f3214-c5bf-4a8b-b12c-c29b51f7d685","scope":"openid profile email"}

6.5. クライアントポリシーで FAPI1 Advanced の適用

次にクライアントポリシーを変更してみます。 さきほどまでは FAPI1 Baseline だったので次は FAPI1 Advanced を設定してみましょう。

6.2. クライアントポリシーで FAPI1 Baseline の適用 で作成した、test-policy の設定画面に移動します。例示した Docker Compose の設定を使っている場合は下記になります。

 FAPI1 Baseline を追加した時と同じように  Add client profile... という表示を押すと、適用する Profile の一覧が表示されるので、今回は fapi-1-advanced を選択してください。
追加後の画面は下記のような画面になります。

6.6 "6.4" と同じ条件でアクセストークンを取得してみる

次に FAPI1 Baseline に準拠した方法で、FAPI1 Advanced が適用されているクライアントに同じ手順で試してみます。では PKCE に対応した認可リクエストを送ってみましょう。例示した Docker Compose の設定を使っている場合は下記のようなリクエストになります。

今回は下記のようなエラーを示す情報を含んで Redirect URI へ登録したページにリダイレクトしてきているはずです。

  • 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

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

実は FAPI1 Advanced の5.1. Introductionにこのプロファイルはパブリッククライアントをサポートしていないことが書かれています。

This profile does not support public clients.

その他に認可サーバーに対する要求事項の 5.2.2. Authorization serverを見ると、FAPI1 Advanced に対応している認可サーバーはパブリッククライアントをサポートしてはいけないという記述もあります。

shall not support public clients;

ここまでで、FAPI1 Advanced に準拠する場合、 public クライアントは使えないということがわかりました。ここから Confidential なクライアントも試してみたい所なのですが、この記事は一旦ここまでとさせていただきます。

7. 後書き

この記事では Keycloak のクライアントポリシーと、FAPI を簡単にふれていく過程で PKCE も試してみました。この記事を読むことで、Keycloak のクライアントポリシーやそれによって FAPI に準拠していく過程のイメージや、PKCE でのリクエストのやりとりを掴むことに対して少しでも参考になれば幸いです。

CL LAB Mail Magazine

CL LABの情報を逃さずチェックしよう!

メールアドレスを登録すると記事が投稿されるとメールで通知します。

メールアドレス: 登録

※登録後メールに記載しているリンクをクリックして認証してください。

Related post

DXウェビナーシリーズ第1弾_GitLab編