Keycloak と curl を使って Device Authorization Grant (ID Token の取得) をやってみる
前回の記事で Authorization Code Grant を使った ID Token の取得 (可能な限り curl を使って取得) をやってみたのですが、今回は Device Authorization Grant を使った ID Token の取得を試してみました。
環境
今回は、以下のような環境 (minikube で作った Kubernetes 上に Keycloak をデプロイ) で検証しています。
バージョン
Kubernetes : v1.35.0 minikube : v1.38.0 Docker : v29.2.1 Keycloak : v26.5.2 Ubuntu (WSL2 / Windows 11) : 24.04.3 LTS Firefox : 147.0.3 (64 bit)
環境構成概要図
+---[Kubernetes (minikube)]-------------------------------+
| |
| +---[Keycloak (Pods)]-----------------------------+ |
| | | |
| | +---[Realm: device-authz-grant-realm]-----+ | |
| | | | | |
[1. curl (Act as Device Client)]---+-->| | [Client: device-authz-grant-client] | | |
| | | | | |
[2. Firefox (Act as End-User)]-----+-->| | [User: foo] | | |
| | | | | |
| | +-----------------------------------------+ | |
| | | |
| +-------------------------------------------------+ |
| |
+---------------------------------------------------------+
検証
ということで、さっそく検証してみます。
Keycloak をデプロイ
まずは環境構築です。前回の記事では Keycloak をデプロイした後 GUI でポチポチ設定をしていたのですが、GUI で設定しながらブログ用に全部の画面のスクリーンショットをとるのがめんどくさいので 今回はなんかいい感じに CLI で シュッ と環境を作れるように keycloak-samples というリポジトリを用意しました。
ということで、まずはこのリポジトリを clone して Keycloak をデプロイします。
$ git clone https://github.com/kota2and3kan/keycloak-samples.git
$ kubectl apply -k ./keycloak-samples/
デプロイが完了すると、以下のような感じになります。
$ kubectl get pod NAME READY STATUS RESTARTS AGE keycloak-0 1/1 Running 0 48s postgres-54d858b658-z4s5g 1/1 Running 0 48s
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE keycloak LoadBalancer 10.100.242.123 <pending> 8888:32258/TCP 71s keycloak-discovery ClusterIP None <none> <none> 71s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 54m postgres ClusterIP 10.101.181.60 <none> 5432/TCP 71s
minikube tunnel を実行
別のターミナルで minikube tunnel コマンドを実行し、Keycloak に localhost (127.0.0.1) 経由でアクセスできるようにします。
$ minikube tunnel ✅ Tunnel successfully started 📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ... 🔗 Starting tunnel for service keycloak.
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE keycloak LoadBalancer 10.100.242.123 127.0.0.1 8888:32258/TCP 96s keycloak-discovery ClusterIP None <none> <none> 96s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 54m postgres ClusterIP 10.101.181.60 <none> 5432/TCP 96s
Keycloak でいろいろ設定
Keycloak のデプロイが完了したら、Keycloak 側でいろいろと設定をします。いい感じに設定をするためのスクリプトを用意したので、Keycloak のコンテナの中でスクリプトを実行します。スクリプトの中身や設定内容が気になる方はこのへんを参照してみてください。
$ kubectl exec -it keycloak-0 -- /samples/device-authorization-grant/setup-keycloak.sh
このスクリプトでは、以下の 3つを作成しています。
Realm : device-authz-grant-realm Client : device-authz-grant-client User : foo
また、後で必要になる Client Secret を出力してくれるので、この値 (=== Show client secret === の場所に出力されている value の値) をメモしておきます。今回の場合は jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9 です。
$ kubectl exec -it keycloak-0 -- /samples/device-authorization-grant/setup-keycloak.sh === Log in to Keycloak as admin === Logging into http://localhost:8080 as user admin of realm master === Create realm === Created new realm with id 'device-authz-grant-realm' { "id" : "d0ecb354-8230-4cd2-b283-e80a64a87b23", "realm" : "device-authz-grant-realm" } === Create user === Created new user with id '218249df-6639-444d-9330-fb609e829c64' { "id" : "218249df-6639-444d-9330-fb609e829c64", "username" : "foo" } === Set user password === Set password for user 'foo' to 'foofoo' === Create client === Created new client with id '4e4e75db-8861-4cfe-861a-cf7a0806e5c0' { "id" : "4e4e75db-8861-4cfe-861a-cf7a0806e5c0", "clientId" : "device-authz-grant-client" } === Show client secret === Client secret for client 'device-authz-grant-client' is: { "type" : "secret", "value" : "jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9" } === Keycloak setup completed ===
これで Keycloak 側での設定は終わりです。
Code Verifier と Code Challenge の準備
実際の検証の前に、もう一つだけ準備をしておきます。詳細は割愛しますが、Device Authorization Grant の中で PKCE (Proof Key for Code Exchange) というセキュリティを向上させるための仕組みが使われているので、その PKCE の処理に必要な Code Verifier と Code Challenge を事前に準備しておきます。
後述する検証では Appendix B. Example for the S256 code_challenge_method に記載されている以下のサンプルを使います。
Code Verifier : dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk Code Challenge : E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
上記以外の Code Verifier / Code Challenge を使って検証したい場合は、Code Challenge を生成するツールを作ってみたので、そちらを参照してみてください。
Device Authorization Grant をやってみる
さて、諸々の準備が完了したので、いよいよ Device Authorization Grant を使った ID Token の取得をやってみます。
一部詳細は省略していますが、Device Authorization Grant を使ったログイン処理は、概ね以下のような感じの流れになります。
- Device Client が OpenID Provider (今回は Keycloak) に Device Authorization Request を送る。
$ example-command login的な CLI を実行するイメージ。 - OpenID Provider は Device Client に Device Code / User Code / Verification URI を返す。
Device Client は End-User に Verification URI (OpenID Provider にアクセスして認証するための URL) と User Code (認証時に入力する一時的なコード) を何等かの形で提供する。CLI の場合、標準出力に Verification URI と User Code を出力するイメージ。RFC 8628 には以下のような例が記載されている。
+-----------------------------------------------+ | | | Using a browser on another device, visit: | | https://example.com/device | | | | And enter the code: | | WDJB-MJHT | | | +-----------------------------------------------+Device Client は OpenID Provider に対して Polling を実施しながら、End-User 側で認証が実施されるのを待つ。
- End-User は Device Client から提示された URL (OpenID Provider) に何等かデバイス上のブラウザでアクセスする。
- End-User は OpenID Provider のログイン画面で認証 (ログイン) し、User Code を入力する。
- Device Client は引き続き OpenID Provider に対して Polling を実施している。
- OpenID Provider での End-User の認証が完了している場合、OpenID Provider は Device Client に対して ID Token (+ いろいろ) を返す。
- Device Client は取得した ID Token の情報を基にログイン処理を実施する。
+---------------+ +-----------------+ +--------------------+
| Device Client | | OpenID Provider | | End-User (Browser) |
+---------------+ +-----------------+ +--------------------+
| | |
+----(1. Start login flow)------------------------------------->+ |
| | |
+<---(2. Return Device Code, User Code, and Verification URI)---+ |
| | |
+----(3. Provide User Code and Verification URI)----------------+------------------------------------------------------->+
| | |
+----(4. Polling authentication result)------------------------>+ |
| | |
+----(4. Polling authentication result)------------------------>+ |
| | |
+----(4. Polling authentication result)------------------------>+ |
| | |
| +<---(5. Access OpenID Provider from secondary device)---+
| | |
| +----+ |
| | | (6. Authentication / Enter User Code) |
| +<---+ |
| | |
+----(7. Polling authentication result)------------------------>+ |
| | |
+<---(8. Return ID Token)---------------------------------------+ |
| | |
+----+ | |
| | (9. Log in Device Client) | |
+<---+ | |
| | |
+---------------+ +-----------------+ +--------------------+
| Device Client | | OpenID Provider | | End-User (Browser) |
+---------------+ +-----------------+ +--------------------+
ということで、各ステップ毎の処理をやってみます。
1. Start login flow
まず、Device Client から OpenIP Provider に Device Authorization Request (RFC 8628 - Section 3.1) を送信します。
curl -s -XPOST \ -d 'client_id=device-authz-grant-client' \ -d 'client_secret=jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9' \ -d 'scope=openid' \ -d 'nonce=foo-device-flow-nonce' \ -d 'code_challenge_method=S256' \ -d 'code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM' \ http://localhost:8888/realms/device-authz-grant-realm/protocol/openid-connect/auth/device
client_id=device-authz-grant-client # Client ID。今回はセットアップ用のスクリプト内で device-authz-grant-client を設定。 client_secret=jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9 # Keycloak をセットアップした際に出力された client secret の値。 scope=openid # ID Token が欲しいので、scope には openid を指定。 nonce=foo-device-flow-nonce # ID Token に含まれる nonce の値。今回は適当な値 (ダミー) を設定。 code_challenge_method=S256 # Code Challenge の生成に使うメソッド。 code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM # 事前に準備した Code Challenge の値。今回は RFC 7636 に載ってるサンプルの値。
2. Return Device Code, User Code, and Verification URI
OpenIP Provider に Device Authorization Request (RFC 8628 - Section 3.1) を送ると、OpenID Provider から Device Authorization Response (RFC 8628 - Section 3.2) が返ってきます。
{ "device_code": "qB8qsTsn0Fzaq2blf6JZDKkPS9l_U84RzTmAOoka8sI", "user_code": "EPQR-NIRI", "verification_uri": "http://localhost:8888/realms/device-authz-grant-realm/device", "verification_uri_complete": "http://localhost:8888/realms/device-authz-grant-realm/device?user_code=EPQR-NIRI", "expires_in": 600, "interval": 5 }
ユーザーは、verification_uri に出力されている URL にアクセスして、user_code に出力されている値を入力することで、認証認可を実施する感じになります。
3. Provide User Code and Verification URI
ここは各 Device Client の実装に依存する部分ですが、何等かの形で verification_uri と user_code がユーザーに伝えられる感じになります。User Interaction (RFC 8628 - Section 3.2) にいくつか例が記載されています。
今回は curl で取得した Device Authorization Response (RFC 8628 - Section 3.2) がそのまま見えているので、その値を利用します。
4. Polling authentication result
verification_uri と user_code が得られたのでさっそく認証したいところですが、検証のために一度 Device Access Token Request (RFC 8628 - Section 3.4) を実行してみます。
curl -s -XPOST \ -d 'client_id=device-authz-grant-client' \ -d 'client_secret=jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9' \ -d 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \ -d 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code' \ -d 'device_code=qB8qsTsn0Fzaq2blf6JZDKkPS9l_U84RzTmAOoka8sI' \ http://localhost:8888/realms/device-authz-grant-realm/protocol/openid-connect/token
client_id=device-authz-grant-client # Client ID。今回はセットアップ用のスクリプト内で device-authz-grant-client を設定。 client_secret=jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9 # Keycloak をセットアップした際に出力された client secret の値。 code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk # 事前に準備した Code Verifier の値。今回は RFC 7636 に載ってるサンプルの値。 grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code # 固定値 "urn:ietf:params:oauth:grant-type:device_code" を (URL encode してから) 指定。 device_code=qB8qsTsn0Fzaq2blf6JZDKkPS9l_U84RzTmAOoka8sI # Step 2. で得られた device_code の値。
すると、まだ認証が完了していないため authorization_pending というエラーが返ってきます。通常、このエラーが返ってきた場合 Device Client は自動的にリトライを実施 (Polling を継続) する動作になると思います。
{ "error": "authorization_pending", "error_description": "The authorization request is still pending" }
また、エラーの種類や詳細については Device Access Token Response (RFC 8628 - Section 3.5) に記載されています。
5. Access OpenID Provider from secondary device
では、実際に Keycloak 側で認証を実施してみます。Step 2. で得られた Device Authorization Response (RFC 8628 - Section 3.2) の verification_uri に記載されている URL にブラウザ (今回は Firefox) を利用してアクセスすると、以下のような画面が表示されます。

6. Authentication / Enter User Code
Keycloak の Device Login ページにて、Step 2. で得られた Device Authorization Response (RFC 8628 - Section 3.2) に含まれる user_code の値を入力します。

user_code を入力すると、Keycloak のログイン画面が表示されるので、事前に設定した (スクリプトで設定した) ユーザーでログイン (認証) します。Username は foo / Password は foofoo です。

ユーザー foo でログインすると、今度は Device Client に対して許可する内容の確認が表示されるので、Yes を選択 (認可) します。

最後に Device Login Successful と表示されれば、ユーザー側での認証認可は完了です。

