Keycloak@セキュリティ系ミドルウェア¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたする。
01. Keycloak とは¶
アプリケーションに代わって、認証/認可処理を実行する。
認証/認可に関する API を公開し、認証時のアカウントの CRUD や、認可時のアカウントに対する権限スコープ付与ができる。
01-02. Keycloak の仕組み¶
アーキテクチャ¶
Keycloak は、認証処理サービス、Infinispan、アカウント管理用の RDBMS、といったコンポーネントから構成されている。

認証処理サービス¶
ID プロバイダーとして、DB からセッションデータを取得し、認証する。
Infinispan¶
Keycloak からセッションデータを取得し、DB に永続化する。
RDBMS¶
セッションデータを保管する。
01-03. Keycloak の拡張性設計¶
クラスタリング設計¶
Keycloak では、クラスタリング構成を使用できる。
Keycloak クラスターでは、JGroups は Infinispan クラスターインスタンス間でレプリケーション通信 (例:PING、TCPPING、JDBC_PING、DNS_PING、KUBE_PING など) を実施する。
レプリケーション通信によって、Keycloak クラスター内の Infinispan クラスターインスタンス間でセッションデータを同期する。

クラスターインスタンス検出方法¶
▼ TCPPING¶
固定された IP とポート番号を宛先情報として使用する。
7800 番ポート (以前は 7600 番だった) を使用し、TCP プロトコルのレプリケーション通信を実施する。
▼ JDBC_PING¶
動的な IP アドレスとポート番号を宛先情報として使用する。
クラスターインスタンスは自身の宛先情報をサービスレジストリに登録する (セルフ登録パターン) 。
7800 番 (以前は 7600 番だった) と 57800 番のポートを使用し、TCP プロトコルのレプリケーション通信を実施する。
▼ DNS_PING¶
ドメインレジストラ (例:CoreDNS) 内の A レコードや SVC レコードを宛先情報として使用する。
ドメインレジストラはクラスターインスタンスの宛先情報をサービスレジストリに登録する (サードパーティ登録パターン) 。
7800 番ポート (以前は 7600 番だった) を使用し、TCP プロトコルのレプリケーション通信を実施する。
- Kubernetes 環境で Headless Service を作成する
- Keycloak の環境変数を設定する
- KC_CACHE_STACK=kubernetes
- JAVA_OPTS_APPEND=-Djgroups.dns.query=
▼ KUBE_PING¶
サービスレジストリ (例:Etcd) 内の宛先情報として使用する。
性能設計¶
▼ 性能設計¶
Keycloak は CPU とメモリを使用する。
▼ CPU¶
最大リクエスト数 (1 秒間にどのくらいのリクエストを処理できるのか) に影響する。
設定例
- 1 秒あたり 24 回のログインリクエスト ➡️ 3 vCPU
- 1 秒あたり 450 回のクライアント資格情報の付与処理 ➡️ 1 vCPU
- 350 回のリクエストのリフレッシュトークン ➡️ 1 vCPU
▼ メモリ¶
最大セッション数 (最大何人が同時にログイン状態になれるのか) に影響する。
設定例
1000 MB を設定し、余剰分を増設する。
- 50000 のアクティブセッション用 ➡️ 余剰 250 MB
02. SSO¶
SSO の種類¶
▼ OIDC の場合¶
- 認可コードフロー (標準フロー)
- 暗黙的フロー
02-02. 認証¶
Realm¶
Keycloak では、Admin アカウントの認証は master realm で、それ以外はユーザー定義の realm、で管理する。
master realm でログイン後、ユーザー定義の realm を作成するとよい。
認証アーティファクトの伝播方法¶
▼ クライアントシークレットの場合¶
記入中...
▼ X509 証明書の場合¶
記入中...
▼ JWT の場合¶
Keycloak クライアントは、『ヘッダー』『ペイロード』『署名』のそれぞれの JSON 型データを base64 方式によってエンコードし、ドットでつなぐ。
これらの処理によって、JWT を作成する。
その後、Keycloak の認可エンドポイントに JWT を送信する。
JWT とクライアントシークレットの場合¶
記入中...
ユースケース¶
▼ 認証マイクロサービスとして¶
記入中...
セッション、Cookie、アクセストークンの有効期限¶
▼ OIDC の場合¶
| 項目 | 説明 |
|---|---|
| SSO のセッションアイドル | ブラウザを操作しなかった場合に、再認証が必要になる有効期限を表す、 |
| アクセストークンのライフスパン | OIDC のアクセストークンの有効期限を表す。 |
02-03. 認可¶
リソース¶
認可スコープの適用するエンドポイントを設定する。

パーミッション¶
リソースとポリシーの紐付けを設定する。

ポリシー¶
認可スコープを設定する。

