コンテンツにスキップ

Keycloak@セキュリティ系ミドルウェア

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたする。


01. Keycloakとは

アプリケーションに代わって、認証/認可処理を実行する。

認証/認可に関する API を公開し、認証時のアカウントの CRUD や、認可時のアカウントに対する権限スコープ付与ができる。


01-02. Keycloakの仕組み

アーキテクチャ

Keycloak は、認証処理サービス、Infinispan、アカウント管理用の RDBMS、といったコンポーネントから構成されている。

keycloak_architecture


認証処理サービス

ID プロバイダーとして、DB からセッションデータを取得し、認証処理を実施する。


Infinispan

Keycloak からセッションデータを取得し、DB に永続化する。


RDBMS

セッションデータを保管する。


01-03. Keycloakの拡張性設計

クラスタリング設計

Keycloak では、クラスタリング構成を使用できる。

Keycloak クラスターでは、JGroups は Infinispan クラスターインスタンス間でレプリケーション通信 (例:PING、TCPPING、JDBC_PING、DNS_PING、KUBE_PING など) を実施する。

レプリケーション通信によって、Keycloak クラスター内の Infinispan クラスターインスタンス間でセッションデータを同期する。

keycloak_clustering


クラスターインスタンス検出方法

▼ 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とクライアントシークレットの場合

記入中...


ユースケース

▼ 認証マイクロサービスとして

記入中...


▼ OIDCの場合

項目 説明
SSOのセッションアイドル ブラウザを操作しなかった場合に、再認証が必要になる有効期限を表す、
アクセストークンのライフスパン OIDCのアクセストークンの有効期限を表す。


02-03. 認可

リソース

認可スコープの適用するエンドポイントを設定する。

keycloak_authorization


パーミッション

リソースとポリシーの紐付けを設定する。

keycloak_authorization


ポリシー

認可スコープを設定する。

keycloak_authorization


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 パラメーターの値