7. Polling authentication result
ユーザー側での認証認可が完了したので、改めて curl を使って Device Access Token Request (RFC 8628 - Section 3.4) を実行してみます。実行する内容は Step 4. と全く同じです。
curl -s -XPOST \ -d 'client_id=device-authz-grant-client' \ -d 'client_secret=jYgbxXxxVOUkg1Fqdhp76XzsOGyVzPP9' \ -d 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \ -d 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code' \ -d 'device_code=qB8qsTsn0Fzaq2blf6JZDKkPS9l_U84RzTmAOoka8sI' \ http://localhost:8888/realms/device-authz-grant-realm/protocol/openid-connect/token
8. Return ID Token
Device Access Token Request (RFC 8628 - Section 3.4) を実行すると、(Keycloak 側での認証認可が完了しているので) Device Access Token Response (RFC 8628 - Section 3.5) として以下のような JSON が返ってきます。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSMUR4SjBRbUlzX0lrelNuQVFVZzJFdVdnTkl5ZXFBcHlXUDd1LVFYdWVvIn0.eyJleHAiOjE3NzA3OTE1MzAsImlhdCI6MTc3MDc5MTIzMCwiYXV0aF90aW1lIjoxNzcwNzkxMTg0LCJqdGkiOiJvbnJ0ZGc6MDBkYmI0MzctMzJkNi1jODgyLThlNTMtNzEwODNiYjFhNThiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9kZXZpY2UtYXV0aHotZ3JhbnQtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMjE4MjQ5ZGYtNjYzOS00NDRkLTkzMzAtZmI2MDllODI5YzY0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZGV2aWNlLWF1dGh6LWdyYW50LWNsaWVudCIsInNpZCI6IkpzR2R1OE1HTkNmMWg0MDdxcWNYb09CTiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1kZXZpY2UtYXV0aHotZ3JhbnQtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGb28gQmFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZm9vIiwiZ2l2ZW5fbmFtZSI6IkZvbyIsImZhbWlseV9uYW1lIjoiQmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5jb20ifQ.l1rhmWD325NLzffP5TNcdvO0IaTwSqdkgOMZ-A1YyJOQIDgSFCkhYzJczDA-5cV9drlkUALw771RJqNe6wkzGJkFZyuSTTCfLdmfuO9XqAmyWo6bCBpha_aJ2KXlB6-tNhmfPq1RMbXAhqPiFzCinJZ2pYs1mkqzBow99ipjbpCninG9BzqqDsXGL1JhNZR1dNxbsNSpF1yiZBlwwUB_aRLvHdalK0j11uztQfr3jX6tlSqNfmJ1gkPwxlXFLgN43wBdA1Ec7F2N2LBLBXXPxadpgcprYdjzsXZ6Ame_uUhvJEIiZNnYjfzwldzWodSl0EzCoBdqn5SvS_F2n93kTw", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhNGUyZmVhZC1mNGNiLTRlNjQtOWQ5ZS00NjVlMTAwZTE0YjIifQ.eyJleHAiOjE3NzA3OTMwMzAsImlhdCI6MTc3MDc5MTIzMCwianRpIjoiOWQ0NDhlZDMtZWRiZC03MTcwLWIzMjYtN2EzNjdiM2ZhZWVkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9kZXZpY2UtYXV0aHotZ3JhbnQtcmVhbG0iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2RldmljZS1hdXRoei1ncmFudC1yZWFsbSIsInN1YiI6IjIxODI0OWRmLTY2MzktNDQ0ZC05MzMwLWZiNjA5ZTgyOWM2NCIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJkZXZpY2UtYXV0aHotZ3JhbnQtY2xpZW50Iiwic2lkIjoiSnNHZHU4TUdOQ2YxaDQwN3FxY1hvT0JOIiwic2NvcGUiOiJvcGVuaWQgcm9sZXMgcHJvZmlsZSB3ZWItb3JpZ2lucyBhY3IgYmFzaWMgZW1haWwifQ.QUI8ZFJ01Ege9voQ9Hz8OpgjrWVtudU1KdEilw4CAwQPFrJEfxM_p_CMOleuQUMHnSpFV1W5hbmTYxR9ruk6ew", "token_type": "Bearer", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSMUR4SjBRbUlzX0lrelNuQVFVZzJFdVdnTkl5ZXFBcHlXUDd1LVFYdWVvIn0.eyJleHAiOjE3NzA3OTE1MzAsImlhdCI6MTc3MDc5MTIzMCwiYXV0aF90aW1lIjoxNzcwNzkxMTg0LCJqdGkiOiJlMDM0OTk0My05NjFkLWFhNDAtNTVkYS1mMjJhZDA0YzU4ZTIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2RldmljZS1hdXRoei1ncmFudC1yZWFsbSIsImF1ZCI6ImRldmljZS1hdXRoei1ncmFudC1jbGllbnQiLCJzdWIiOiIyMTgyNDlkZi02NjM5LTQ0NGQtOTMzMC1mYjYwOWU4MjljNjQiLCJ0eXAiOiJJRCIsImF6cCI6ImRldmljZS1hdXRoei1ncmFudC1jbGllbnQiLCJub25jZSI6ImZvby1kZXZpY2UtZmxvdy1ub25jZSIsInNpZCI6IkpzR2R1OE1HTkNmMWg0MDdxcWNYb09CTiIsImF0X2hhc2giOiI0ckI0NXFBSEExZS1JcTBucVpmN3RBIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkZvbyBCYXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJmb28iLCJnaXZlbl9uYW1lIjoiRm9vIiwiZmFtaWx5X25hbWUiOiJCYXIiLCJlbWFpbCI6ImZvb0BleGFtcGxlLmNvbSJ9.Q7mClhz-M4PjMvjvtTHUp7wKRcMRFbjU0ZtnpH-1jQPqSP3ihZuRdruQ4u2YVXEEYub2Nxk9KSUz6KbgAxKOrMtsyJzAGtNnihoMsogWmsvOE06ka9-0Gt9JPcaQ-zO4BHgK_CMOB0RNnO3VqA0rbg07hShi9P8Tfd0LS_OlfWPPOXig2Sd7HVscj5U4_ScDeFn9t3TVqfNOW9H0Y697hKyXWhc6M1X2Bh4MICGjp7Z7IRq3lfuYHvhGS0Y0x6GOABNZDGZ38AJupyemR_gGL0Un55bT_iA7_qv4rFkaqQVyCz6d_K9yqmV5vLKYsUa7QZBMcwnLKPQt8nhWQSDkGg", "not-before-policy": 0, "session_state": "JsGdu8MGNCf1h407qqcXoOBN", "scope": "openid profile email" }
id_token の値が Device Client のログインに利用する ID Token です。ID Token の中身をデコードしてみると、以下のような感じになっています。
ヘッダー:
{ "alg": "RS256", "typ": "JWT", "kid": "R1DxJ0QmIs_IkzSnAQUg2EuWgNIyeqApyWP7u-QXueo" }
{ "exp": 1770791530, "iat": 1770791230, "auth_time": 1770791184, "jti": "e0349943-961d-aa40-55da-f22ad04c58e2", "iss": "http://localhost:8888/realms/device-authz-grant-realm", "aud": "device-authz-grant-client", "sub": "218249df-6639-444d-9330-fb609e829c64", "typ": "ID", "azp": "device-authz-grant-client", "nonce": "foo-device-flow-nonce", "sid": "JsGdu8MGNCf1h407qqcXoOBN", "at_hash": "4rB45qAHA1e-Iq0nqZf7tA", "acr": "1", "email_verified": false, "name": "Foo Bar", "preferred_username": "foo", "given_name": "Foo", "family_name": "Bar", "email": "foo@example.com" }
9. Log in Device Client
Step 8. で得られた ID Token を利用して Device Client でのログイン処理が実行されます。この辺は Device Client 毎に実装される内容であり、今回は ID Token の取得が目的なので、特にやることはありません。
ということで、無事 Device Authorization Grant を利用した ID Token の取得が実施できたので、今回の検証はここまでです。
おまけ
一度 ID Token を取得した後に、同じ Device Access Token Request (RFC 8628 - Section 3.4) を実行 (もう一度 ID Token を取得) しようとすると、以下のようなエラーが返ってきます。
{ "error": "invalid_grant", "error_description": "Device code not valid" }
この辺は OpenID Provider 側の実装に依存するのかもしれないですが、同じ Device Code を複数回使いまわすことはできないようなので、もう一度 ID Token を取得したい場合は改めて最初から認証認可の処理を実施する必要がありそうです。
参考情報
- OpenID Connect
- OAuth 2.0
- OAuth 2.0 Device Authorization Grant
まとめ
Keycloak と curl を利用し、Device Authorization Grant による ID Token の取得 (認証認可) の流れをやってみました。
この辺について調べている時に、偶然お仕事で使っているツール (CLI) が「Device Authorization Grant を利用した Google アカウントでのログイン」を要求してきたので、「あ!これ、見たことあるやつだ!」ってなりました。普段なんとなく「Google でログイン」とかを実施していますが、裏の仕組みがわかってくると面白いですね。
今回いろいろ調べているうちに、PostgreSQL 18 で最近サポートされた認証機能 (SASL + OAuth) で、Device Authorization Grant を利用したログインができる (PostgreSQL 用の CLI である psql で Device Authorization Grant を利用したログインができる) っぽい感じの情報を見つけたので、この辺もそのうち検証してみたいと思います (思ってるだけ)。
Keycloak と curl を使って OpenID Connect の Authorization Code Flow (ID Token の取得) をやってみる
以前の記事 (これ とか これ) で「Keycloak (OIDC) を使った認証で CockroachDB にアクセスする方法」を試してみましたが、そもそも OIDC がよくわかってなかったので、Keycloak を使って ID Token を取得する流れを試してみました。
環境
今回は、以下のような環境 (minikube で作った Kubernetes 上に Keycloak をデプロイ) で検証しています。
バージョン
Kubernetes : v1.34.0 minikube : v1.37.0 Docker : v29.1.3 Keycloak : v26.4.7 Ubuntu (WSL2 / Windows 11) : 24.04.3 LTS Firefox : 146.0.1 (64 bit)
環境構成概要図
+---[Kubernetes (minikube)]------------------------+
| |
| +---[Keycloak (Pods)]------------------+ |
[0. Firefox (Initial setting)]-----+-->| | |
| | +---[Realm: foo-realm]---------+ | |
[1. Firefox (Act as End-User)]-----+-->| | | | |
| | | [Client: foo-client] | | |
[2. curl (Act as End-User)]--------+-->| | | | |
| | | [User: foo@example.com] | | |
[3. curl (Act as Relying Party)]---+-->| | | | |
| | +------------------------------+ | |
| | | |
| +--------------------------------------+ |
| |
+--------------------------------------------------+
検証
ということで、さっそく検証してみます。
Keycloak をデプロイ
まずは環境構築です。以前の記事で利用した manifest を流用し、OpenID Provider (OP) として利用する Keycloak をデプロイします。諸事情あって、Keycloak にアクセスするための port 番号はデフォルトの 8080 から 8888 に変更してあります。
$ git clone https://github.com/kota2and3kan/cockroachdb-sso-with-keycloak.git
$ kubectl apply -f ./cockroachdb-sso-with-keycloak/keycloak/keycloak.yaml
デプロイが完了すると、以下のような感じになります。
$ kubectl get pod NAME READY STATUS RESTARTS AGE keycloak-0 1/1 Running 0 25m keycloak-1 1/1 Running 0 24m postgres-589496bddf-8zxcs 1/1 Running 0 25m
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE keycloak LoadBalancer 10.102.101.241 <pending> 8888:30466/TCP 25m keycloak-discovery ClusterIP None <none> <none> 25m kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14h postgres ClusterIP 10.111.51.27 <none> 5432/TCP 25m
minikube tunnel を実行
別のターミナルで minikube tunnel コマンドを実行し、Keycloak に localhost (127.0.0.1) 経由でアクセスできるようにします。
$ minikube tunnel ✅ Tunnel successfully started 📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ... 🏃 Starting tunnel for service keycloak./
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE keycloak LoadBalancer 10.102.101.241 127.0.0.1 8888:30466/TCP 27m keycloak-discovery ClusterIP None <none> <none> 27m kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14h postgres ClusterIP 10.111.51.27 <none> 5432/TCP 27m
Keycloak でいろいろ設定
Keycloak のデプロイが完了したら、Keycloak 側でいろいろと設定をします。
ブラウザから http://localhost:8888/admin にアクセスします。Username / Password は admin / admin です。

ログインしたら、画面左の Manage realms を選択した後、Create realm を押します。

Create realm 画面で Realm name に foo-realm と入力し、Create を選択します。

Realm を作成したら、画面左の Users を選択し、Create new user を押します。

Create user 画面で以下の値入力し、Create ボタンを押します。
Username : foo Email : foo@example.com First name : Foo Last name : Bar

User を作成したら、そのまま Credentials タブを選択し Set password を押します。

Set password 画面で foo ユーザーのパスワードを入力します。また、検証用の一時的なユーザーなので Temporary を Off にした上で、Save を押します。

確認画面が表示されるので、Save password を選択します。

次に、画面左の Clients を選択し、Create client を押します。

Create client 画面で Client type として OpenID Connect を選択した上で、Client ID に foo-client と入力し、Next を押します。

次の Capability config 画面で以下のような設定をして、Next を選択します。
Client authentication : On Authorization : Off Authentication flow : "Standard flow" だけを選択 PKCE Method : S256 Require DPoP bound tokens : Off

最後に、Login settings 画面で以下の値を設定して、Save を押します。今回は ID Token を取得するところまでしか検証しない (実際に何等かのサービス / Relying Party にログインする操作はしない) ので、http://localhost:8888/callback は存在しないページの URL です (アクセスすると Keycloak の 404 画面になります)。
Valid redirect URIs : http://localhost:8888/callback Web origins : http://localhost:8888/

Client の設定が完了したら、そのまま Credentials タブを選択し Client Secret の値をメモしておきます (後の手順で使います)。