03. エンドポイント¶
OIDC¶
▼ ディスカバリーエンドポイント¶
すべての設定を取得できる。
事前に作成したユーザー定義の realm を設定する。
/realms/<realm名>/.well-known/openid-configuration
$ curl https://<Keycloakのドメイン名>/realms/<realm名>/.well-known/openid-configuration
{
"issuer": "https://<Keycloakのドメイン名>/realms/<realm名>",
"authorization_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/auth",
"token_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/token",
"introspection_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/userinfo",
"end_session_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/logout",
"frontchannel_logout_session_supported": true,
"frontchannel_logout_supported": true,
"jwks_uri": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/certs",
"check_session_iframe": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/login-status-iframe.html",
"registration_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/clients-registrations/openid-connect",
"revocation_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/revoke",
"device_authorization_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/auth/device",
"backchannel_authentication_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/ext/ciba/auth",
"pushed_authorization_request_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/ext/par/request",
"mtls_endpoint_aliases": {
"token_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/token",
"revocation_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/revoke",
"introspection_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/token/introspect",
"device_authorization_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/auth/device",
"registration_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/clients-registrations/openid-connect",
"userinfo_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/userinfo",
"pushed_authorization_request_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/ext/par/request",
"backchannel_authentication_endpoint": "https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/ext/ciba/auth"
},
...
}
▼ / (issuer エンドポイント)¶
JWT トークンの発行元 ID プロバイダーの識別子である。
クライアント側では authority 値として指定する。
GET https://<Keycloakのドメイン名>/realms/<realm名>
▼ /auth (認可エンドポイント)¶
アプリケーションがブラウザ経由で接続するエンドポイントである。
Keycloak のほかのエンドポイントとは異なり、インターネットから接続できるように公開する必要がある。
GET https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/auth
▼ /certs (JWKs エンドポイント)¶
アクセストークンの署名を検証する。
(イントロスペクションエンドポイントとの違いがややこしい)
GET https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/certs
▼ /introspect (イントロスペクションエンドポイント)¶
アクセストークンの有効期限が失効しているかどうかを検証する。
(JWKs エンドポイントとの違いがややこしい)
POST https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/token/introspect
▼ /token¶
フローに応じたトークン (アクセストークン、ID トークン) や認可コードを取得できる。
また、Token Exchange を使用すると新しいトークンに交換したり、別の ID プロバイダーのトークンに変換できる。
なお、Keycloak は JWT 仕様のアクセストークンを採用している。
GET https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/token
▼ /userinfo¶
クレームを取得できる。
GET https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/userinfo
▼ /logout¶
認証を意図的に無効化する。
フロントチャネルとバックチャネルのエンドポイントがある。
GET https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/logout?id_token_hint=<IDトークン>&post_logout_redirect_uri=<クライアントシークレット>
# state、ui_locakesが必要な場合もある
POST https://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/logout?client_id=<クライアントID>&client_secret=<クライアントシークレット>&refresh_token=<リフレッシュトークン>
04. SLO:シングルログアウト¶
バックチャネル¶
▼ ID プロバイダーへのリクエスト¶
アプリケーションは、ID プロバイダーのログアウトエンドポイント (/logout) に POST リクエストを送信する。
# リクエスト
# ID プロバイダーのログアウトエンドポイント
POST /realms/<realm名>/protocol/openid-connect/logout HTTP/1.1
---
Host: <Keycloakのドメイン名>
Content-Type: application/x-www-form-urlencoded
Content-Length: 759
---
client_id=python-client&client_secret=a07f9...8213d1&refresh_token=eyJhbGci...twOA
| パラメーター | 説明 |
|---|---|
client_id |
クライアント ID |
client_secret |
クライアントシークレット |
refresh_token |
リフレッシュトークン |
▼ アプリケーションへのリクエスト¶
ID プロバイダーは、バックエンドアプリケーションのバックチャネルログアウトエンドポイント (/k_logout) に POST リクエストを送信する。
すべてのアプリケーションに対して、この処理を繰り返す。
# リクエスト
# バックエンドアプリケーションのバックチャネルログアウトエンドポイント
POST /k_logout HTTP/1.1
---
Host: localhost:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 759
---
logout_token=eyJhbGciOiJSUzI1NiIs...zspo4weMQfU-1jL0DxSg
POST リクエストには、JWT トークン (たぶん ID トークン) が含まれている。
ID トークンには、アプリケーション間で共有しているクライアントのセッション ID が含まれてする。
Keycloak は、このセッション ID でログアウトすべきクライアントを判定し、認証処理を実行する。
{
"id": "edfd2bf0-1f2d-4875-a4b1-2752caa07ee1-1606363972255",
"expiration": 1606364002,
"resource": "kc-tomcat",
"action": "LOGOUT",
# アプリケーション間で共有しているクライアントのセッション ID
"adapterSessionIds": ["FC60BED115518DFB043EDDB77F0E0A8E"],
"notBefore": 0,
"keycloakSessionIds": ["ac04ef9d-7793-481c-a5c7-5750560e3c14"],
}
▼ ID プロバイダーからのレスポンス¶
ID プロバイダーのログアウトエンドポイントは、アプリケーションにレスポンスを送信する。
# レスポンス
HTTP/1.1 204 No Content
フロントチャネル¶
▼ ID プロバイダーへのリクエスト¶
ブラウザは、ID プロバイダーのログアウトエンドポイント (/logout) に GET リクエストを送信する。
# リクエスト
# ID プロバイダーのログアウトエンドポイント
GET http://<Keycloakのドメイン名>/realms/<realm名>/protocol/openid-connect/logout?id_token_hint=eyJhbGciOiJS...RE2AZmGgKJAj-HlHw&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauth%2Flogout%2Fcomplete&state=e18689b0503aab42574427fb575645aca0065bb758aa8463acf4506fe8a61e81
| パラメーター | 説明 |
|---|---|
id_token_hint |
ID トークン |
post_logout_redirect_uri |
ログアウト後のリダイレクト URL |
state |
CSRF 対策の文字列 |
▼ ID プロバイダーからのレスポンス¶
ID プロバイダーのログアウトエンドポイントは、ブラウザにレスポンスを送信する。
# レスポンス
HTTP/1.1 307 Temporary Redirect
http://localhost:8000/logout/complete?state=e18689b0503aab42574427fb575645aca0065bb758aa8463acf4506fe8a61e81
| パラメーター | 説明 |
|---|---|
state |
リクエスト時の state パラメーターの値 |