前編 では、Dify Community 版の手前に nginx + oauth2-proxy + Keycloak を置き、EntraID で認証できたユーザーなら Dify に到達できる ところまで作りました。ただしこの状態では、EntraID テナントにログインできる人であれば誰でも通ってしまいます。
実運用では「営業部はこの Dify、開発部はあの Dify」というように、部署ごとにアクセスを制御したい はずです。後編ではこの認可(Authorization)を、Keycloak のグループと EntraID の連携で実装します。
TL;DR
- 認可は 「Keycloak のグループ
/<team>に所属しているか」 で判定し、oauth2-proxy の--allowed-groupで強制します -
EntraID → Keycloak グループの自動割り当て は、Keycloak の IdP マッパー(
oidc-advanced-group-idp-mapper)で行います。EntraID のライセンスで2パターンに分かれます-
Microsoft Entra ID P1 以上: App ロール(
rolesクレーム) -
Free(P1 なし): セキュリティグループ(
groupsクレーム=グループの Object ID)
-
Microsoft Entra ID P1 以上: App ロール(
-
syncMode=FORCEにより、ログインのたびに所属が 自動で付与・剥奪 されます(人事異動は EntraID 側の変更だけで完結) - ハマりどころ: 403 の原因、
kcadm -sではconfig.claimsの JSON が壊れる(-f -で回避)、groupsクレームはグループ名ではなく Object ID を使用
前提
- 前編の構成(nginx + oauth2-proxy + Keycloak + EntraID 連携)が動いていること
- Keycloak の realm 名は
tenant1、Dify インスタンスに対応するチーム名をdifyとします
認可の設計:グループ = インスタンス
方針はシンプルで、Dify インスタンス1つにつき Keycloak グループを1つ 用意します。
- Keycloak グループ
/dify= 「この Dify に入れる人の集合」 - oauth2-proxy は
--allowed-group=/difyを持ち、トークンのgroupsクレームにこのグループが含まれるか を検証する - 「誰がそのグループに入るか」は、EntraID の App ロールまたはセキュリティグループから自動で決める
認可時の流れは次のとおりです。
1. oauth2-proxy に認可を足す
このステップのゴール: グループ /dify に所属する人だけを通す。
Keycloak グループを作る
set -a; . ./.env; set +a
KC() { docker compose exec -T keycloak /opt/keycloak/bin/kcadm.sh "$@"; }
KC config credentials --server http://localhost:8080 --realm master \
--user admin --password "$KEYCLOAK_ADMIN_PASSWORD"
# インスタンスに対応するグループ
KC create groups -r tenant1 -s name=dify
上の KC() は、Keycloak の管理 CLI である kcadm.sh を Keycloak コンテナ内で実行するためのショートハンド(シェル関数)です。以降の KC <サブコマンド> は、すべて docker compose exec -T keycloak /opt/keycloak/bin/kcadm.sh <サブコマンド> に展開されます(前編で使った kcadm.sh と同じものです)。
最初の KC config credentials は管理者としてのログインです。取得したトークンはコンテナ内に保存されるため、以降の KC create ... などで認証を毎回指定する必要はありません。この記事のコード例はいずれも、この KC() を定義してログイン済みである前提です。
クライアントに groups クレームを出すマッパーを付ける
oauth2-proxy がグループを検証できるよう、クライアント oauth2-proxy-dify に グループメンバーシップを groups クレームとして出力する マッパーを付けます。full.path=true により /dify という フルパス で出力されます。
# クライアントの id を取得
CID=$(KC get clients -r tenant1 -q clientId=oauth2-proxy-dify --fields id --format csv | tr -d '"')
KC create clients/$CID/protocol-mappers/models -r tenant1 \
-s name=groups \
-s protocol=openid-connect \
-s protocolMapper=oidc-group-membership-mapper \
-s 'config."full.path"=true' \
-s 'config."claim.name"=groups' \
-s 'config."id.token.claim"=true' \
-s 'config."access.token.claim"=true' \
-s 'config."userinfo.token.claim"=true'
oauth2-proxy に --allowed-group を追加
前編の oauth2-proxy の起動オプションに、次の1行を足します。
- --allowed-group=/dify
これで「認証は通ったがグループに入っていない」リクエストは 403 になります。
確認方法: この時点では、まだ誰も /dify に所属していません。ログインすると 403 になるはずです(次のステップで所属を割り当てます)。
2. EntraID → グループの自動割り当て(2パターン)
「誰が /dify に入るか」を EntraID 側から自動で決めます。EntraID のライセンスにより方法が2つに分かれるため、まず自分のテナントがどちらかを確認します。
P1 の有無を確認する
Microsoft Entra 管理センター > 概要 を開くと、ライセンス種別(Microsoft Entra ID Free / P1 / P2)が表示されます。判断が難しければ、エンタープライズ アプリケーション > 対象アプリ > ユーザーとグループ > 追加 で、グループを割り当てられれば P1 以上、ユーザー個別しか選べなければ Free です。
パターン B:Microsoft Entra ID P1 以上 → App ロール(推奨)
割り当てを EntraID の1画面に集約でき、アクセスレビューなどのガバナンス機能も使えます。
-
アプリ登録 > アプリ ロール で、ロール(値
dify)を定義します。 -
エンタープライズ アプリケーション > ユーザーとグループ で、部署のセキュリティグループを App ロールに割り当てます。
→ トークンのrolesクレームにdifyが含まれるようになります。
パターン A:Free → セキュリティグループ
App ロールへのグループ割り当てが使えない場合は、グループ所属を groups クレームで判定します。
-
アプリ登録 > トークン構成 で、
groupsクレームを発行します(種類=セキュリティ グループ)。
→ トークンのgroupsクレームに、グループの Object ID(GUID) が含まれます(グループ名ではありません)。 - 対象部署グループの Object ID を控えます(グループ > 概要)。
3. Keycloak に IdP マッパーを作る
このステップのゴール: EntraID のクレーム値を、Keycloak グループ /dify に対応付ける。
使うのは oidc-advanced-group-idp-mapper です。ここで config.claims の指定方法に注意 が必要です(詳細は後述のハマりどころ)。kcadm -s config.claims=[...] は docker compose exec 経由でクォートが壊れて Cannot parse the JSON になるため、マッパー全体を JSON ボディとして -f -(標準入力)で投入 します。
パターン B(App ロール、claim=roles)の場合:
cat <<'JSON' | KC create identity-provider/instances/entraid/mappers -r tenant1 -f -
{
"name": "dify-from-roles-dify",
"identityProviderAlias": "entraid",
"identityProviderMapper": "oidc-advanced-group-idp-mapper",
"config": {
"syncMode": "FORCE",
"group": "/dify",
"are.claim.values.regex": "false",
"claims": "[{\"key\":\"roles\",\"value\":\"dify\"}]"
}
}
JSON
パターン A(セキュリティグループ、claim=groups)の場合は、claims の key を groups、value を グループの Object ID にします。
"claims": "[{\"key\":\"groups\",\"value\":\"11111111-2222-3333-4444-555555555555\"}]"
syncMode=FORCE により、ログインのたびにこのマッパーが再評価 されます。該当クレームを持つユーザーは /dify に自動で追加され、割り当てが外れたユーザーは自動で除去されます。
確認方法: 対象ユーザーで再ログインすると、今度は 403 にならず Dify に到達します。
4. 動作確認
-
グループ未所属のユーザー: EntraID の認証自体は成功しますが、oauth2-proxy の
--allowed-group検証で弾かれ 403 になります。oauth2-proxy のログにはInvalid authentication via OAuth2: unauthorizedが出ます。 - グループ所属のユーザー: Dify に到達します(その先の Dify 自身のログインは前編のとおり残ります)。
5. 多対多・剥奪・手動割り当て
複数部署に同じインスタンスを許可する
同一インスタンスに複数部署を通したい場合は、部署ごとにマッパーを追加します(マッパー名を値ごとに一意にします)。
# 営業部・開発部の両方に dify を許可(App ロール例)
# 上記 JSON の name と claims の value を部署ごとに変えて追加
剥奪
対応するマッパーを削除すれば、以降のログインでその部署は所属を得られなくなります。すでに所属済みのユーザーは、次回ログイン時(syncMode=FORCE)に除去されます。即時に剥奪したい場合は、Keycloak でグループのメンバーを手動削除します。
手動割り当て(暫定・例外運用)
マッパーを使わず、特定ユーザーだけを通したい場合は、直接グループに所属させます。
U=$(KC get users -r tenant1 -q email=user@example.com --fields id --format csv | tr -d '"')
G=$(KC get groups -r tenant1 --fields id,name --format csv | grep '"dify"' | cut -d, -f1 | tr -d '"')
KC update users/$U/groups/$G -r tenant1 -s realm=tenant1 -s userId=$U -s groupId=$G -n
6. ハマりどころ
403 の正体
認証は成功しているのに 403 になる場合、原因は グループ未所属 です。oauth2-proxy のログに Invalid authentication via OAuth2: unauthorized が出ていれば、--allowed-group の検証で拒否されています。対象ユーザーを /dify に所属させれば解決します。
kcadm -s では config.claims の JSON が壊れる
oidc-advanced-group-idp-mapper の config.claims はオブジェクト配列の JSON です。これを kcadm -s "config.claims=[{...}]" で渡すと、docker compose exec の層でクォートが崩れ Cannot parse the JSON になります。前述のとおり、マッパー全体を JSON ボディにして -f -(標準入力)で投入 すれば確実です。
groups クレームはグループ名ではなく Object ID
パターン A(セキュリティグループ)で groups クレームに載るのは、グループ名ではなく Object ID(GUID) です。マッパーの value には Object ID を指定してください。グループ名を入れても一致しません。
複数インスタンス運用時の cookie 名の衝突
oauth2-proxy の cookie 名は既定で _oauth2_proxy です。複数インスタンスで共有したまま、親ドメインにまたがる --cookie-domain を設定すると、cookie がサブドメインへ漏れて同名で衝突し、認証が不安定になります。--cookie-name=_oauth2_proxy_<team> のようにインスタンスごとに一意化しておくと、構造的に回避できます。
まとめ
前編と後編で、次の状態まで到達しました。
- 前編(認証): EntraID で認証できたユーザーだけが Dify に到達できる
- 後編(認可): さらに、特定の部署(Keycloak グループ)に所属するユーザーだけ に絞れる
「人 ↔ 部署」は EntraID 側の割り当てだけで管理でき、Keycloak と oauth2-proxy の設定は各インスタンスで固定です。組織変更に強い構成になりました。
なお、この前段(nginx + oauth2-proxy + Keycloak)を インターネットに公開する方法(TLS・ドメイン)は別記事 にまとめています。ポート開放や固定 IP なしで正規の TLS 証明書で公開する方法として、Cloudflare Tunnel を使った手順を解説しています。