1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dify Community版を nginx + oauth2-proxy + Keycloak で EntraID 認証に対応する(後編)

1
Last updated at Posted at 2026-07-09

前編 では、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)
  • 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画面に集約でき、アクセスレビューなどのガバナンス機能も使えます。

  1. アプリ登録 > アプリ ロール で、ロール(値 dify)を定義します。
  2. エンタープライズ アプリケーション > ユーザーとグループ で、部署のセキュリティグループを App ロールに割り当てます。
    → トークンの roles クレームに dify が含まれるようになります。

パターン A:Free → セキュリティグループ

App ロールへのグループ割り当てが使えない場合は、グループ所属を groups クレームで判定します。

  1. アプリ登録 > トークン構成 で、groups クレームを発行します(種類=セキュリティ グループ)。
    → トークンの groups クレームに、グループの Object ID(GUID) が含まれます(グループ名ではありません)。
  2. 対象部署グループの 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)の場合は、claimskeygroupsvalueグループの 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-mapperconfig.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 を使った手順を解説しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?