TL;DR
- Dify Community 版は SSO 非対応。手前に nginx + oauth2-proxy + Keycloak を置いて解決しました
- Keycloak が EntraID に認証を委譲(ID フェデレーション) し、oauth2-proxy が認証状態を検証、nginx の
auth_requestで未認証のリクエストを拒否します - 前編のゴールは 「EntraID でログインできる人なら Dify を開ける」まで(=認証)。「どの部署がどの Dify に入れるか(=認可)」は後編で扱います
- 注意点として、Community 版ではこの後に Dify 自身のログインが残ります(二重ログイン)
対象読者・前提
- Docker Compose に慣れたインフラ / バックエンド寄りの方
- 前提として次が揃っていること
- Dify が docker compose で起動済み(本記事では Dify の nginx がホストの
8080番で待ち受けている想定) - EntraID (Azure AD) テナントの管理権限(アプリ登録ができる)
- 手前に nginx を1つ立てられる環境
- Dify が docker compose で起動済み(本記事では Dify の nginx がホストの
なお、前段の TLS 終端やインターネット公開(証明書の取得や Cloudflare Tunnel など)は別記事 とし、前編はローカル検証(自己署名証明書 + hosts)で通します。ドメインは example.com、auth.example.com を Keycloak、app.example.com を Dify とします。
完成形
まず全体像です。ブラウザからのアクセスは、必ず手前の nginx を通ります。
役割分担はこうです。
| コンポーネント | 役割 |
|---|---|
| front-nginx | リバースプロキシ。TLS を終端し、auth_request により各リクエストの認証状態を oauth2-proxy に問い合わせる |
| oauth2-proxy | OIDC の RP(Relying Party / クライアント)。セッション cookie を検証し、未認証のリクエストを Keycloak の認可エンドポイントへリダイレクトする |
| Keycloak | OpenID Provider(IdP)兼 ID ブローカー。EntraID を上流の IdP として OIDC 連携し、認証を委譲する |
| Dify | 保護対象のアプリケーション。手前で認証済みのリクエストのみを受け取る |
ポイントは役割分担です。oauth2-proxy はセッション cookie を検証して認証状態を判定するのみで、未認証であれば Keycloak の認可エンドポイントへリダイレクトします。ユーザーの認証は Keycloak が担い、Keycloak 自身はその認証を上流の EntraID に委譲します(ID フェデレーション)。
1. なぜ nginx + oauth2-proxy + Keycloak なのか
何が困るのか?
Dify Community 版には SSO 機能がありません。つまり URL を知っていれば誰でもログイン画面にたどり着けます。社内利用ではここに認証を挟みたくなります。
手前で認証を挟むという発想
そこで、Dify のアプリ自体には手を入れず、手前の nginx で未認証のリクエストを拒否することにします。nginx の auth_request は、本来のリクエストを転送する前にサブリクエストを別のエンドポイントへ送り、その応答(200 か 401 か)で転送可否を決める仕組みです。この認証状態の判定を oauth2-proxy が行い、認証そのものは Keycloak(および委譲先の EntraID)が担います。
先に正直な注意:二重ログイン
この方式で守れるのは 「Dify に到達できるかどうか」 です。到達したあとに Dify 自身のログイン画面が別途出ます。つまり Community 版では EntraID ログイン → Dify ログインの二重ログインになります。ここは仕様上避けられないので、最初に共有しておきます(単一ログインが必須なら Dify の Enterprise 版 SSO を検討することになります)。
2. 全体像とリクエストの流れ
未ログインのユーザーが https://app.example.com/ を開いたときの流れです。
一度ログインすれば cookie が付くので、以降は auth_request が 200 を返し、そのまま Dify に通ります。
前編で作るのはここまで、「EntraID でログインできれば通る」認証です。「特定の部署だけ通す」認可は後編で足します。
3. Keycloak を立てる
このステップのゴール: Keycloak を起動し、専用の realm(テナントのような区画)を作る。
Keycloak 本体と、その専用 Postgres を docker compose で立てます。秘匿値は .env に置きます。
# docker-compose.yml(Keycloak 部分)
services:
keycloak:
image: quay.io/keycloak/keycloak:26.1
command: ["start"]
environment:
# 管理者の初期パスワード(Keycloak 26 のブートストラップ用)
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
# DB
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
# 前段の nginx で TLS を終端するので、Keycloak 自体は http で動かし公開ホスト名だけ教える
KC_HOSTNAME: https://auth.example.com
KC_HTTP_ENABLED: "true"
KC_PROXY_HEADERS: xforwarded
# oauth2-proxy が内部 http://keycloak:8080 でトークンを取りに来られるようにする
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
KC_HEALTH_ENABLED: "true"
depends_on:
keycloak-db:
condition: service_healthy
networks: [gateway]
keycloak-db:
image: postgres:16
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
volumes:
- keycloak-pgdata:/var/lib/postgresql/data
networks: [gateway]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 5s
timeout: 5s
retries: 10
volumes:
keycloak-pgdata:
networks:
gateway:
ここで少し勘所を補足します。
-
KC_HOSTNAME: https://auth.example.com: 前段の nginx で TLS を終端するため、Keycloak が発行する URL(リダイレクト先など)を公開ホスト名で固定します。 -
KC_PROXY_HEADERS: xforwarded: 手前の nginx が付けるX-Forwarded-*を信頼し、実際は https で来ていると認識させます。これがないと「http で来ているのに https を期待している」といったズレでログインループになりがちです。 -
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true": 表向きの URL はhttps://auth.example.comのままにしつつ、oauth2-proxy が内部ネットワークのhttp://keycloak:8080でトークンや鍵を取りに来るのを許可します。フロント(ブラウザ)とバックチャネル(サーバー間)で経路が違うための設定です。
起動して、realm を作ります。realm 名は tenant1 とします。Keycloak には kcadm.sh という管理 CLI が同梱されているので、コンテナ内で叩きます。
# .env を読み込んでおく
set -a; . ./.env; set +a
GW="docker compose"
# 管理者としてログイン(トークンはコンテナ内に保存される)
$GW exec -T keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 --realm master \
--user admin --password "$KEYCLOAK_ADMIN_PASSWORD"
# realm を作成
$GW exec -T keycloak /opt/keycloak/bin/kcadm.sh create realms \
-s realm=tenant1 -s enabled=true
確認方法: ブラウザで https://auth.example.com/(ローカルなら hosts に登録)を開き、管理コンソールに admin でログインできれば OK です。
4. EntraID と連携する
このステップのゴール: Keycloak に EntraID を外部 IdP として登録し、Keycloak のログイン画面から EntraID に飛んで戻ってこられるようにする。
Azure ポータル側(アプリ登録)
- Azure ポータル > アプリの登録 > 新規登録
- リダイレクト URI(Web)に次を登録します。これは Keycloak が EntraID から認可コードを受け取る URL です。
https://auth.example.com/realms/tenant1/broker/entraid/endpoint -
証明書とシークレット > 新しいクライアント シークレットを作成し、値を控えます(
ENTRA_CLIENT_SECRET)。 - 概要ページの アプリケーション (クライアント) ID(
ENTRA_CLIENT_ID)と ディレクトリ (テナント) ID(ENTRA_TENANT_ID)を控えます。
Keycloak 側(外部 IdP の登録)
控えた3つの値を .env に入れ、kcadm で EntraID を OIDC の外部 IdP として登録します。alias=entraid が、先ほどのリダイレクト URI の .../broker/entraid/endpoint と対応します。
$GW exec -T keycloak /opt/keycloak/bin/kcadm.sh create identity-provider/instances -r tenant1 \
-s alias=entraid \
-s displayName="Microsoft EntraID" \
-s providerId=oidc \
-s enabled=true \
-s trustEmail=true \
-s "config.clientId=${ENTRA_CLIENT_ID}" \
-s "config.clientSecret=${ENTRA_CLIENT_SECRET}" \
-s "config.issuer=https://login.microsoftonline.com/${ENTRA_TENANT_ID}/v2.0" \
-s "config.authorizationUrl=https://login.microsoftonline.com/${ENTRA_TENANT_ID}/oauth2/v2.0/authorize" \
-s "config.tokenUrl=https://login.microsoftonline.com/${ENTRA_TENANT_ID}/oauth2/v2.0/token" \
-s "config.jwksUrl=https://login.microsoftonline.com/${ENTRA_TENANT_ID}/discovery/v2.0/keys" \
-s "config.userInfoUrl=https://graph.microsoft.com/oidc/userinfo" \
-s "config.useJwksUrl=true" \
-s "config.defaultScope=openid profile email" \
-s "config.clientAuthMethod=client_secret_post" \
-s "config.syncMode=FORCE"
確認方法: https://auth.example.com/realms/tenant1/account を開くと Keycloak のログイン画面が出ます。「Microsoft EntraID」ボタンから EntraID にログインし、Keycloak に戻ってこられれば連携成功です。
5. oauth2-proxy と nginx で認証を強制する
ここが前編の山場です。このステップのゴール: oauth2-proxy を立て、nginx の auth_request で未認証のリクエストを拒否する。
Keycloak に oauth2-proxy 用のクライアントを作る
oauth2-proxy が Keycloak にログインを依頼するための「クライアント」を登録します。
# クライアント作成(id を控える)
CID=$($GW exec -T keycloak /opt/keycloak/bin/kcadm.sh create clients -r tenant1 \
-s clientId=oauth2-proxy-dify \
-s enabled=true -s protocol=openid-connect \
-s publicClient=false -s standardFlowEnabled=true \
-s 'redirectUris=["https://app.example.com/oauth2/callback"]' \
-s 'webOrigins=["https://app.example.com"]' -i | tr -d '\r')
# クライアントシークレットを設定して取得
$GW exec -T keycloak /opt/keycloak/bin/kcadm.sh update clients/$CID -r tenant1 \
-s "secret=${OAUTH2_PROXY_CLIENT_SECRET}"
oauth2-proxy を立てる
# docker-compose.yml(oauth2-proxy 部分)
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
command:
- --http-address=0.0.0.0:4180
- --reverse-proxy=true
- --provider=oidc
- --skip-oidc-discovery=true
# 表向きの issuer は https://auth.example.com、トークン取得は内部 http://keycloak:8080
- --oidc-issuer-url=https://auth.example.com/realms/tenant1
- --login-url=https://auth.example.com/realms/tenant1/protocol/openid-connect/auth
- --redeem-url=http://keycloak:8080/realms/tenant1/protocol/openid-connect/token
- --oidc-jwks-url=http://keycloak:8080/realms/tenant1/protocol/openid-connect/certs
- --profile-url=http://keycloak:8080/realms/tenant1/protocol/openid-connect/userinfo
- --client-id=oauth2-proxy-dify
- --client-secret=${OAUTH2_PROXY_CLIENT_SECRET}
- --redirect-url=https://app.example.com/oauth2/callback
- --cookie-domain=app.example.com
- --whitelist-domain=app.example.com
- --cookie-secret=${OAUTH2_PROXY_COOKIE_SECRET}
- --cookie-secure=true
- --email-domain=*
- --scope=openid email profile
- --set-xauthrequest=true
- --skip-provider-button=true
# 認証判定だけが目的なので、自分自身は中身を返さない
- --upstream=static://200
networks: [gateway]
--cookie-secret は 16 / 24 / 32 バイトのいずれかでなければなりません(後述のハマりどころ参照)。次のように 32 文字の hex で作ります。
openssl rand -hex 16 # 32文字 = 32バイト
前編では「EntraID でログインできれば通す」ところまでなので、
--allowed-group(特定グループだけ許可する設定)はあえて付けていません。ここを足して「特定部署だけ」に絞るのが後編です。
nginx の設定
http {} に共通のプロキシ設定を置き、server {} で auth.example.com(Keycloak)と app.example.com(Dify)を振り分けます。
# nginx.conf(http ブロックの要点)
http {
# WebSocket 用
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
include /etc/nginx/conf.d/*.conf;
}
# conf.d/auth.conf — Keycloak を auth.example.com で公開
server {
listen 443 ssl;
http2 on;
server_name auth.example.com;
ssl_certificate /etc/nginx/certs/tls.crt;
ssl_certificate_key /etc/nginx/certs/tls.key;
location / {
proxy_pass http://keycloak:8080;
}
}
# conf.d/app.conf — app.example.com を認証付きで Dify に通す
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/nginx/certs/tls.crt;
ssl_certificate_key /etc/nginx/certs/tls.key;
# oauth2-proxy 自身のエンドポイント(サインイン・コールバック)
location /oauth2/ {
proxy_pass http://oauth2-proxy:4180;
}
# auth_request の問い合わせ先。本文は送らない
location = /oauth2/auth {
proxy_pass http://oauth2-proxy:4180;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
# 機械系エンドポイントは SSO を課さず素通し(Dify 内の API キーで認証されるため)
location ~ ^/(v1|triggers|e|mcp)(/|$) {
proxy_pass http://host.docker.internal:8080;
}
# 対話系:ログインしていなければ /oauth2/sign_in へ飛ばす
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
proxy_pass http://host.docker.internal:8080;
}
}
補足を2点だけ。
-
location = /oauth2/auth:auth_requestはここに問い合わせます。ログイン済みなら 200、未ログインなら 401 が返り、401 のときerror_page 401 = /oauth2/sign_inでログインへ誘導します。 -
機械系
/v1などを素通しにする理由: Dify の API(外部連携やワークフローの Webhook 等)は Dify 内部の API キーや署名で認証されます。ここに対話用の SSO を課すと API が使えなくなるため、正規表現で/v1/triggers/e/mcpとその配下だけを認証なしで通しています。
front-nginx は Dify(ホストの 8080)へ host.docker.internal 経由で到達させるため、compose に次を足します。
front-nginx:
image: nginx:1.27-alpine
ports: ["443:443"]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks: [gateway]
TLS 証明書はローカル検証なら自己署名で十分です(本番の証明書と公開は別記事)。
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout certs/tls.key -out certs/tls.crt \
-subj "/CN=example.com" \
-addext "subjectAltName=DNS:auth.example.com,DNS:app.example.com"
ローカルなら /etc/hosts に次を足しておきます。
127.0.0.1 auth.example.com app.example.com
確認方法: 未ログインの状態で app.example.com にアクセスすると、Keycloak のログインへリダイレクトされます。curl で 302 を確認できます。
$ curl -sk -o /dev/null -w "%{http_code} -> %{redirect_url}\n" \
-H "Host: app.example.com" https://localhost/
302 -> https://auth.example.com/realms/tenant1/protocol/openid-connect/auth?...
6. 通しで動作確認
ブラウザで https://app.example.com/ を開くと、次の順に進みます。
- Keycloak のログイン画面 →「Microsoft EntraID」でログイン
- EntraID の認証
- Keycloak に戻り、Dify に到達
- Dify 自身のログイン画面(=二重ログインの実挙動)
Dify の管理者が未作成なら、ここで管理者を登録します。Dify は初回セットアップに INIT_PASSWORD(Dify 側の .env で設定した値)を要求します。これを入力し、続けて管理者のメールとパスワードを作れば Dify を使い始められます。
これで 「EntraID でログインできる人だけが Dify に到達できる」 状態になりました。
7. ハマりどころ
実際に構築する中で踏んだものを、症状 → 原因 → 対処で残します。
cookie_secret must be 16, 24, or 32 bytes
oauth2-proxy が起動直後に crash し、ログに cookie_secret must be 16, 24, or 32 bytes to create an AES cipher, but is 44 bytes と出ました。
openssl rand -base64 32 の出力は 44 文字で、oauth2-proxy はこの文字列長を鍵長として見ます。44 は 16/24/32 のどれでもないため弾かれていました。openssl rand -hex 16(= 32 文字) に変えて解決しました。
ログインがループする / issuer 不一致
oauth2-proxy が期待する issuer(https://auth.example.com/realms/tenant1)と、実際に Keycloak が名乗る issuer がずれるとループします。ポイントは フロントは https://auth.example.com、バックチャネルは内部 http://keycloak:8080 を使い分けることです。Keycloak 側は KC_HOSTNAME と KC_HOSTNAME_BACKCHANNEL_DYNAMIC、oauth2-proxy 側は --oidc-issuer-url と --redeem-url / --oidc-jwks-url の組み合わせで、この使い分けを成立させています。
二重ログインは仕様
繰り返しになりますが、Community 版では nginx 手前の EntraID 認証の後に Dify 自身のログインが残ります。これは Community 版の仕様上の制約であり、前段の認証(到達制御)と Dify のログイン(アプリ内認証)は別レイヤーとして扱う前提で設計します。単一ログインが要件であれば Enterprise 版の SSO が必要です。
8. まとめと後編
前編では、Dify Community 版の手前に nginx + oauth2-proxy + Keycloak を置き、EntraID でログインできる人だけが Dify に到達できるところまでを作りました。
- nginx の
auth_requestで未認証のリクエストを拒否する - oauth2-proxy が OIDC の RP としてセッション(認証状態)を管理する
- Keycloak が EntraID に認証を委譲する(ID フェデレーション)
一方で、今の状態では 「EntraID にログインできる人なら誰でも」 通ってしまいます。実運用では「営業部はこの Dify、開発部はあの Dify」というように 部署ごとにアクセスを絞りたい はずです。
後編では、Keycloak のグループ(/tenant1-<チーム>)と oauth2-proxy の --allowed-group を使い、さらに EntraID の App ロール / グループから自動でアクセスを割り当てるところまで(=認可)を扱います。