これで Keycloak 側での設定は終わりです。
Code Verifier と Code Challenge の準備
実際の検証の前に、もう一つだけ準備をしておきます。詳細は割愛しますが、Authorization Code Flow の中で PKCE (Proof Key for Code Exchange) というセキュリティを向上させるための仕組みが使われているので、その PKCE の処理に必要な Code Verifier と Code Challenge を事前に準備しておきます。
後述する検証では Appendix B. Example for the S256 code_challenge_method に記載されている以下のサンプルを使います。
Code Verifier : dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk Code Challenge : E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
上記以外の Code Verifier / Code Challenge を使って検証したい場合は、Code Challenge を生成するツールを作ってみたので、そちらを参照してみてください。
Authorization Code Flow をやってみる
さて、諸々の準備が完了したので、いよいよ Authorization Code Flow を使った ID Token の取得をやってみます。
一部詳細は省略していますが、OpenID Connect の Authorization Code Flow を使ったログイン処理は、概ね以下のような感じの流れになります。
- End-User が Relying Party (RP) にアクセスし、外部の OpenID Provider (OP) での認証 (今回は Keycloak で認証) を選択。一般的には「Google で認証」「GitHub で認証」のようなボタンを選択するイメージ。
- Relying Party から End-User に対して HTTP Response
302 Foundが返される。この時、Locationヘッダーに OpenID Provider にアクセスするための URL (および必要なパラメーター) が含まれている。 - End-User は
Locationヘッダーに含まれている URL (OpenID Provider) にアクセスする。 - End-User は OpenID Provider にて認証を行う (今回は Keycloak のログイン画面で username / password を使った認証を行う)。
- OpenID Provider での認証が成功すると、HTTP Response
302 Foundが返される。この時、Locationヘッダーに Relying Party (実際にログインしようとしているサービス) に再びアクセスするための URL (および Authorization Code 等の必要なパラメーター) が含まれている。 - End-User は
Locationヘッダーに含まれている URL (Relying Party) にアクセスする。この時、前のステップで OpenID Provider から受け取った Authorization Code を Relying Party に渡す。 - Replying Party は End-User から受け取った Authorization Code を使って OpenID Provider に Token Request を送る。
- OpenID Provider は Relying Party に ID Token (+ Access Token etc) を返す。
- Relying Party は OpenID Provider から受け取った ID Token の検証を実施し、問題がなければログイン処理を実行する。
- Relying Party はログイン処理完了後、End-User にログイン完了を返す。
- 必要な場合、Relying Party は OpenID Provider の UserInfo Endpoint に対して UserInfo Request を送る (ユーザーの情報を取得する) こともできる。
- OpenID Provider は Relying Party に UserInfo Response を返す。
- Access Token の有効期限が切れた場合、Relying Party は Refresh Token を使って新しい Access Token を取得することもできる。
- OpenID Provider は Relying Party に新しい Access Token (+ Refresh Token etc) を返す。
+--------------------+ +---------------+ +-----------------+
| End-User (Browser) | | Relying Party | | OpenID Provider |
+--------------------+ +---------------+ +-----------------+
| | |
+----(1. Start log in flow)------------------------------>+ |
| | |
+<---(2. 302 / Redirect URL and parameters)---------------+ |
| | |
+----(3. Access OpenID Provider)--------------------------+----------------------------------------------------->+----+
| | | | (4. Authentication)
+<---(5. 302 / Redirect URL with Authorization Code)------+------------------------------------------------------+<---+
| | |
+----(6. Access Relying Party with Authorization Code)--->+ |
| | |
| +----(7. Request ID Token)---------------------------->+
| | |
| +<---(8. Return ID Token)------------------------------+
| | |
| +----+ |
| | | (9. Validate ID Token) |
| +<---+ |
| | |
+<----(10. Log in completed)------------------------------+ |
| | |
| +----(11. Request User Info)-------------------------->+
| | |
| +<---(12. Return User Info)----------------------------+
| | |
| +----(13. Request Access Token using Refresh Token)--->+
| | |
| +<---(14. Return new Access Token)---------------------+
| | |
+--------------------+ +---------------+ +-----------------+
| End-User (Browser) | | Relying Party | | OpenID Provider |
+--------------------+ +---------------+ +-----------------+
ということで、各ステップ毎の処理をやってみます。
1. Start log in flow
いきなりですが、ここのステップは省略します。何等かのサービス (Relying Party) のログイン画面にアクセスした状況をイメージすると良いと思います。
2. 302 / Redirect URL and parameters
1. Start log in flow において、「何等かのサービスのログイン画面」で「〇〇を使ってログイン」的なボタンを選択した場合、本来は Relying Party から 302 Found が返されます。この時 Location ヘッダーに URL が入っているのですが、今回は手動でそれっぽい URL を作りました。
http://localhost:8888/realms/foo-realm/protocol/openid-connect/auth?response_type=code&client_id=foo-client&state=foo-state&scope=openid&redirect_uri=http://localhost:8888/callback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
パラメーターを見やすくするために改行してみると、以下のような感じです。
http://localhost:8888/realms/foo-realm/protocol/openid-connect/auth # OpenID Provider (今回は Keycloak) の URL。 ?response_type=code # Authorization Code をリクエスト。 &client_id=foo-client # Keycloak で設定した Client 名。 &state=foo-state # 今回は検証なので適当な文字列。本当はちゃんとした値が必要。 &scope=openid # OpenID Connect による ID Token をリクエスト。 &redirect_uri=http://localhost:8888/callback # Keycloak で設定した Valid redirect URIs の値。今回は 404 になるダミーの URL。 &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM # 事前に準備した Code Challenge の値。今回は RFC 7636 に載ってるサンプルの値。 &code_challenge_method=S256 # Code Challenge の生成に使うメソッド。
今回はこの「手動で作ったそれっぽい URL」を検証のスタート地点とします。
3. Access OpenID Provider
前のステップ 2. 302 / Redirect URL and parameters で準備した URL (本来は Relying Party から受け取った URL) にブラウザからアクセスします。記事のタイトルに「curl を使って」と書いてあるのですが、Keycloak のログイン画面で username / password を入力する必要があるため、その処理まではブラウザを使って実施します。

いろいろ方法を探してみましたが、この部分のログイン処理を curl でいい感じにやる方法は見つけられませんでした... 何かいい情報を持ってる方がいたら教えていただけると嬉しいです!
4. Authentication
前述した URL にブラウザでアクセスすると、Keycloak のログイン画面が表示されるため、前の手順で作成したユーザー foo でログインします。

5. 302 / Redirect URL with Authorization Code
Keycloak 側での認証が完了すると、以下のように Keycloak の 404 画面が表示されると思います。今回は検証目的で「ダミーの URL」にリダイレクトするようにしているため、この 404 自体は想定通りの結果です。本来は「ログインしようとしているサービス側のページ」にリダイレクトされる動作になると思います。

上記の通り、404 になること自体は想定通りなので無視して問題ないのですが、重要なのは URL の部分です。

