【Keycloak】 FAPI Baseline 対応のクライアントを Go 言語で実装する
はじめに
過去4回に渡り、Keycloak と Go 言語で Authorization Code Flow を学びました。今回は、Keycloak で Financial-grade API (FAPI) Baseline に対応するためのクライアントを Go 言語で実装します。
- KeycloakとGo言語でAuthorization Code Flowを学ぶ
- KeycloakとGo言語でAuthorization Code Flowを学ぶ 2
- KeycloakとGo言語でAuthorization Code Flowを学ぶ 3
- KeycloakとGo言語でAuthorization Code Flowを学ぶ 3
環境
バージョン | |
---|---|
Mac | Sonoma 14.5 |
Keycloak | 24.0.3 |
Docker | 26.0.0 |
Docker Compose | v2.24.5 |
Terraform | 1.8.4 |
成果物
https://github.com/kntks/blog-code/tree/main/2024/07/keycloak-fapi-baseline
Client Authentication
トークンエンドポイントを使用してアクセストークンや ID トークンを取得したい場合、先にクライアント認証をする必要があります。Keycloak で Confidential クライアントタイプを使用したクライアント認証方法と OIDC の Client Authentication 以下のように対応しています。
OIDC | Keycloak |
---|---|
client_secret_basic | Client Id and Secret |
client_secret_post | Client Id and Secret |
private_key_jwt | Signed Jwt |
client_secret_jwt | Signed Jwt using Client Secret |
参考:
- 9. Client Authentication - OpenID Connect Core 1.0 incorporating errata set 1
- Confidential client credentials - Keycloak Documentation
環境準備
Keycloakのセットアップ
Keycloak を docker compose で起動します。
Terraform を使用するために Keycloak側のセットアップ が必要です。
terraform apply で Keycloak の設定を行います。
Go言語のコードは以前書いた、KeycloakとGo言語でAuthorization Code Flowを学ぶ 3 で使用したコードをベースとして利用します。
https://github.com/kntks/blog-code/tree/main/2024/04/keycloak-authorization-code-flow-3
APIサーバー起動してから、ブラウザで http://localhost:8081/login
にアクセスします。
Keycloak のログイン画面が表示されるので、username と password に myapp
を入力してログインできます。
クライアントポリシー
Keycloak は、クライアントが Financial-grade API (FAPI) をサポートするための設定を提供しています。
クライアントが FAPI に対応していることを示すために、Keycloak ではクライアントポリシーを使用します。 クライアントポリシーはクライアント・アプリケーションをセキュアにするための設定で、主に以下のことができます。
- クライアントがどのような構成を持つことができるかについてのポリシーを設定する
- クライアント設定の検証
- 金融グレードAPI(FAPI)やOAuth 2.1など、要求されるセキュリティ標準やプロファイルへの準拠
参考:
- 2.7. Financial-grade API (FAPI) Support - Keycloak Documentation
- Client Policies - Keycloak Documentation
クライアントポリシーの設定
Realm settings
-> Client policies
-> Policies
から Client client policy
を選択することで、クライアントポリシーを設定できます。
デフォルトではクライアントポリシーが存在しないため、実際にクライアントポリシーを作成してみます。名前は fapi
にします。
Save
ボタンを押した後、Conditions と Client profiles が表示されます。
Conditions
Condintions は、ポリシーがどのクライアントにいつ採用されるかを決定できます。今回は any-client
に設定することで、すべてのクライアントに対してポリシーの適用をしますが、その他にも以下のような Condition があります。
- client-access-type
- client-roles
- client-scopes
- client-updater-context
- client-updater-source-groups
- client-updater-source-host
- client-updater-source-roles
詳細は Condition を参照してください。
Client profiles
以下の画像で表示されているクライアントプロファイルは、Keycloak で提供されているデフォルトのプロファイルです。クライアントポリシーを作成する際に、これらのプロファイルを選択することで、FAPI や OAuth 2.1 のような標準的なセキュリティプロファイルに準拠するように設定できます。
クライアントポリシーの適用後
クライアントポリシーを作成後以下のようになっているはずです。
1つ前のセクションでAPIサーバー起動してから、ブラウザで http://localhost:8081/login
にアクセスしました。
しかし、クライアントポリシー適用後に再度ログインを試みると、URLが以下のようになり、ログイン画面が表示されなくなりました。
FAPI のポリシーが適用されているようです。ここからリクエストを成功させるために、どのような設定が必要なのか調査し、API サーバーの実装を変更します。
クライアントが対応すべき要件
Go 言語で作成した API サーバーは confidential client です。そのため 5.2.4. Confidential client から要件を確認します。するとこのセクションの1行目に「In addition to the provisions for a public client」と記載されていることから、public client に対する要件も同時に確認する必要があることがわかります。
7.1. TLS and DNSSEC considerationss では、すべてのやり取りはTLS(HTTPS)で暗号化されなければならない。
と記載されています。さらに 5.2.2. Authorization server でもリダイレクト URI に対して https の使用が要求されています。
public client に対する要件
- RFC7636 (PKCE) のサポート
- RFC7636 (PKCE) にあるコードチャレンジ方式として
S256
を用いたものを使用すること - 対話する各認証サーバーに対して、別個のリダイレクト URI を使用すること
- リソースオーナーのユーザーエージェント(ブラウザなど)のセッションにリダイレクト URI 値を保存し、認可応答を受け取ったリダイレクト URI と比較しなければならない。URI が一致しない場合、クライアントはエラーで処理を終了しなければならない
- 効果的な CSRF 防御を実装しなければならない
public clientが ID トークンを要求する場合
- scope に openid を含める
- 認証リクエストに、OIDCのセクション3.1.2.1 で定義されている nonce パラメータを含める。
scope に openid
が存在しない場合(つまり ID トークンを要求しない場合かな?)
- RFC6749のセクション4.1.1 で定義されている state パラメータを含める
- トークンレスポンスで取得した scope が完全に一致するか、または認可リクエストで送信されたスコープのサブセットを含むことを検証しなければならない。
- OIDF または RFC8414 に定義される well known endpoint で認可サーバーによって公開されたメタデータドキュメントから取得された認可サーバーメタデータのみを使用すること
参考:5.2.3. Public client - Financial-grade API Security Profile 1.0 - Part 1: Baseline
confidential client に対する要件
- トークン・エンドポイントに対して認証するために以下の2つ両方をサポートしなければならない
- MTLSのセクション2 に規定されている OAuth クライアント認証のための相互 TLS(mTLS)
- OIDCのセクション9 に規定されている
client_secret_jwt
またはprivate_key_jwt
- RSA 暗号を使用する場合、最低 2048 ビットの RSA 鍵を使用しなければならない
- 楕円曲線暗号を使用する場合、最低160ビットの楕円曲線鍵を使用しなければならない
- 共通鍵暗号を使用する場合、クライアントシークレットが最低 128 ビットであることを検証しなければならない
今までのブログで書いてきたコードは、client_secret_post
を使用していましたが、これを client_sercret_jwt
または private_key_jwt
に変更します。
参考:
- 9. Client Authentication - OpenID Connect Core
- 5.2.4. Confidential client - Financial-grade API Security Profile 1.0 - Part 1: Baseline
FAPI に準拠するためのコード修正
クライアントの設定を変更します。そのときクライアントポリシーによって設定の変更ができないため、Client policies で作成した fapi ポリシーを disable にしておきます。
PKCE を使用する
PKCE については以前のブログで書いたので、コードはすでに実装があります。
Proof Key for Code Exchange (PKCE)
https://github.com/kntks/blog-code/blob/main/2024/07/keycloak-fapi-baseline/base/main.go#L64-L79
nonce を必須にする
今回使用するコードではすでに nonce を設定しているため、この条件を満たしています。
https://github.com/kntks/blog-code/blob/main/2024/07/keycloak-fapi-baseline/base/main.go#L77
API サーバーを HTTPS にする
以下のコマンドを実行して証明書を作成します。
key.pem と cert.pem が作成されました。
https://localhost:8443/login
にアクセスするために、API サーバーを HTTPS に変更します。
https://github.com/kntks/blog-code/pull/31/commits/fba2ebd1dbd241b4c2ca562941b6141f332da8a8
参考:https://pkg.go.dev/net/http#ListenAndServeTLS
その後、redirect_uri を https://localhost:8443/callback
に変更します。
以下のコマンドで API サーバーを起動します。
先ほど Web ブラウザで https://localhost:8443/login
にアクセスすると Keycloak のログイン画面が表示されませんでしたが、表示されるようになります。しかし、ログイン後に token is empty
となり、まだエラーが残っています。Keycloak のログには、WARN [org.keycloak.events] (executor-thread-33) type="CODE_TO_TOKEN_ERROR", realmId="9bfe15a2-1b59-46de-a41d-f756e4ddb616", clientId="myapp", userId="null", ipAddress="172.29.0.1", error="invalid_code", grant_type="authorization_code", client_auth_method="client-secret"
が表示されます。
このエラーは、クライアント認証で client_secret_jwt または private_key_jwt を使用すると解消できます。
※ SameSite=Strict
だと Cookie が送信されないため、コード内に存在する SameSite
の設定を SameSite=Lax
に変更しています。
リダイレクトURIを比較する
public client に対する要件の中に、“リソースオーナーのユーザーエージェント(ブラウザなど)のセッションにリダイレクト URI 値を保存し、認可応答を受け取ったリダイレクト URI と比較しなければならない” とありました。
今回は、Web ブラウザの Cookie にリダイレクト URI を保存し、認可応答を受け取ったリダイレクト URI と比較します。
https://github.com/kntks/blog-code/pull/31/commits/9d084051b792c98308356350ff7cf720e888b988
client_secret_jwt または private_key_jwt を使用する
ベースとなるコードでは、client_secret_basic
または、client_secret_post
を使用してクライアント認証を行っていました。しかし、FAPI に準拠するためには、その認証方法を client_secret_jwt
または private_key_jwt
に変更する必要があります。
client_secret_basic
または、client_secret_post
を使用している箇所
client_secret_jwt に変更する
client_secret_jwt
は client_secret_post
や client_secret_basic
と異なり、クライアントシークレットを直接送信するのではなく、JWT の署名に使用します。クライアントシークレットを共通鍵として使用し、JWT に署名を行います。これにより、認可サーバー(Keycloak)に直接クライアントシークレットを送信する必要がなくなります。
They can be used to demonstrate knowledge of some secret, such as a client secret, without actually communicating the secret directly in the transaction.
これらは、クライアントシークレットのような何らかの秘密の知識を、取引の中で直接その秘密を通信することなく示すために使用できます。
引用:RFC7521
JWT の Claim に入れる内容は OpenID Connect の仕様にある Client Authentication に説明があります。
項目 | 必須 or 任意 | 説明 |
---|---|---|
iss | 必須 | OAuth クライアントの client_id にする |
sub | 必須 | OAuth クライアントの client_id にする |
aud | 必須 | 認証サーバーのトークンエンドポイントの URL にする |
jti | 必須 | トークンの一意な識別子。トークンの再利用を防ぐために使用できる |
exp | 必須 | JWTを処理に受け入れてはならない有効期限 |
iat | 任意 | JWTが発行された時刻 |
トークンエンドポイントへのリクエストについて、RFC7521 のセクション 4.2 によると、 client_assertion
と client_assertion_type
をリクエストデータに追加するだけです。
ここからは実際に Keycloak の管理画面で設定を変更します。
Clients > Credentials > Client Authenticator で設定されている Client Authenticator
を Client Id and Secret
から Signed jwt with Client Secret
に変更します。
Save
ボタンを押すと、以下のようなダイアログが表示されるので、Yes
を選択します。
リダイレクト URI を比較したときに作成したコードを使用します。
client_secret_jwt
を使用するためには、JWT を署名する必要があります。Go 言語で JWT を署名するためには、以下のようにします。
https://github.com/kntks/blog-code/pull/31/commits/56881892828245206522fa8be3d822b6a8ee97a2
実際に API サーバーを起動して、ブラウザで https://localhost:8443/login
にアクセスします。 Keycloak のログイン画面が表示され、ユーザー名、パスワードともに myuser
でログイン後 home
画面にリダイレクトされることを確認できます。
private_key_jwt に変更する
client_secret_jwt
認証方式では、クライアントシークレットを使用して JWT を署名しました。次は private_key_jwt
認証方式を試してみます。この方式では、client secret の代わりに非対称暗号を使用します。具体的には、クライアントが秘密鍵を使用して JWT に署名し、認可サーバーは対応する公開鍵を使用してその署名を検証します。
暗号鍵は confidential client に対する要件 に記載されているように、最低 2048 ビットの RSA 鍵または最低 160 ビットの楕円曲線鍵を使用する必要があります。今回はRSA 鍵を使用します。
JWT の Claim に入れる内容は OpenID Connect の仕様にある Client Authentication に説明があります。
項目 | 必須 or 任意 | 説明 |
---|---|---|
iss | 必須 | OAuth クライアントの client_id にする |
sub | 必須 | OAuth クライアントの client_id にする |
aud | 必須 | 認証サーバーのトークンエンドポイントの URL にする |
jti | 必須 | トークンの一意な識別子。トークンの再利用を防ぐために使用できる |
exp | 必須 | JWTを処理に受け入れてはならない有効期限 |
iat | 任意 | JWTが発行された時刻 |
リダイレクト URI を比較したときに作成したコードを使用します。
ここからは実際に Keycloak の管理画面で設定を変更します。
Clients > Credentials > Client Authenticator で設定されている Client Authenticator
を Client Id and Secret
から Signed jwt
に変更します。
次に署名を行うための鍵を作成し、Keycloak に公開鍵を登録します。
この方法には2通りあります。
- 自前で秘密鍵、公開鍵を作成し、Keycloak の管理画面で公開鍵を設定する
- Keycloak の管理画面で秘密鍵、公開鍵を作成し、秘密鍵をローカルに保存する
1つ目の方法は、こちらの記事で紹介されているため、このブログでは2つ目の方法を実施します。
管理画面左のナビゲーションペインから Clients をクリックし myapp
の詳細画面から Keys
タブがをクリックします。
Generate new keys
ボタンを押した後、Archive Format
で PKCS12
を選択し、残りはすべて myapp
に設定します。 その後 Generate
ボタンをクリックします。
openssl
コマンドを使用して、keystore.p12
から秘密鍵を取り出します。
Go言語にはファイルの中身を埋め込む機能があります。これを使用して秘密鍵を埋め込みます。
https://github.com/kntks/blog-code/pull/31/commits/611dd8e347f7a6ce15f7764892742b038b5c5911
実際に API サーバーを起動して、ブラウザで https://localhost:8443/login
にアクセスします。 Keycloak のログイン画面が表示され、ユーザー名、パスワードともに myuser
でログイン後 home
画面にリダイレクトされることを確認できます。
参考:
- Confidential client credentials - Keycloak Documentation
- 9. Client Authentication - OpenID Connect Core 1.0
- 4.2. Using Assertions for Client Authentication - RFC7521
- client_secret_jwt によるクライアント認証 - authlete
さいごに
今回の記事では、Financial-grade API Security Profile 1.0 - Part 1: Baseline の資料を参考にしながら、Keycloak で FAPI に準拠するための設定を行いました。まだ FAPI Advanced については触れていませんが、今後の課題として取り組んでいきたいと思います。
メモ
JWT-Secured Authorization Request(JAR)
JARが解決する課題
どのような課題を解決するために提案された仕様なのか RFC9101 の冒頭部分を読むと以下のことがわかります。
認可サーバーへの認可リクエストURLは、以下のようにシリアライズされたクエリパラメータを含むことができます。
引用: 13.1. Query String Serialization - OpenID Connect Core 1.0 incorporating errata set 1
しかし、以下のような問題があります。
- ユーザーエージェントを介した通信は完全性が保護されていないため、パラメータが汚染される可能性がある(完全性保護の失敗)
- 通信元が認証されていない(送信元認証の失敗)
- ユーザーエージェントを介した通信が監視される可能性がある