上記 URL の内容は以下のような感じになっていると思います。
http://localhost:8888/callback?state=foo-state&session_state=4827e50c-112c-aeb8-945b-c918eddbea9f&iss=http%3A%2F%2Flocalhost%3A8888%2Frealms%2Ffoo-realm&code=5132fa1b-1dfd-1a3f-a25a-5d810b464b25.4827e50c-112c-aeb8-945b-c918eddbea9f.df9e2b4d-3006-4ffc-8cd5-06ee74dd33be
パラメーターを見やすくするために改行してみると、以下のような感じです。
http://localhost:8888/callback # Keycloak で設定した Valid redirect URIs の値。今回は 404 になるダミーの URL。 ?state=foo-state # ステップ 2 で指定した値。 &session_state=4827e50c-112c-aeb8-945b-c918eddbea9f # この値がなんなのかはまだちょっとよくわかってません... &iss=http%3A%2F%2Flocalhost%3A8888%2Frealms%2Ffoo-realm # ID Token の Issuer (今回の場合 Keycloak) の URL。 &code=5132fa1b-1dfd-1a3f-a25a-5d810b464b25.4827e50c-112c-aeb8-945b-c918eddbea9f.df9e2b4d-3006-4ffc-8cd5-06ee74dd33be # Authorization Code。
ここで一番重要なのは code に設定されている値です。これが Authorization Code Flow における Authorization Code (認可コード) です。次のステップでこの Authorization Code を利用して OpenID Provider の Token Endpoint に Token Request を送ります。
6. Access Relying Party with Authorization Code
本来は 302 を受け取ったブラウザが自動でリダイレクトして、End-User のブラウザから Relying Party に Authorization Code が渡されます。
しかし、今回は curl で検証するために、手動で Authorization Code をコピーし、Relying Party から OpenID Provider に Token Request を送るための URL を準備します。
具体的な URL (HTTP Request) の内容は、次のステップに記載します。
7. Request ID Token
5. 302 / Redirect URL with Authorization Code で取得した Authorization Code を使って Relying Party から OpenID Provider に Token Request を送ります。
curl -XPOST \ -d 'client_id=foo-client' \ -d 'client_secret=jtnwDopaLXJNdg8NRCJg27Otkgcq5Vw2' \ -d 'redirect_uri=http://localhost:8888/callback' \ -d 'grant_type=authorization_code' \ -d 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \ -d 'code=5132fa1b-1dfd-1a3f-a25a-5d810b464b25.4827e50c-112c-aeb8-945b-c918eddbea9f.df9e2b4d-3006-4ffc-8cd5-06ee74dd33be' \ http://localhost:8888/realms/foo-realm/protocol/openid-connect/token
client_id=foo-client # Keycloak で設定した Client ID。 client_secret=jtnwDopaLXJNdg8NRCJg27Otkgcq5Vw2 # Keycloak の Clients -> Client details -> Credentials でコピーした値。 redirect_uri=http://localhost:8888/callback # Keycloak で設定した Valid redirect URIs の値。今回は 404 になるダミーの URL。 grant_type=authorization_code # 今回は Authorization Code Flow なので "authorization_code" を指定。 code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk # 事前に準備した Code Verifier の値。今回は RFC 7636 に載ってるサンプルの値。 code=5132fa1b-1dfd-1a3f-a25a-5d810b464b25.4827e50c-112c-aeb8-945b-c918eddbea9f.df9e2b4d-3006-4ffc-8cd5-06ee74dd33be # Authorization Code。
8. Return ID Token
7. Request ID Token の HTTP Request を実行すると、Response として以下のような JSON が返ってきます。Response (JSON) の中に id_token が含まれており、無事 ID Token を取得することができました。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDgzNzIsImlhdCI6MTc2NjMwODA3MiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiJvbnJ0YWM6MzNjMjRkYjQtZWE4MS0xM2RlLWIzMDctMjlhYzcyNWZmNjI1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9mb28tcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMjE5Y2M2YjEtNjcyOC00NjA3LTkzY2UtZGEyODkwMGJlMDRiIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZm9vLWNsaWVudCIsInNpZCI6IjQ4MjdlNTBjLTExMmMtYWViOC05NDViLWM5MThlZGRiZWE5ZiIsImFjciI6IjAiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4ODg4LyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1mb28tcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkZvbyBCYXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJmb28iLCJnaXZlbl9uYW1lIjoiRm9vIiwiZmFtaWx5X25hbWUiOiJCYXIiLCJlbWFpbCI6ImZvb0BleGFtcGxlLmNvbSJ9.sEZiV1GQJOp8Dn3gGHpAXLw90KeUWcdMg39e2CgJcqsui-w1JSZfv8iFashlWvfOi_TjEA0RCu6uAgJeO-4b7OI9SOuLqUDpLUcbWFZqwipHQK--NXjg_I8OIqpljCgTrdIsJwxf-yNCyIQDHyZGxR8w5P1bOt4hmVcJ1bA5MB_qcYaAM4UTdvHq_lhjSj3d_cLOzg0FnfyZvH0UE0WOGGBbiXcdyympIGszufR4tGkEWQdf3j5VBOcdd__1CqpkzKPKCO7xqI-tAq42S5uVYuC1tlyLhTzIuggNWUg2CObCPXrCEJSxVzcvyA59TDK6G7cAUkkdARLGbK66gabq_g", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmYTIxZDU2Yy0wZjc0LTQzMzUtOTZlYy0xNzM2MDdkMDUzMGIifQ.eyJleHAiOjE3NjYzMDk4NzIsImlhdCI6MTc2NjMwODA3MiwianRpIjoiMDcyNjBhNzktZjdhNy04YjEyLTk2NmUtNTFlNzFiNTVkOTk1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9mb28tcmVhbG0iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsInN1YiI6IjIxOWNjNmIxLTY3MjgtNDYwNy05M2NlLWRhMjg5MDBiZTA0YiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJmb28tY2xpZW50Iiwic2lkIjoiNDgyN2U1MGMtMTEyYy1hZWI4LTk0NWItYzkxOGVkZGJlYTlmIiwic2NvcGUiOiJvcGVuaWQgd2ViLW9yaWdpbnMgcHJvZmlsZSByb2xlcyBiYXNpYyBhY3IgZW1haWwifQ.SkfVTnMeNg5VK4EZWoAQK-8lnajB0Yhe7WvlhaPn0pB5sVKqhElxRMftDJgdNLFh9ULM9D5Ccxs4Iy64-GOUfA", "token_type": "Bearer", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDgzNzIsImlhdCI6MTc2NjMwODA3MiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiIyYTdmYjg5Ni1mNzViLWVlZjItMDM5YS0xYjZhMWJkZTBlZGUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsImF1ZCI6ImZvby1jbGllbnQiLCJzdWIiOiIyMTljYzZiMS02NzI4LTQ2MDctOTNjZS1kYTI4OTAwYmUwNGIiLCJ0eXAiOiJJRCIsImF6cCI6ImZvby1jbGllbnQiLCJzaWQiOiI0ODI3ZTUwYy0xMTJjLWFlYjgtOTQ1Yi1jOTE4ZWRkYmVhOWYiLCJhdF9oYXNoIjoiakVPWGl5VG9xSUhfOTgyU2hjMk1mdyIsImFjciI6IjAiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGb28gQmFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZm9vIiwiZ2l2ZW5fbmFtZSI6IkZvbyIsImZhbWlseV9uYW1lIjoiQmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5jb20ifQ.M8mvkwwOR_qlGUzYD6vI5VGjoUKvzu036c-dNpuQyY2aRjOQHZjjJVffz5NZSeQnx_fdnUmkklytxsvWwA0Q2RGXohoV9Umab6pH8WVFlj3FeTYUapnfFaMtEq3_7nNMzNXTTCfiwMCGOPgpuFbfF6u5qCvtdr67EuN6p37snznbpLmteDQDObhglJ-dH3QsOETThd8cPMAuyb_8NCxtXEMVvHLaHYoWC-nH8Y2j7iy-l3B-fCHE86khpJHme4nN9N0Ru0DVSc5qV5aqBCoXsG2Rlo0U2XY0Hscf3NX90aPco-zbRyJnE6nfYkGtH1BGwgRxTm-S-0xG_DotHokDXA", "not-before-policy": 0, "session_state": "4827e50c-112c-aeb8-945b-c918eddbea9f", "scope": "openid profile email" }
ID Token の中身をデコードしてみると、以下のような感じになっています。
ヘッダー:
{ "alg": "RS256", "typ": "JWT", "kid": "XgwwWH9pfe6w_ua-Gl5tyew04HjT-k1gTXfFG2eQuLA" }
{ "exp": 1766308372, "iat": 1766308072, "auth_time": 1766307740, "jti": "2a7fb896-f75b-eef2-039a-1b6a1bde0ede", "iss": "http://localhost:8888/realms/foo-realm", "aud": "foo-client", "sub": "219cc6b1-6728-4607-93ce-da28900be04b", "typ": "ID", "azp": "foo-client", "sid": "4827e50c-112c-aeb8-945b-c918eddbea9f", "at_hash": "jEOXiyToqIH_982Shc2Mfw", "acr": "0", "email_verified": false, "name": "Foo Bar", "preferred_username": "foo", "given_name": "Foo", "family_name": "Bar", "email": "foo@example.com" }
なお、Authorization Code には有効期限 (そこそこ短めの有効期限) があるため、5. 302 / Redirect URL with Authorization Code で Authorization Code を取得してから時間が経ちすぎていると、以下のようなエラーが返ってきます。
{"error":"invalid_grant","error_description":"Code not valid"}
その場合は、改めて 3. Access OpenID Provider からやり直してみてください。5. 302 / Redirect URL with Authorization Code で取得した Authorization Code をそこそこ素早くコピーして、7. Request ID Token の curl を実行する必要があります。
9. Validate ID Token
Relying Party は OpenID Provider から提供された ID Token を検証します。検証内容はいろいろあるようですが、今回は JWKS (公開鍵) を利用した署名検証をやってみます。
JWKS (公開鍵) の取得
Keycloak は OpenID Connect Discovery 1.0 という仕様に準拠しているようなので、.well-known/openid-configuration という Endpoint から OpenID Provider の様々な情報が取得できます。
今回は、「JWKS を公開している Endpoint (URL)」の情報を取得します。多くの情報が返されるため、jq コマンドを使って必要な情報のみに絞り込んでいます。
$ curl -s -XGET http://localhost:8888/realms/foo-realm/.well-known/openid-configuration | jq .jwks_uri "http://localhost:8888/realms/foo-realm/protocol/openid-connect/certs"
上記結果から、Keycloak が JWKS を公開している URL は http://localhost:8888/realms/foo-realm/protocol/openid-connect/certs であることがわかりました。
次は、この Endpoint から JWKS を取得します。JWKS は複数の JWK が含まれた配列になっているので、必要な値だけ取り出します。前述した ID Token のヘッダーの内容 (alg の値) から、署名に使われているアルゴリズムは RS256 であることが確認できるので、jq コマンドで alg の値が RS256 になっている JWK のみを取得します。
$ curl -s -XGET http://localhost:8888/realms/foo-realm/protocol/openid-connect/certs | jq '.keys[] | select(.alg == "RS256")' { "kid": "XgwwWH9pfe6w_ua-Gl5tyew04HjT-k1gTXfFG2eQuLA", "kty": "RSA", "alg": "RS256", "use": "sig", "x5c": [ "MIICoTCCAYkCBgGbPrn3xDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlmb28tcmVhbG0wHhcNMjUxMjIxMDIyMzUzWhcNMzUxMjIxMDIyNTMzWjAUMRIwEAYDVQQDDAlmb28tcmVhbG0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRsuZifx6NiryTjSHpoGqRUHIEnUv/3Cnmm3vCHa5MV2sFXs8sU5uPaSa7hnk3TYnplvQwdF9FJqpNlWoFeMcSLu4WHvDwu3Wb8xjXLcHQxNMXySfBWa8g9SEfVo+TQfEc5P4OvegvAKRNFRR5ZZ8WE4OWMBUB2XNhMD4x6yw52lvC8yduIaotefnLjBFLEot53semkmdpuwF6S8M6qf/1UGLA35GvImh4lRYbbPpzlngyIDBdNVObMzKYVB9n4OjDeddE7ZsbuZxTWv4BPWLS+7ZmKW0x4pZUcFoYV+Ad4tt+bxPUC9XIAr5U1ODi2c3/vYJlhev5ZXh+aqocDV9NAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFXW8sKGLJCgSKSaaVBKo6Njped5umj+ACevDdOpTAa77lfgZ/+ugS8pjZRbTWqNWkMAV9432UdYqqG3gpBqaNwySoLWiuZ/8B0Mi9My3BfDGvVFw1J+a2oM/BVS9TI22AygcGAAQd9Eod4+m2EYWAWhoHgE6Vtu2EbONMqiBY6RuXpD7HvL0ZgYBNAvx+UwCONaLF1PZNPcz0SI3Lhqvvucz0eFL7vW3F6G1yrv3mf3/X7BMr+RGwYhqF+6zEPNRXBMny4ZrdoWXA001bPWeLeXdG/AFeOpAEnzI/BKl7g23VFqbZfGdN894+nCXwHH4gr/FHCew8mAp31O0fAno3I=" ], "x5t": "IOnYk94boHwImw5nsOrc8VzCQnw", "x5t#S256": "1CbYxOP_0jhnYayM-p_ArGU_eTxZ5etl47zJBqLHOIU", "n": "0bLmYn8ejYq8k40h6aBqkVByBJ1L_9wp5pt7wh2uTFdrBV7PLFObj2kmu4Z5N02J6Zb0MHRfRSaqTZVqBXjHEi7uFh7w8Lt1m_MY1y3B0MTTF8knwVmvIPUhH1aPk0HxHOT-Dr3oLwCkTRUUeWWfFhODljAVAdlzYTA-MessOdpbwvMnbiGqLXn5y4wRSxKLed7HppJnabsBekvDOqn_9VBiwN-RryJoeJUWG2z6c5Z4MiAwXTVTmzMymFQfZ-Dow3nXRO2bG7mcU1r-AT1i0vu2ZiltMeKWVHBaGFfgHeLbfm8T1AvVyAK-VNTg4tnN_72CZYXr-WV4fmqqHA1fTQ", "e": "AQAB" }
上記出力内の "x5c" (X.509 Certificate Chain) の値が、ID Token の署名検証に利用する公開鍵です。
どうやら配列になっているようですが、今回は 1つしか含まれていないようなので、この公開鍵を使って署名を検証します。
また、この x5c の中身を PEM 形式にするために、以下のようなコマンドを実行します。
$ curl -s -XGET http://localhost:8888/realms/foo-realm/protocol/openid-connect/certs | \ jq -r '.keys[] | select(.alg == "RS256") | .x5c[0]' | \ (echo "-----BEGIN CERTIFICATE-----"; cat; echo "-----END CERTIFICATE-----") | \ openssl x509 -pubkey -noout > ./public_key.pem
すると、以下のような PEM 形式の公開鍵が出力されるはずです。
$ ls ./public_key.pem ./public_key.pem $ cat ./public_key.pem -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0bLmYn8ejYq8k40h6aBq kVByBJ1L/9wp5pt7wh2uTFdrBV7PLFObj2kmu4Z5N02J6Zb0MHRfRSaqTZVqBXjH Ei7uFh7w8Lt1m/MY1y3B0MTTF8knwVmvIPUhH1aPk0HxHOT+Dr3oLwCkTRUUeWWf FhODljAVAdlzYTA+MessOdpbwvMnbiGqLXn5y4wRSxKLed7HppJnabsBekvDOqn/ 9VBiwN+RryJoeJUWG2z6c5Z4MiAwXTVTmzMymFQfZ+Dow3nXRO2bG7mcU1r+AT1i 0vu2ZiltMeKWVHBaGFfgHeLbfm8T1AvVyAK+VNTg4tnN/72CZYXr+WV4fmqqHA1f TQIDAQAB -----END PUBLIC KEY-----
上記公開鍵を使って ID Token を検証します。
JWT の中身を加工
検証の準備として、ID Token (JWT) の中身をいろいろ加工しないといけないので、まずは変数 JWT に ID Token を入れます。
$ JWT='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDgzNzIsImlhdCI6MTc2NjMwODA3MiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiIyYTdmYjg5Ni1mNzViLWVlZjItMDM5YS0xYjZhMWJkZTBlZGUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsImF1ZCI6ImZvby1jbGllbnQiLCJzdWIiOiIyMTljYzZiMS02NzI4LTQ2MDctOTNjZS1kYTI4OTAwYmUwNGIiLCJ0eXAiOiJJRCIsImF6cCI6ImZvby1jbGllbnQiLCJzaWQiOiI0ODI3ZTUwYy0xMTJjLWFlYjgtOTQ1Yi1jOTE4ZWRkYmVhOWYiLCJhdF9oYXNoIjoiakVPWGl5VG9xSUhfOTgyU2hjMk1mdyIsImFjciI6IjAiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGb28gQmFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZm9vIiwiZ2l2ZW5fbmFtZSI6IkZvbyIsImZhbWlseV9uYW1lIjoiQmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5jb20ifQ.M8mvkwwOR_qlGUzYD6vI5VGjoUKvzu036c-dNpuQyY2aRjOQHZjjJVffz5NZSeQnx_fdnUmkklytxsvWwA0Q2RGXohoV9Umab6pH8WVFlj3FeTYUapnfFaMtEq3_7nNMzNXTTCfiwMCGOPgpuFbfF6u5qCvtdr67EuN6p37snznbpLmteDQDObhglJ-dH3QsOETThd8cPMAuyb_8NCxtXEMVvHLaHYoWC-nH8Y2j7iy-l3B-fCHE86khpJHme4nN9N0Ru0DVSc5qV5aqBCoXsG2Rlo0U2XY0Hscf3NX90aPco-zbRyJnE6nfYkGtH1BGwgRxTm-S-0xG_DotHokDXA'
次に、変数 JWT から Header と Payload の部分のみをファイル jwt_header_and_payload.txt に出力します。
$ HEADER_AND_PAYLOAD=$(echo -n $JWT | cut -d'.' -f1,2)
$ echo -n ${HEADER_AND_PAYLOAD} > jwt_header_and_payload.txt
$ cat ./jwt_header_and_payload.txt
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDgzNzIsImlhdCI6MTc2NjMwODA3MiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiIyYTdmYjg5Ni1mNzViLWVlZjItMDM5YS0xYjZhMWJkZTBlZGUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsImF1ZCI6ImZvby1jbGllbnQiLCJzdWIiOiIyMTljYzZiMS02NzI4LTQ2MDctOTNjZS1kYTI4OTAwYmUwNGIiLCJ0eXAiOiJJRCIsImF6cCI6ImZvby1jbGllbnQiLCJzaWQiOiI0ODI3ZTUwYy0xMTJjLWFlYjgtOTQ1Yi1jOTE4ZWRkYmVhOWYiLCJhdF9oYXNoIjoiakVPWGl5VG9xSUhfOTgyU2hjMk1mdyIsImFjciI6IjAiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGb28gQmFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZm9vIiwiZ2l2ZW5fbmFtZSI6IkZvbyIsImZhbWlseV9uYW1lIjoiQmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5jb20ifQ
今度は、変数 JWT から Signature の部分を取り出して、変数 SIG_BASE64URL に格納します。
$ SIG_BASE64URL=$(echo -n ${JWT} | cut -d'.' -f3)
JWT は base64url でエンコードされているので、一度 base64 でエンコードされたデータに変換します。
$ len=$((${#SIG_BASE64URL} % 4))
$ [[ $len -eq 2 ]] && SIG_BASE64URL="${SIG_BASE64URL}=="
$ [[ $len -eq 3 ]] && SIG_BASE64URL="${SIG_BASE64URL}="
$ SIG_BASE64=$(echo -n "${SIG_BASE64URL}" | tr '_-' '/+')
$ echo -n ${SIG_BASE64} M8mvkwwOR/qlGUzYD6vI5VGjoUKvzu036c+dNpuQyY2aRjOQHZjjJVffz5NZSeQnx/fdnUmkklytxsvWwA0Q2RGXohoV9Umab6pH8WVFlj3FeTYUapnfFaMtEq3/7nNMzNXTTCfiwMCGOPgpuFbfF6u5qCvtdr67EuN6p37snznbpLmteDQDObhglJ+dH3QsOETThd8cPMAuyb/8NCxtXEMVvHLaHYoWC+nH8Y2j7iy+l3B+fCHE86khpJHme4nN9N0Ru0DVSc5qV5aqBCoXsG2Rlo0U2XY0Hscf3NX90aPco+zbRyJnE6nfYkGtH1BGwgRxTm+S+0xG/DotHokDXA==
そして、base64 でエンコードされたデータを base64 コマンドでデコードし、signature.bin というファイルに出力します。
$ echo -n ${SIG_BASE64} | base64 -d > ./signature.bin
この signature.bin ファイルはバイナリファイルになっています。
$ file ./signature.bin ./signature.bin: data
$ hexdump -C ./signature.bin 00000000 33 c9 af 93 0c 0e 47 fa a5 19 4c d8 0f ab c8 e5 |3.....G...L.....| 00000010 51 a3 a1 42 af ce ed 37 e9 cf 9d 36 9b 90 c9 8d |Q..B...7...6....| 00000020 9a 46 33 90 1d 98 e3 25 57 df cf 93 59 49 e4 27 |.F3....%W...YI.'| 00000030 c7 f7 dd 9d 49 a4 92 5c ad c6 cb d6 c0 0d 10 d9 |....I..\........| 00000040 11 97 a2 1a 15 f5 49 9a 6f aa 47 f1 65 45 96 3d |......I.o.G.eE.=| 00000050 c5 79 36 14 6a 99 df 15 a3 2d 12 ad ff ee 73 4c |.y6.j....-....sL| 00000060 cc d5 d3 4c 27 e2 c0 c0 86 38 f8 29 b8 56 df 17 |...L'....8.).V..| 00000070 ab b9 a8 2b ed 76 be bb 12 e3 7a a7 7e ec 9f 39 |...+.v....z.~..9| 00000080 db a4 b9 ad 78 34 03 39 b8 60 94 9f 9d 1f 74 2c |....x4.9.`....t,| 00000090 38 44 d3 85 df 1c 3c c0 2e c9 bf fc 34 2c 6d 5c |8D....<.....4,m\| 000000a0 43 15 bc 72 da 1d 8a 16 0b e9 c7 f1 8d a3 ee 2c |C..r...........,| 000000b0 be 97 70 7e 7c 21 c4 f3 a9 21 a4 91 e6 7b 89 cd |..p~|!...!...{..| 000000c0 f4 dd 11 bb 40 d5 49 ce 6a 57 96 aa 04 2a 17 b0 |....@.I.jW...*..| 000000d0 6d 91 96 8d 14 d9 76 34 1e c7 1f dc d5 fd d1 a3 |m.....v4........| 000000e0 dc a3 ec db 47 22 67 13 a9 df 62 41 ad 1f 50 46 |....G"g...bA..PF| 000000f0 c2 04 71 4e 6f 92 fb 4c 46 fc 3a 2d 1e 89 03 5c |..qNo..LF.:-...\| 00000100
ID Token の署名を検証
公開鍵を利用した ID Token の署名検証の準備が整ったので、openssl コマンドを使って署名を検証してみます。署名が正しく検証できた場合は、以下のように Verified OK と出力されます。
$ openssl dgst -sha256 -verify ./public_key.pem -signature ./signature.bin ./jwt_header_and_payload.txt Verified OK
もし、何等かの理由で署名の検証に失敗すると、以下のように Verification failure と出力されます。下記は「異なる公開鍵を使って署名を検証しようとした」時の出力例です。
$ openssl dgst -sha256 -verify ./public_key.pem -signature ./signature.bin ./jwt_header_and_payload.txt Verification failure 40F7E8CF71790000:error:0200008A:rsa routines:RSA_padding_check_PKCS1_type_1:invalid padding:../crypto/rsa/rsa_pk1.c:79: 40F7E8CF71790000:error:02000072:rsa routines:rsa_ossl_public_decrypt:padding check failed:../crypto/rsa/rsa_ossl.c:697: 40F7E8CF71790000:error:1C880004:Provider routines:rsa_verify:RSA lib:../providers/implementations/signature/rsa_sig.c:774:
10. Log in completed
9. Validate ID Token で公開鍵を利用した ID Token の署名検証 (および本来はその他諸々の検証) が正常に完了すれば、End-User が OpenID Provider で認証されたことが確認できるので、Relying Party は自身のサービス内で必要なログイン処理を行なって、End-User に対して「ログイン完了」を返します。例えば、ログイン後にトップページやマイページのようなページが表示された状態になるイメージです。
11. Request User Info
特に追加で情報が必要なければ、前述した 10. Log in completed でログイン処理は完了しますが、Relying Party は OpenID Provider の UserInfo Endpoint からユーザーの情報を取得することも可能です。
UserInfo Endpoint に UserInfo Request を送る場合は、Authorization ヘッダーに Bearer Token として Access Token (8. Return ID Token で取得した JWT 内の access_token の値) を設定し、http://localhost:8888/realms/foo-realm/protocol/openid-connect/userinfo に GET Request を送ります。
curl -XGET -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDgzNzIsImlhdCI6MTc2NjMwODA3MiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiJvbnJ0YWM6MzNjMjRkYjQtZWE4MS0xM2RlLWIzMDctMjlhYzcyNWZmNjI1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9mb28tcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMjE5Y2M2YjEtNjcyOC00NjA3LTkzY2UtZGEyODkwMGJlMDRiIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZm9vLWNsaWVudCIsInNpZCI6IjQ4MjdlNTBjLTExMmMtYWViOC05NDViLWM5MThlZGRiZWE5ZiIsImFjciI6IjAiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4ODg4LyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1mb28tcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkZvbyBCYXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJmb28iLCJnaXZlbl9uYW1lIjoiRm9vIiwiZmFtaWx5X25hbWUiOiJCYXIiLCJlbWFpbCI6ImZvb0BleGFtcGxlLmNvbSJ9.sEZiV1GQJOp8Dn3gGHpAXLw90KeUWcdMg39e2CgJcqsui-w1JSZfv8iFashlWvfOi_TjEA0RCu6uAgJeO-4b7OI9SOuLqUDpLUcbWFZqwipHQK--NXjg_I8OIqpljCgTrdIsJwxf-yNCyIQDHyZGxR8w5P1bOt4hmVcJ1bA5MB_qcYaAM4UTdvHq_lhjSj3d_cLOzg0FnfyZvH0UE0WOGGBbiXcdyympIGszufR4tGkEWQdf3j5VBOcdd__1CqpkzKPKCO7xqI-tAq42S5uVYuC1tlyLhTzIuggNWUg2CObCPXrCEJSxVzcvyA59TDK6G7cAUkkdARLGbK66gabq_g" http://localhost:8888/realms/foo-realm/protocol/openid-connect/userinfo
12. Return User Info
11. Request User Info の HTTP Request (UserInfo Request) を実行すると、Response として以下のような JSON が返ってきます。Response (JSON) の中にユーザー情報が含まれていることが確認できます。
{ "sub": "219cc6b1-6728-4607-93ce-da28900be04b", "email_verified": false, "name": "Foo Bar", "preferred_username": "foo", "given_name": "Foo", "family_name": "Bar", "email": "foo@example.com" }
13. Request Access Token using Refresh Token
前述した UserInfo Request を実行する際は Access Token を指定する必要がありますが、基本的に Access Token は有効期限が短く設定されています。Access Token の有効期限が切れた場合は、Refresh Token を使って新しい Access Token を取得することが可能です。
新しい Access Token を取得する場合は、以下のような Request を Token Endpoint に送ります。refresh_token には 8. Return ID Token で取得した JWT 内の refresh_token の値を指定します。
curl -XPOST \ -d 'client_id=foo-client' \ -d 'client_secret=jtnwDopaLXJNdg8NRCJg27Otkgcq5Vw2' \ -d 'grant_type=refresh_token' \ -d 'refresh_token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmYTIxZDU2Yy0wZjc0LTQzMzUtOTZlYy0xNzM2MDdkMDUzMGIifQ.eyJleHAiOjE3NjYzMDk4NzIsImlhdCI6MTc2NjMwODA3MiwianRpIjoiMDcyNjBhNzktZjdhNy04YjEyLTk2NmUtNTFlNzFiNTVkOTk1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9mb28tcmVhbG0iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsInN1YiI6IjIxOWNjNmIxLTY3MjgtNDYwNy05M2NlLWRhMjg5MDBiZTA0YiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJmb28tY2xpZW50Iiwic2lkIjoiNDgyN2U1MGMtMTEyYy1hZWI4LTk0NWItYzkxOGVkZGJlYTlmIiwic2NvcGUiOiJvcGVuaWQgd2ViLW9yaWdpbnMgcHJvZmlsZSByb2xlcyBiYXNpYyBhY3IgZW1haWwifQ.SkfVTnMeNg5VK4EZWoAQK-8lnajB0Yhe7WvlhaPn0pB5sVKqhElxRMftDJgdNLFh9ULM9D5Ccxs4Iy64-GOUfA' \ -d 'scope=openid' \ http://localhost:8888/realms/foo-realm/protocol/openid-connect/token
14. Return new Access Token
13. Request Access Token using Refresh Token の HTTP Request を実行すると、Response として以下のような JSON が返ってきます。Response (JSON) の中に新しい Access Token や Refresh Token が含まれていることが確認できます。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDkxOTYsImlhdCI6MTc2NjMwODg5NiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiJvbnJ0cnQ6YWFkNTBkNjItY2JjZC1iZjNkLWVhNmMtMDRkZTVmMjhkNWFhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9mb28tcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMjE5Y2M2YjEtNjcyOC00NjA3LTkzY2UtZGEyODkwMGJlMDRiIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZm9vLWNsaWVudCIsInNpZCI6IjQ4MjdlNTBjLTExMmMtYWViOC05NDViLWM5MThlZGRiZWE5ZiIsImFjciI6IjAiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4ODg4LyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1mb28tcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkZvbyBCYXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJmb28iLCJnaXZlbl9uYW1lIjoiRm9vIiwiZmFtaWx5X25hbWUiOiJCYXIiLCJlbWFpbCI6ImZvb0BleGFtcGxlLmNvbSJ9.GW_7D3x7k3AwcF_Y5Gfk0TCjKxPtp56PpjO3RPlkpzqsFbHDDVjxju87tSJHdrpvxWkvGRZo6JzTOIbQUgyvljaMvb6dZG8hNgWoh_Go6nusqi6nOrsWRjIOSan6g1fYz-A519FJsi08wBIK0OhQZjMNecZCLUhJrhHz011N_aavaljH39a1LfGwoRlLZiTeWtbJk1KIKrKYhmbVq-D2lnTqnIDi3SRh1AoMBmtmzPit5TVbOQfCsTrQ8fQ3jVwe9QG8O7jkLVNHs3xngXIJEig9ojcJWteiStdF1yAiMv-qJxIcQ_ukOV0s9DMgaH_oGN6tdGIuz-hCYNk-nxQUyg", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmYTIxZDU2Yy0wZjc0LTQzMzUtOTZlYy0xNzM2MDdkMDUzMGIifQ.eyJleHAiOjE3NjYzMTA2OTYsImlhdCI6MTc2NjMwODg5NiwianRpIjoiNTQxYThlMTUtMjYzMi05NDBiLWIzNzktMGI4YmE0ZjdjMmQ1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L3JlYWxtcy9mb28tcmVhbG0iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsInN1YiI6IjIxOWNjNmIxLTY3MjgtNDYwNy05M2NlLWRhMjg5MDBiZTA0YiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJmb28tY2xpZW50Iiwic2lkIjoiNDgyN2U1MGMtMTEyYy1hZWI4LTk0NWItYzkxOGVkZGJlYTlmIiwic2NvcGUiOiJvcGVuaWQgd2ViLW9yaWdpbnMgcHJvZmlsZSByb2xlcyBiYXNpYyBhY3IgZW1haWwifQ.dMy-mw9J5d3WJaPG5S0SF974UOG4_Amb6dHh5SsFoEaRXgMheOk4DwOyW-Ko9CjFigJkBULM5TDshoqvKykC-w", "token_type": "Bearer", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYZ3d3V0g5cGZlNndfdWEtR2w1dHlldzA0SGpULWsxZ1RYZkZHMmVRdUxBIn0.eyJleHAiOjE3NjYzMDkxOTYsImlhdCI6MTc2NjMwODg5NiwiYXV0aF90aW1lIjoxNzY2MzA3NzQwLCJqdGkiOiJhMDFlZDVlZS1jNzM5LWQ4YmMtMWViYS1kYzQyMjllMTgyNTIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvcmVhbG1zL2Zvby1yZWFsbSIsImF1ZCI6ImZvby1jbGllbnQiLCJzdWIiOiIyMTljYzZiMS02NzI4LTQ2MDctOTNjZS1kYTI4OTAwYmUwNGIiLCJ0eXAiOiJJRCIsImF6cCI6ImZvby1jbGllbnQiLCJzaWQiOiI0ODI3ZTUwYy0xMTJjLWFlYjgtOTQ1Yi1jOTE4ZWRkYmVhOWYiLCJhdF9oYXNoIjoiU0JSa0V3V3Z5QlEtMEdISURpLVcyZyIsImFjciI6IjAiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGb28gQmFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZm9vIiwiZ2l2ZW5fbmFtZSI6IkZvbyIsImZhbWlseV9uYW1lIjoiQmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5jb20ifQ.HAuF5LJnGo3FLjBj0v-8qM912ySApCE-WoLIp8_gZsxip2xb-aJ0pa5kXAOJt4mOBurlyS98K3Qq-ClFK-twO1NU2T8F64meWPAx8P05RpqvbKNVOkja8PO4u7McHuU_rHVFXNj9_XihkH376g9t5n4H72y5xOj88xHcOKkHvHL4YSMw52DuPsA9hDu8fB_Hp-rkf4gv4cLeBhnqme50nE1hpmL8O-tCmQYF1aHY9MSvklkOL5zGdZ9p6TULXTUQaspTipUKAeOSL0zJwC-m7idxi2sK4XMpU4bJVGUJh7o8wG91zg598LFfcQcW15EapX7Jo2R9ETMZdS-S9mozPA", "not-before-policy": 0, "session_state": "4827e50c-112c-aeb8-945b-c918eddbea9f", "scope": "openid profile email" }
ということで、OpenID Connect で ID Token を取得する流れを一通り試してみることができました。今回の検証はここまでです。
参考情報
- OpenID Connect
- OAuth 2.0
まとめ
Keycloak を利用し、OpenID Connect による認証の流れを (可能な範囲で) curl を使ってやってみました。
なんとなくふわっと雰囲気で理解していた OpenID Connect や OAuth の仕組みを、少し理解できたような気がします。やはり、実際に手を動かしてみるのは大事 (& 楽しい) ですね。
今なら、以前のブログで設定した OpenID Connect 関連の設定項目の中身も多少理解できるようになったんじゃないかと思います。
また、最近はお仕事でもクラウド周りの認証設定 (GitHub Actions からクラウド上のリソースにアクセスする時 etc) で OpenID Connect を使うことがあるので、今後は今までよりもスムーズにその辺の設定作業を実施できるようになったんじゃないかと期待してます (できるようになったとは言ってない)。
PKCE (Proof Key for Code Exchange) の Code Challenge を生成する Java App を作った
最近 OpenID Connect とか OAuth 2.0 とかをちょっと勉強しているのですが、いろいろ検証している時に PKCE (Proof Key for Code Exchange) の Code Challenge を手元で シュッ と生成したくなったので、それっぽいツールを作ってみました。
引数に Code Verifier を渡してあげると、Code Challenge を出力してくれます。
$ pkce-code-challenge-generator dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk Code Verifier : dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk Code Challenge : E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
一応、RFC 7636 の 4.1. Client Creates a Code Verifier に記載されている内容を基に Code Verifier の Validation も実行してくれます。
文字数が 43 以上 128 以下 という条件を満たしてない時:
$ pkce-code-challenge-generator foobarbaz java.lang.IllegalArgumentException: Error: Input string must be between 43 and 128 characters. See: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 at org.example.PkceCodeChallengeGenerator.validateArguments(PkceCodeChallengeGenerator.java:38) at org.example.Main.main(Main.java:26) at java.base@21.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
利用できない記号が含まれている時:
$ pkce-code-challenge-generator aaaaaaaaaa#bbbbbbbbbb?cccccccccc*dddddddddd%eee java.lang.IllegalArgumentException: Error: Input string contains invalid characters. Only characters from the unreserved set are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~" See: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 at org.example.PkceCodeChallengeGenerator.validateArguments(PkceCodeChallengeGenerator.java:47) at org.example.Main.main(Main.java:26) at java.base@21.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
実装については、Keycloak の実装を参考にしました。ありがとう Keycloak。
おまけ
前述したようなツールを作りましたが、いろいろ試行錯誤してみたところ、Linux (Ubuntu) 上で shasum とか xxd とか basenc を使っていい感じに Code Challenge が生成できました。ちょっとした検証ならこれで十分だったかもしれません (´・ω・`)
$ echo -n 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' | shasum -a 256 -b | awk '{ print $1 }' | xxd -r -p | basenc --base64url | sed 's/=//g' E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
GraalVM の公式コンテナイメージ内で "./gradlew nativeCompile" がエラーになる
とある Java のプロジェクト (Gradle を利用) で、Gradle 用の GraalVM Plugin を入れ、./gradlew nativeCompile で Native Image を Build できるようにしています。
この Native Image を Build する作業に GraalVM 公式のコンテナイメージ (ghcr.io/graalvm/native-image-community:21-muslib とか) を使おうと思い、コンテナの中で ./gradlew nativeCompile を実行してみたところ、↓ の感じのエラー xargs is not available になってしまいました。
$ docker run --rm -v $(pwd):/build -w /build \ --entrypoint /bin/bash \ ghcr.io/graalvm/native-image-community:21-muslib \ -c "./gradlew nativeCompile" xargs is not available
xargs が無いと言われているので、実際に ./gradlew の中を grep してみたところ、確かに ./gradlew の中で xargs を使っているようでした。
$ grep -i xargs ./gradlew
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
die "xargs is not available"
# Use "xargs" to parse quoted args.
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
xargs -n1 |
xargs は findutils というパッケージに入っているようなので、./gradlew nativeCompile を実行する前にコンテナの中で xargs (findutils)をインストールするようにして解決しました。
$ docker run --rm -v $(pwd):/build -w /build \ --entrypoint /bin/bash \ ghcr.io/graalvm/native-image-community:21-muslib \ -c "microdnf install -y findutils && ./gradlew nativeCompile"
実行例:
$ docker run --rm -v $(pwd):/build -w /build \
--entrypoint /bin/bash \
ghcr.io/graalvm/native-image-community:21-muslib \
-c "microdnf install -y findutils && ./gradlew nativeCompile"
Downloading metadata...
Downloading metadata...
Downloading metadata...
Package Repository Size
Installing:
findutils-1:4.8.0-7.el9.x86_64 ol9_baseos_latest 604.9 kB
Transaction Summary:
Installing: 1 packages
Reinstalling: 0 packages
Upgrading: 0 packages
Obsoleting: 0 packages
Removing: 0 packages
Downgrading: 0 packages
Downloading packages...
Running transaction test...
Installing: findutils;1:4.8.0-7.el9;x86_64;ol9_baseos_latest
Complete.
Downloading https://services.gradle.org/distributions/gradle-9.2.1-bin.zip
............10%.............20%.............30%.............40%.............50%.............60%.............70%.............80%.............90%.............100%
Welcome to Gradle 9.2.1!
Here are the highlights of this release:
- Windows ARM support
- Improved publishing APIs
- Better guidance for dependency verification failures
For more details see https://docs.gradle.org/9.2.1/release-notes.html
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no cached configuration is available for tasks: nativeCompile
> Task :app:processResources NO-SOURCE
> Task :app:compileJava
> Task :app:classes
> Task :app:jar
> Task :app:generateResourcesConfigFile
[native-image-plugin] Resources configuration written into /build/app/build/native/generated/generateResourcesConfigFile/resource-config.json
> Task :app:nativeCompile
[native-image-plugin] GraalVM Toolchain detection is disabled
[native-image-plugin] GraalVM location read from environment variable: JAVA_HOME
[native-image-plugin] Native Image executable path: /usr/lib64/graalvm/graalvm-community-java21/lib/svm/bin/native-image
========================================================================================================================
GraalVM Native Image: Generating 'pkce-code-challenge-generator' (static executable)...
========================================================================================================================
For detailed information and explanations on the build output, visit:
https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md
------------------------------------------------------------------------------------------------------------------------
[1/8] Initializing... (5.7s @ 0.11GB)
Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
Graal compiler: optimization level: 2, target machine: x86-64-v3
C compiler: x86_64-linux-musl-gcc (linux, x86_64, 10.2.1)
Garbage collector: Serial GC (max heap size: 80% of RAM)
1 user-specific feature(s):
- com.oracle.svm.thirdparty.gson.GsonFeature
------------------------------------------------------------------------------------------------------------------------
Build resources:
- 26.49GB of memory (56.3% of 47.05GB system memory, determined at start)
- 14 thread(s) (100.0% of 14 available processor(s), determined at start)
[2/8] Performing analysis... [*****] (17.3s @ 0.34GB)
3,341 reachable types (72.2% of 4,625 total)
3,976 reachable fields (48.2% of 8,247 total)
16,181 reachable methods (45.9% of 35,264 total)
1,061 types, 90 fields, and 693 methods registered for reflection
57 types, 57 fields, and 52 methods registered for JNI access
4 native libraries: dl, pthread, rt, z
[3/8] Building universe... (2.7s @ 0.40GB)
[4/8] Parsing methods... [*] (1.8s @ 0.41GB)
[5/8] Inlining methods... [***] (1.3s @ 0.40GB)
[6/8] Compiling methods... [****] (18.0s @ 0.32GB)
[7/8] Layouting methods... [**] (2.2s @ 0.38GB)
[8/8] Creating image... [**] (3.3s @ 0.44GB)
5.69MB (40.33%) for code area: 9,215 compilation units
8.07MB (57.14%) for image heap: 108,156 objects and 47 resources
365.81kB ( 2.53%) for other data
14.12MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area: Top 10 object types in image heap:
4.32MB java.base 1.69MB byte[] for code metadata
988.94kB svm.jar (Native Image) 1.42MB byte[] for java.lang.String
113.72kB java.logging 1.05MB java.lang.String
65.00kB org.graalvm.nativeimage.base 776.68kB java.lang.Class
47.59kB jdk.proxy1 287.12kB com.oracle.svm.core.hub.DynamicHubCompanion
45.84kB jdk.proxy3 279.30kB byte[] for general heap data
27.06kB jdk.internal.vm.ci 252.94kB java.util.HashMap$Node
22.06kB org.graalvm.collections 231.24kB java.lang.Object[]
11.42kB jdk.proxy2 204.55kB java.lang.String[]
8.07kB jdk.internal.vm.compiler 172.73kB java.util.concurrent.ConcurrentHashMap$Node
9.71kB for 3 more packages 1.75MB for 946 more object types
------------------------------------------------------------------------------------------------------------------------
Recommendations:
INIT: Adopt '--strict-image-heap' to prepare for the next GraalVM release.
HEAP: Set max heap for improved and more predictable memory usage.
CPU: Enable more CPU features with '-march=native' for improved performance.
------------------------------------------------------------------------------------------------------------------------
2.2s (4.1% of total time) in 178 GCs | Peak RSS: 0.89GB | CPU load: 9.23
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
/build/app/build/native/nativeCompile/pkce-code-challenge-generator (executable)
========================================================================================================================
Finished generating 'pkce-code-challenge-generator' in 52.9s.
[native-image-plugin] Native Image written to: /build/app/build/native/nativeCompile
BUILD SUCCESSFUL in 1m 46s
4 actionable tasks: 4 executed
Configuration cache entry stored.
以上!
Keycloak を使った SSO (OIDC/JWT) で CockroachDB に SQL CLI で接続してみる
前回、Keycloak を使った SSO (OIDC) で CockroachDB の DB Console にログインしてみたのですが、CockroachDB では SQL の実行も SSO で認証 (JTW を使って認証) できるようなので、そちらも追加で検証してみました。
環境
今回は、以下のような環境 (minikube で作った Kubernetes 上に諸々デプロイ) で検証しています。
バージョン
Kubernetes : v1.34.0 minikube : v1.37.0 Docker : v29.1.2 CockroachDB : v25.4.1 Keycloak : v26.4.7 Helm : v4.0.0 Chart Version (CockroachDB) : v19.0.1 Ubuntu (WSL2 / Windows 11) : 24.04.3 LTS Firefox : 145.0.2 (64 bit)
環境構成概要図
+---[Kubernetes (minikube)]------------------------+
| |
| +---[CockroachDB (Pods)]---------------+ |
| | | |
+---(Access with SSO)-----------+---+-->[DB Console (8080 port)] | |
| | | | |
| | | [SQL Interface (26257 port)]<------+---+ |
| | | | | |
| | +--------------------------------------+ | |
| | | |
| | | |
| | +---[CockroachDB Client (Pod)]---------+ | |
| | | | | |
[Client]---+---(Access with SSO / JWT)-----+---+-->[SQL CLI]--------------------------+---+ |
| | | | |
| | +--------------------------------------+ |
| | |
| | |
| | +---[Keycloak (Pods)]------------------+ |
| | | | |
| | | +---[Realm: keycroach]---------+ | |
| | | | | | |
| | | | [Client: cockroachdb] | | |
+---(Authentication)------------+-->| | | | |
| | | [User: goki@example.com] | | |
| | | | | |
| | +------------------------------+ | |
| | | |
| +--------------------------------------+ |
| |
+--------------------------------------------------+
Note:
- CockroachDB の設定を変更 (環境構築) する際は、クライアント証明書を使った認証で
rootユーザーとしてアクセスしています。最後に一般ユーザーとして SQL を実行する部分は SSO / JWT を使った認証でアクセスします。 - Keycloak については全然詳しくないので、Realm / Client / User あたりの位置関係が間違ってるかもしれません... 何か間違ってたらご指摘いただけると嬉しいです!
検証
ということで、さっそく検証してみます。検証で利用するマニフェストは以下の GitHub リポジトリにまとめてあります。
環境構築 (前回のおさらい)
まずは、前回のブログ記事の手順に沿って、同じ環境を構築します。
環境構築 (追加設定)
次に、前回作成した環境に、追加でいろいろ設定していきます。
CockroachDB 側の設定を変更するために、SQL CLI で CockroachDB に接続します (ここはクライアント証明書を利用した認証で root ユーザとして接続しています)。
$ kubectl exec -it cockroachdb-client -- ./cockroach sql --certs-dir=./cockroach-certs --host=cockroachdb-public # # Welcome to the CockroachDB SQL shell. # All statements must be terminated by a semicolon. # To exit, type: \q. # # Server version: CockroachDB CCL v25.4.1 (x86_64-pc-linux-gnu, built 2025/11/26 12:08:42, go1.23.12 X:nocoverageredesign) (same version as client) # Cluster ID: 0ad72295-881b-495e-99f9-817e306f51b7 # # Enter \? for a brief introduction. # root@cockroachdb-public:26257/defaultdb>
SQL CLI で接続したら、server.jwt_authentication.* の各項目に対して必要な値を設定していきます。
enabled に true を設定し、JWT を利用した認証を有効にします。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.jwt_authentication.enabled = true; SET CLUSTER SETTING Time: 9ms total (execution 8ms / network 0ms)
issuers.configuration に IdP (Keycloak) の Issuer URL を設定します。この設定は server.oidc_authentication.provider_url の値 (前回の環境構築で設定した値) と同じ値である必要があるっぽいです。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.jwt_authentication.issuers.configuration = 'http://keycloak:8888/realms/keycroach'; SET CLUSTER SETTING Time: 15ms total (execution 15ms / network 0ms)
audience の値を設定します。この設定は server.oidc_authentication.client_id の値 (前回の環境構築で設定した値) と同じ値である必要があるっぽいです。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.jwt_authentication.audience = 'cockroachdb'; SET CLUSTER SETTING Time: 6ms total (execution 6ms / network 0ms)
claim を設定します。今回はメールアドレスを使ってユーザーを識別するので、email を設定します。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.jwt_authentication.claim = 'email'; SET CLUSTER SETTING Time: 6ms total (execution 6ms / network 0ms)
jwks_auto_fetch.enabled を true に設定します。この設定を有効にすることで、CockroachDB が JWKS (JWT の署名検証に利用する公開鍵) を Keycloak から自動で取得してくれます。個別に JWKS を設定したい場合は、server.jwt_authentication.jwks という設定項目で設定することもできるようです。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.jwt_authentication.jwks_auto_fetch.enabled = true; SET CLUSTER SETTING Time: 6ms total (execution 6ms / network 0ms)
server.identity_map.configuration をいい感じに設定します。今回は、「ユーザーのメールアドレス goki@example.com からドメイン部分 (@ 以降) を除いたもの」と「DB 側に作成した SQL ユーザー」を紐づける感じの設定になっています。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.identity_map.configuration = 'http://keycloak:8888/realms/keycroach /^(.*)@example\.com$ \1'; SET CLUSTER SETTING Time: 6ms total (execution 6ms / network 0ms)
server.oidc_authentication.generate_cluster_sso_token.enabled に true を設定し、Token (JWT) の生成を有効にします。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.generate_cluster_sso_token.enabled = true; SET CLUSTER SETTING Time: 20ms total (execution 19ms / network 0ms)
最後に、server.oidc_authentication.generate_cluster_sso_token.use_token に id_token を設定します。この設定値は利用する IdP に合わせて設定する必要があるっぽいのですが、正直なところ詳細はよくわかりませんでした... 今回の構成 (local で Keycloak を使う) であれば、たぶんこの設定で良いのではないかと思います。動いたので。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.generate_cluster_sso_token.use_token = id_token; SET CLUSTER SETTING Time: 7ms total (execution 6ms / network 0ms)
これで、「SSO (JWT) を利用した認証による SQL 実行」に必要な追加設定は完了です。
SSO (JWT) で認証して SQL を実行
それでは、SSO (JWT) を利用した認証で SQL を実行してみましょう。
改めて CockroachDB の DB Console (https://localhost:8080/) にアクセスします。前回の検証で一度ログインしている場合は、画面右上の G (ユーザー名のイニシャル) から Logout を選んでログアウトします。

すると、DB Console のログイン画面に Generate JWT auth token for cluster SSO というボタンが表示されているので、それを選択します。

Generate JWT auth token for cluster SSO を選択すると、Keycloak 側のログイン画面にリダイレクトされ、Username と Password の入力を求められるので、Keycloak 側の Users ページで作成した User のメールアドレス goki@example.com と Credentials 画面で設定したパスワードをそれぞれ入力し、Keycloak 側で認証します。

Keycloak 側での認証が完了すると、CockroachDB の Cluster SSO 画面にリダイレクトされ、JWT を取得することができます。

上記画面で取得した JWT を利用して、SQL CLI で CockroachDB に接続します。以下のコマンドの <JWT_TOKEN> の部分を JWT に置き換えます。
kubectl exec -it cockroachdb-client -- ./cockroach sql --url "postgresql://goki:<JWT_TOKEN>@cockroachdb-public:26257?options=--crdb:jwt_auth_enabled=true" --certs-dir=./cockroach-certs
ちょっとコマンドが長い (主に JWT が長い) ですが、実際に値を入れて実行すると以下のような感じになります。
$ kubectl exec -it cockroachdb-client -- ./cockroach sql --url "postgresql://goki:eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJLbUNjUXZsUjJQS253RDhQbU1yREw4dmhuZUtVejJYcURpSHF6bzV3RWFvIn0.eyJleHAiOjE3NjQ4OTE2MjcsImlhdCI6MTc2NDg5MTMyNywiYXV0aF90aW1lIjoxNzY0ODkwOTc0LCJqdGkiOiI5NzRkZTQxZC04OWY3LWNlNmEtNjZiZS1mODUwMTc0MGQzYTciLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODg4OC9yZWFsbXMva2V5Y3JvYWNoIiwiYXVkIjoiY29ja3JvYWNoZGIiLCJzdWIiOiI2NjA2ZDM5Zi1jNDMwLTRhN2QtODQ0YS0zYzU2YmVkYjU0MDAiLCJ0eXAiOiJJRCIsImF6cCI6ImNvY2tyb2FjaGRiIiwic2lkIjoiOGYxZmYxMzItMzVjZi0zMTFiLWY3MGEtMDAyZjg3OWRhMmU2IiwiYXRfaGFzaCI6IkUyaVR6aFJfSmdzbmVGZlJ6M0VTQmciLCJhY3IiOiIwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiRm9vIEJhciIsInByZWZlcnJlZF91c2VybmFtZSI6Imdva2kiLCJnaXZlbl9uYW1lIjoiRm9vIiwiZmFtaWx5X25hbWUiOiJCYXIiLCJlbWFpbCI6Imdva2lAZXhhbXBsZS5jb20ifQ.c7s91bpncxqYTjaYewEVAcNyHCF6e0pwbtNlhrp8oCNGY1uvvBby367LcwPHAZjt_1lCrfOXI2WfRuhl2rMAcxOR-zqxgjUZY4VnTEn-bH6WsP3u5EoEEQ3ZvdoC_yuyfUkv-HJeAkuRe5mVdeGLFOxd4N38blkSFnYXpB589NIhoeM3liR_nryrel348S3GkfKK59q-DRyfxZseSNh2kZS4KEzA6oGIdSvQ20IGGp2O5i3s7ih1Oy89b8uuMh_8vTl5b1yK7KCfta_o1fjel_gYoPXwikKOolAVndbuDsyD4i9ynVugo5m7-84ULzktYOtp6zxskeIK0RUmyJZ4UA@cockroachdb-public:26257?options=--crdb:jwt_auth_enabled=true" --certs-dir=./cockroach-certs # # Welcome to the CockroachDB SQL shell. # All statements must be terminated by a semicolon. # To exit, type: \q. # # Server version: CockroachDB CCL v25.4.1 (x86_64-pc-linux-gnu, built 2025/11/26 12:08:42, go1.23.12 X:nocoverageredesign) (same version as client) # Cluster ID: 0ad72295-881b-495e-99f9-817e306f51b7 # # Enter \? for a brief introduction. # goki@cockroachdb-public:26257/defaultdb>
上記のように SQL CLI のプロンプトが表示され、SSO (JWT) を利用した認証で SQL CLI から CockroachDB にアクセスできました。
せっかくなので、試しに適当なテーブルを作って読み書きしてみたところ、以下のようにちゃんと SQL を実行できました。
goki@cockroachdb-public:26257/defaultdb> SELECT version(); version ------------------------------------------------------------------------------------------------------------ CockroachDB CCL v25.4.1 (x86_64-pc-linux-gnu, built 2025/11/26 12:08:42, go1.23.12 X:nocoverageredesign) (1 row) Time: 1ms total (execution 0ms / network 0ms) goki@cockroachdb-public:26257/defaultdb> goki@cockroachdb-public:26257/defaultdb> CREATE TABLE sso_sql_test (goki TEXT); CREATE TABLE Time: 49ms total (execution 18ms / network 32ms) goki@cockroachdb-public:26257/defaultdb> goki@cockroachdb-public:26257/defaultdb> INSERT INTO sso_sql_test VALUES ('buri'); INSERT 0 1 Time: 7ms total (execution 6ms / network 0ms) goki@cockroachdb-public:26257/defaultdb> goki@cockroachdb-public:26257/defaultdb> SELECT * FROM sso_sql_test; goki -------- buri (1 row) Time: 3ms total (execution 2ms / network 0ms)
おまけ 1
JWT の有効期限が切れていると、以下のように ERROR: JWT authentication: invalid token というエラーが出力され、CockroachDB にアクセスできません。いい感じに JWT で認証ができていることが確認できます。
$ kubectl exec -it cockroachdb-client -- ./cockroach sql --url "postgresql://goki:eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJLbUNjUXZsUjJQS253RDhQbU1yREw4dmhuZUtVejJYcURpSHF6bzV3RWFvIn0.eyJleHAiOjE3NjQ4OTE2MjcsImlhdCI6MTc2NDg5MTMyNywiYXV0aF90aW1lIjoxNzY0ODkwOTc0LCJqdGkiOiI5NzRkZTQxZC04OWY3LWNlNmEtNjZiZS1mODUwMTc0MGQzYTciLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODg4OC9yZWFsbXMva2V5Y3JvYWNoIiwiYXVkIjoiY29ja3JvYWNoZGIiLCJzdWIiOiI2NjA2ZDM5Zi1jNDMwLTRhN2QtODQ0YS0zYzU2YmVkYjU0MDAiLCJ0eXAiOiJJRCIsImF6cCI6ImNvY2tyb2FjaGRiIiwic2lkIjoiOGYxZmYxMzItMzVjZi0zMTFiLWY3MGEtMDAyZjg3OWRhMmU2IiwiYXRfaGFzaCI6IkUyaVR6aFJfSmdzbmVGZlJ6M0VTQmciLCJhY3IiOiIwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiRm9vIEJhciIsInByZWZlcnJlZF91c2VybmFtZSI6Imdva2kiLCJnaXZlbl9uYW1lIjoiRm9vIiwiZmFtaWx5X25hbWUiOiJCYXIiLCJlbWFpbCI6Imdva2lAZXhhbXBsZS5jb20ifQ.c7s91bpncxqYTjaYewEVAcNyHCF6e0pwbtNlhrp8oCNGY1uvvBby367LcwPHAZjt_1lCrfOXI2WfRuhl2rMAcxOR-zqxgjUZY4VnTEn-bH6WsP3u5EoEEQ3ZvdoC_yuyfUkv-HJeAkuRe5mVdeGLFOxd4N38blkSFnYXpB589NIhoeM3liR_nryrel348S3GkfKK59q-DRyfxZseSNh2kZS4KEzA6oGIdSvQ20IGGp2O5i3s7ih1Oy89b8uuMh_8vTl5b1yK7KCfta_o1fjel_gYoPXwikKOolAVndbuDsyD4i9ynVugo5m7-84ULzktYOtp6zxskeIK0RUmyJZ4UA@cockroachdb-public:26257?options=--crdb:jwt_auth_enabled=true" --certs-dir=./cockroach-certs # # Welcome to the CockroachDB SQL shell. # All statements must be terminated by a semicolon. # To exit, type: \q. # ERROR: JWT authentication: invalid token SQLSTATE: 28000 DETAIL: unable to parse token: "exp" not satisfied Failed running "sql" command terminated with exit code 1
また、今回検証に利用した JWT の有効期限のデフォルト値は 5分になっているようです。そのあたりの設定は、Keycloak 側の Realm settings -> Tokens -> Access tokens -> Access Token Lifespan の設定で変更できるようです。


おまけ 2
生成された JWT の中身は以下のような感じになってるっぽいです。この辺は詳しくないので、詳細はよくわかってないですが。
{ "alg": "RS256", "typ": "JWT", "kid": "KmCcQvlR2PKnwD8PmMrDL8vhneKUz2XqDiHqzo5wEao" }
{ "exp": 1764891627, "iat": 1764891327, "auth_time": 1764890974, "jti": "974de41d-89f7-ce6a-66be-f8501740d3a7", "iss": "http://keycloak:8888/realms/keycroach", "aud": "cockroachdb", "sub": "6606d39f-c430-4a7d-844a-3c56bedb5400", "typ": "ID", "azp": "cockroachdb", "sid": "8f1ff132-35cf-311b-f70a-002f879da2e6", "at_hash": "E2iTzhR_JgsneFfRz3ESBg", "acr": "0", "email_verified": false, "name": "Foo Bar", "preferred_username": "goki", "given_name": "Foo", "family_name": "Bar", "email": "goki@example.com" }
参考情報
今回は、以下の公式ドキュメントあたりを参考に検証を実施しました。
- CockroachDB
- Keycloak
また、前述した通り検証で利用したマニフェストは、以下の GitHub リポジトリにまとめてあります。ご参考までに。
まとめ
Keycloak (OIDC) を利用した SSO (JWT を使った認証) で CockroachDB に対して SQL CLI で接続し、SQL を実行することができました。なんかモダンな雰囲気 (?) があっていい感じですね。
今回はちょっとした検証だったので CockroachDB の DB Console (GUI) から JWT を取得しましたが、実際のシステム/アプリで利用する場合は (GUI ではなく) API を叩いて Keycloak からいい感じに JWT を取得する感じになる気がします。
まだまだ OIDC とかの認証の仕組みはよくわかってないので、機会があればそのあたりも調査/検証してみようかなぁと思ったりしてます (思ってるだけ)。
Keycloak を使った SSO (OIDC) で CockroachDB の DB Console にログインしてみる
最近 OpenID Connect (OIDC) を使った認証についてちょっと興味があったのですが、「そういえば CockroachDB が OIDC 認証をサポートしてたなぁ~」と思い、検証してみました。また、今回は IdP として Keycloak を利用しています。Keycloak にもちょっと興味があったので。
環境
今回は、以下のような環境 (minikube で作った Kubernetes 上に諸々デプロイ) で検証しています。
バージョン
Kubernetes : v1.33.1 minikube : v1.36.1 Docker : v28.0.4 CockroachDB : v25.3.1 Keycloak : v26.3.3 Helm : v3.18.6 Chart Version (CockroachDB) : v18.0.1 Ubuntu (WSL2 / Windows 10) : 22.04.5 LTS Firefox : 142.0.1 (64 bit)
環境構成概要図
+---[Kubernetes (minikube)]------------------------+
| |
| +---[CockroachDB (Pods)]---------------+ |
| | | |
+---(Access with SSO)-----------+---+-->[DB Console (8080 port)] | |
| | | | |
| | | [SQL Interface (26257 port)]<------+---+ |
| | | | | |
| | +--------------------------------------+ | |
| | | |
| | | |
| | +---[CockroachDB Client (Pod)]---------+ | |
| | | | | |
[Client]---+---(Access with Certificate)---+---+-->[SQL CLI]--------------------------+---+ |
| | | | |
| | +--------------------------------------+ |
| | |
| | |
| | +---[Keycloak (Pods)]------------------+ |
| | | | |
| | | +---[Realm: keycroach]---------+ | |
| | | | | | |
| | | | [Client: cockroachdb] | | |
+---(Authentication)------------+-->| | | | |
| | | [User: goki@example.com] | | |
| | | | | |
| | +------------------------------+ | |
| | | |
| +--------------------------------------+ |
| |
+--------------------------------------------------+
Note:
- SQL CLI は CockroachDB の設定を変更するために利用しており、ここはクライアント証明書を使った認証でアクセスしています。
- Keycloak については全然詳しくないので、Realm / Client / User あたりの位置関係が間違ってるかもしれません... 何か間違ってたらご指摘いただけると嬉しいです!
検証
ということで、さっそく検証してみます。検証で利用するマニフェストは以下の GitHub リポジトリにまとめてあります。
CockroachDB クラスタ構築 (3匹構成)
まずは CockroachDB クラスタをデプロイします。今回は Helm Chart を使って、3匹構成のクラスタをデプロイします。
$ helm repo add cockroachdb https://charts.cockroachdb.com/ "cockroachdb" has been added to your repositories
$ helm install cockroachdb cockroachdb/cockroachdb -f ./cockroachdb/cockroachdb.yaml NAME: cockroachdb LAST DEPLOYED: Wed Sep 10 08:34:19 2025 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES: CockroachDB can be accessed via port 26257 at the following DNS name from within your cluster: cockroachdb-public.default.svc.cluster.local Because CockroachDB supports the PostgreSQL wire protocol, you can connect to the cluster using any available PostgreSQL client. Note that because the cluster is running in secure mode, any client application that you attempt to connect will either need to have a valid client certificate or a valid username and password. Finally, to open up the CockroachDB admin UI, you can port-forward from your local machine into one of the instances in the cluster: kubectl port-forward -n default cockroachdb-0 0 Then you can access the admin UI at https://localhost:0/ in your web browser. For more information on using CockroachDB, please see the project's docs at: https://www.cockroachlabs.com/docs/
デプロイが完了すると、以下の感じになります。
$ kubectl get pod NAME READY STATUS RESTARTS AGE cockroachdb-0 1/1 Running 0 2m9s cockroachdb-1 1/1 Running 0 2m9s cockroachdb-2 1/1 Running 0 2m9s cockroachdb-init-hp59w 0/1 Completed 0 2m9s
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE cockroachdb ClusterIP None <none> 26257/TCP,8080/TCP 2m22s cockroachdb-public LoadBalancer 10.103.247.95 <pending> 26257:31626/TCP,8080:31521/TCP 2m22s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3m50s
ちなみに、helm install コマンドの -f オプションに指定している cockroachdb.yaml の中はこんな感じです。検証用なので、PV は使わないようにしています。また、DB Console にアクセスする際の認証機能は Secure クラスタでのみ有効になるため、TLS を有効にしています。
service: public: type: LoadBalancer storage: persistentVolume: enabled: false tls: enabled: true
Keycloak をデプロイ
次に、Keycloak をデプロイします。こちらは公式ドキュメントの Getting started / Kubernetes あたりを参考にしています (ちょっとだけ設定をいじっています)。
$ kubectl apply -f ./keycloak/keycloak.yaml
service/keycloak created
service/keycloak-discovery created
statefulset.apps/keycloak created
deployment.apps/postgres created
service/postgres created
デプロイが完了すると、以下の感じになります。
$ kubectl get pod NAME READY STATUS RESTARTS AGE cockroachdb-0 1/1 Running 0 13m cockroachdb-1 1/1 Running 0 13m cockroachdb-2 1/1 Running 0 13m cockroachdb-init-hp59w 0/1 Completed 0 13m keycloak-0 1/1 Running 0 3m16s keycloak-1 1/1 Running 0 88s postgres-7867d95d4-tmvqg 1/1 Running 0 3m16s
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE cockroachdb ClusterIP None <none> 26257/TCP,8080/TCP 14m cockroachdb-public LoadBalancer 10.103.247.95 <pending> 26257:31626/TCP,8080:31521/TCP 14m keycloak LoadBalancer 10.100.229.103 <pending> 8888:31670/TCP 3m45s keycloak-discovery ClusterIP None <none> <none> 3m45s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 15m postgres ClusterIP 10.101.204.182 <none> 5432/TCP 3m45s
Note:
- PostgreSQL は Keycloak のデータを保存する DB としてデプロイされているようです。
- Keycloak の Web UI の port は8080がデフォルトのようですが、CockroachDB の DB Console と被ってしまっているので、今回は8888に変更しています。
minikube tunnel を実行
別のターミナルで minikube tunnel コマンドを実行し、CockroachDB の DB Console と Keycloak に localhost (127.0.0.1) 経由でアクセスできるようにします。
$ minikube tunnel ✅ Tunnel successfully started 📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ... 🏃 Starting tunnel for service cockroachdb-public. 🏃 Starting tunnel for service keycloak.
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE cockroachdb ClusterIP None <none> 26257/TCP,8080/TCP 17m cockroachdb-public LoadBalancer 10.103.247.95 127.0.0.1 26257:31626/TCP,8080:31521/TCP 17m keycloak LoadBalancer 10.100.229.103 127.0.0.1 8888:31670/TCP 7m9s keycloak-discovery ClusterIP None <none> <none> 7m9s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 19m postgres ClusterIP 10.101.204.182 <none> 5432/TCP 7m9s
Keycloak でいろいろ設定
Keycloak のデプロイが完了したら、Keycloak 側でいろいろと設定をします。
ブラウザから http://localhost:8888/admin にアクセスします。Username / Password は admin / admin です。

ログインしたら、画面左の Manage realms を選択した後、Create realm を押します。

Create realm 画面で Realm name に keycroach と入力し、Create を選択します。

Realm を作成したら、画面左の Users を選択し、Create new user を押します。

Create user 画面で以下の値入力し、Create ボタンを押します。
Username : goki Email : goki@example.com First name : Foo Last name : Bar

User を作成したら、そのまま Credentials タブを選択し Set password を押します。

Set password 画面で goki ユーザーのパスワードを入力します。また、検証用の一時的なユーザーなので Temporary を Off にした上で、Save を押します。

確認画面が表示されるので、Save password を選択します。

次に、画面左の Clients を選択し、Create client を押します。

Create client 画面で Client type として OpenID Connect を選択した上で、Client ID に cockroachdb と入力し、Next を押します。

次の Capability config 画面では、デフォルト設定のまま特に設定は変えずに、Next を選択します。

最後に、Login settings 画面で以下の値を設定して、Save を押します。https://localhost:8080 は Web ブラウザで CockroachDB の DB Console にアクセスする際の URL です。
Valid redirect URIs : https://localhost:8080/* Web origins : https://localhost:8080/

これで Keycloak 側での設定は終わりです。
CockroachDB でいろいろ設定
次に、CockroachDB 側でいろいろと設定をします。
まずは Client 用の Pod を 1つデプロイして、SQL CLI で CockroachDB に接続します (ここはクライアント証明書を利用した認証で接続しています)。
$ kubectl apply -f ./cockroachdb/client.yaml
pod/cockroachdb-client created
$ kubectl get pod cockroachdb-client NAME READY STATUS RESTARTS AGE cockroachdb-client 1/1 Running 0 24s
$ kubectl exec -it cockroachdb-client -- ./cockroach sql --certs-dir=./cockroach-certs --host=cockroachdb-public # # Welcome to the CockroachDB SQL shell. # All statements must be terminated by a semicolon. # To exit, type: \q. # # Server version: CockroachDB CCL v25.3.1 (x86_64-pc-linux-gnu, built 2025/08/27 20:02:21, go1.23.11 X:nocoverageredesign) (same version as client) # Cluster ID: c550fcdf-24e8-428e-ba96-4873a7183028 # # Enter \? for a brief introduction. # root@cockroachdb-public:26257/defaultdb>
SQL CLI で接続したら、server.oidc_authentication.* の各項目に対して必要な値を設定していきます。
client_id に cockroachdb を設定します。これは、Keycloak 側の Create client 画面で Client ID に入力した値と同じ値を設定する感じになるようです。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.client_id = 'cockroachdb'; SET CLUSTER SETTING Time: 69ms total (execution 65ms / network 4ms)
provider_url に CockroachDB から Keycloak にアクセスする際の URL を設定します。今回 Keycloak 側の TLS は無効なので、scheme は http になっています。また、Kubernetes 内のネットワーク経由で (keycloak という名前の Service リソース を経由して) アクセスするため、ホスト名とポート番号には keycloak:8888 を指定します。加えて、パスの部分は realms/keycroach を指定します。このパスには Keycloak 側の Create realm 画面で Realm name に設定した値を指定する形になります。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.provider_url = 'http://keycloak:8888/realms/keycroach'; SET CLUSTER SETTING Time: 34ms total (execution 34ms / network 0ms)
redirect_url に callback URL を設定します。これは、ユーザーが Keycloak 側で (OIDC で) 認証した後に、CockroachDB 側の DB Console に戻ってくる際の URL のようです。ホスト名とポート番号を今回の環境に合わせて localhost:8080 にしています。また、パスの部分 (/oidc/v1/callback) は固定値のようなので、公式ドキュメントに書いてある値をそのまま指定しています。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.redirect_url = 'https://localhost:8080/oidc/v1/callback'; SET CLUSTER SETTING Time: 12ms total (execution 12ms / network 0ms)
scopes に openid email を設定します。正直この設定は何の設定なのかよくわかっていないのですが、公式ドキュメントに "The openid and email scopes must be included." と記載されているので、それに従って設定しておきます。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.scopes = 'openid email'; SET CLUSTER SETTING Time: 13ms total (execution 13ms / network 0ms)
claim_json_key に email を設定します。今回は Keycloak 側で作成したユーザー goki の email goki@example.com を使ってユーザーを識別するため、email を設定します。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.claim_json_key = 'email'; SET CLUSTER SETTING Time: 13ms total (execution 12ms / network 0ms)
principal_regex をいい感じに設定します。今回は、「ユーザーのメールアドレス goki@example.com からドメイン部分 (@ 以降) を除いたもの」と「DB 側に作成するユーザー (後述する手順で作成する SQL ユーザー)」を紐づける感じの設定になっています。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.principal_regex = '^([^@]+)@example.com$'; SET CLUSTER SETTING Time: 14ms total (execution 14ms / network 0ms)
CockroachDB 側に goki ユーザーを作成します。この goki ユーザーと、Keycloak 側に作成した goki ユーザーを紐づける感じになります。また、Keycloak を利用した SSO でアクセスするため、CREATE USER 文に WITH PASSWORD は指定していません (CockroachDB 側でパスワードは設定していません)。
root@cockroachdb-public:26257/defaultdb> CREATE USER goki; CREATE ROLE Time: 850ms total (execution 850ms / network 0ms)
enabled に true を設定します。これで、ここまでに設定した server.oidc_authentication.* の設定を基に、OIDC 認証が有効になります。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.enabled = true; SET CLUSTER SETTING Time: 13ms total (execution 13ms / network 0ms)
最後に、button_text に Log in with Keycloak という文字列を設定しておきます。この設定はおまけ (OIDC 認証に必須ではない設定) ですが、DB Console で SSO によるログインを実施する際のボタンに表示される文章を任意のものに変更できる感じになっています。
root@cockroachdb-public:26257/defaultdb> SET CLUSTER SETTING server.oidc_authentication.button_text = 'Log in with Keycloak'; SET CLUSTER SETTING Time: 10ms total (execution 10ms / network 0ms)
これで、CockroachDB 側の設定も完了です。
Windows OS 側で hosts を設定 (環境依存の問題対策)
Keycloak / CockroachDB 双方の設定が完了したので、さっそく SSO でのログインを試したいところですが、手元の環境 (Windows OS / WSL2) だと Windows OS 側でも追加で設定が必要でした。
ブラウザ (Firefox) から CockroachDB の DB Console にアクセスし、Keycloak を利用した OIDC 認証でログインする際のクライアント (ブラウザ) 側の動作は、ざっくり言うと以下の感じになるようです。
- CockroachDB の DB Console
https://localhost:8080/にアクセス。 - OIDC (SSO) でのログインを選択すると、Keycloak の URL
http://keycloak:8888/realms/keycroachにリダイレクトされる。 - Keycloak 側で認証が完了すると、改めて CockroachDB の DB Console の URL
https://localhost:8080/oidc/v1/callbackにリダイレクトされる。
この時、「Windows OS 上のブラウザから WSL2 上で listen している CockroachDB の DB Console へのアクセス」は localhost (127.0.0.1) で名前解決して接続できます。
しかし、「Windows OS 上のブラウザから WSL2 上で listen している Keycloak へのアクセス」は、keycloak というホスト名から 127.0.0.1 に名前解決ができないので、接続できません。つまり、上記 2. の部分で Name or service not known となってしまい、ブラウザから Keycloak にアクセスできません。
また、この keycloak というホスト名は CockroachDB 側で設定した server.oidc_authentication.provider_url = 'http://keycloak:8888/realms/keycroach' に依存しているようであり、SSO による認証時の「リダイレクト先のホスト名」のみを変更することはできなさそうでした...
加えて、CockroachDB と Keycloak の間の通信は、Kubernetes 内のネットワークを経由した通信 (以下の Service リソースを利用した通信) になっています。
$ kubectl get svc keycloak NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE keycloak LoadBalancer 10.102.236.44 127.0.0.1 8888:31709/TCP 28s
そのため、CockroachDB 側の設定で server.oidc_authentication.provider_url に http://localhost:8888/realms/keycroach を設定してしまうと、今度は CockroachDB から Keycloak にアクセスできなくなり、認証ができませんでした...
上記の内容を含め、CockroachDB / Keycloak / Kubernetes 側の設定等で (Windows OS / WSL2 側に依存しない形で) いい感じにこの問題を解決できないかを模索してみたのですが、いい感じの方法は見つけられず、最終的に「Windows OS 側の hosts で名前解決できるようにする」という形で対処しました。
ということで、今回の環境では Windows OS 側の hosts に以下の設定を追加しています。
127.0.0.1 keycloak
これで、Windows OS 上のブラウザから http://keycloak:8888/ にアクセスした際に、
WSL2 上の localhost (127.0.0.1) で listen している Keycloak に接続できるようになりました。
Note:
- 通常、OIDC での認証に利用する IdP に対してはグローバルなエンドポイント (ホスト名) で接続できるはずなので、このような問題は発生しないと思います。今回は、CockroachDB および Keycloak (IdP) を両方 localhost 上に構築したことに起因して、このような問題が起きていました。
DB Console にアクセス
必要な設定が全て完了したので、CockroachDB の DB Console に OIDC 認証 (SSO) でログインしてみます。
ブラウザで https://localhost:8080/ にアクセスします。今回は自己証明書を利用しているため、以下のような警告が出てしまいますが、「詳細へ進む...」から「危険性を承知の上で使用」を選択して (警告を無視して) 先に進みます (※ネット上のサイトにアクセスする時は基本やっちゃダメなやつです!)。


警告を無視して先に進むと、CockroachDB の DB Console のログイン画面が表示されます。通常は Log in のボタンしかないのですが、今回は追加で Log in with Keycloak というボタンも表示されているので、こちらの Log in with Keycloak を選択します。

Log in with Keycloak を選択すると、Keycloak 側のログイン画面にリダイレクトされ、Username と Password の入力を求められるので、Keycloak 側の Users ページで作成した User のメールアドレス goki@example.com と Credentials 画面で設定したパスワードをそれぞれ入力し、Keycloak 側で認証します。

Keycloak 側での認証が完了すると、そのまま CockroachDB の DB Console に再度リダイレクトされ、無事 DB Console にアクセスできました。

参考情報
今回は、以下の公式ドキュメントあたりを参考に検証を実施しました。
- CockroachDB
- Keycloak
また、前述した通り検証で利用したマニフェストは、以下の GitHub リポジトリにまとめてあります。ご参考までに。
まとめ
Keycloak (OIDC) を利用した SSO で CockroachDB の DB Console にログインすることができました。なんかモダンな雰囲気 (?) があって面白いですね。
ただ、とりあえず動きはしたものの、正直 OIDC とかの認証の仕組みは良くわかってないので、機会があればいろいろと調べてみようと思います (思ってるだけ)。
また、CockroachDB では、DB Console へのログインだけでなく、SQL の実行も SSO で認証 (JTW を使って認証) できるようなので、そちらも追加で検証してみようと思います。
[2025/12/05 追記] 続きを書きました。
Keycloak を使った SSO (OIDC/JWT) で CockroachDB に SQL CLI で接続してみる
Kubernetes 上に pgvector 入りの PostgreSQL を シュッ とデプロイしたい 🐘
お仕事で pgvector を触ることになったのですが、検証用の pgvector 環境を Kubernetes 上に シュッ とデプロイできるようにしたかったので、いい感じの container image を作ってみました。
背景
普段は local 環境の Kubernetes (kind or minikube) 上に Bitnami の Helm Chart を使って PostgreSQL デプロイしているのですが、今のところ Bitnami の Helm Chart で公式に「pgvecor 入りの PostgreSQL をデプロイする方法」は提供されてなさそうでした。
いろいろ調べてみたところ、pgvector in Bitnami Postgres という issue があったので、この issue の内容を基に Dockerfile を作成し GitHub Packages に container image を置いておくことにしました。
使い方
Bitnami の Helm Chart 用の values file で image の設定を変更します。独自にカスタムした container image を使う場合は global.security.allowInsecureImages に true を設定する必要もあるっぽいです。一応、普段私が検証用に使ってる sample も GitHub 上に置いておきました。
global: security: allowInsecureImages: true image: registry: "ghcr.io" repository: "kota2and3kan/bitnami-postgresql-pgvector" tag: "17"
values file にて container image の取得元の設定を追加したら、以下の感じのコマンドで Kubernetes 上にデプロイします。
helm install postgresql-pgvector \ --set auth.postgresPassword=postgres \ --values postgresql.yaml \ oci://registry-1.docker.io/bitnamicharts/postgresql
$ kubectl get pod NAME READY STATUS RESTARTS AGE postgresql-pgvector-0 1/1 Running 0 12s
PostgreSQL が起動したら、 以下のような感じで 以下のような感じで CREATE EXTENSION を実行し pgvector を有効にします。\dx を実行し pgvector が有効になっていることを確認します。 [2025/03/21 更新]
$ kubectl exec -it postgresql-pgvector-0 -- psql -U postgres -c '\dx vector'
Password for user postgres: ********
List of installed extensions
Name | Version | Schema | Description
--------+---------+--------+------------------------------------------------------
vector | 0.8.0 | public | vector data type and ivfflat and hnsw access methods
(1 row)
これで、なんかいい感じに Vector 型のデータを INSERT したり SELECT できるようになりました。
$ kubectl exec -it postgresql-pgvector-0 -- psql -U postgres Password for user postgres: ******** psql (17.4) Type "help" for help. postgres=# postgres=# CREATE TABLE foo (id INT, vector_data VECTOR(3)); CREATE TABLE postgres=# postgres=# INSERT INTO foo VALUES (1, '[1,2,3]'), (2, '[4,5,6]'), (3, '[7,8,9]'); INSERT 0 3 postgres=# postgres=# SELECT * FROM foo; id | vector_data ----+------------- 1 | [1,2,3] 2 | [4,5,6] 3 | [7,8,9] (3 rows) postgres=# postgres=# SELECT * FROM foo ORDER BY vector_data <-> '[1,2,3]'; id | vector_data ----+------------- 1 | [1,2,3] 2 | [4,5,6] 3 | [7,8,9] (3 rows) postgres=# postgres=# SELECT * FROM foo ORDER BY vector_data <#> '[1,2,3]'; id | vector_data ----+------------- 3 | [7,8,9] 2 | [4,5,6] 1 | [1,2,3] (3 rows)
まとめ
実は Vector Store とか AI/LLM 周りのお話は微妙に逃げていたのですが、いよいよ逃げられなくなってきた感じがしてます。せっかく検証環境を シュッ と作れるようになったので、pgvector を中心に Vector Store とか AL/LLM まわりの勉強もしていこうかなと思います。思ってるだけ。
あと、PostgreSQL 起動時に自動で CREATE EXTENSION vector を実行する方法を探してみたのですが、いい感じの方法が見つかりませんでした... (´・ω・`)
PostgreSQL をデプロイした後に毎回 CREATE EXTENSION vector を実行しないといけないのがちょっとめんどくさいなぁ、と思っているのですが、どなたか良い感じの方法をご存じであれば教えていただけると嬉しいです!
[2025/03/21 追記]
X (旧 Twitter) でいい感じの情報を教えていただいた結果、PostgreSQL 起動時に自動で CREATE EXTENSION vector が実行できるようになりました!ありがとうございます!
https://x.com/tzkb/status/1902604481147080859