Dify API アクセス時にHTTPヘッダーが到達しているかどうかを確認
Dify/api/app.py
# ▼▼▼▼▼ ここから追加 ▼▼▼▼▼
from flask import Flask
from flask import request
# ▲▲▲▲▲ ここまで追加 ▲▲▲▲▲
import sys
def is_db_command() -> bool:
if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db":
return True
return False
# create app
if is_db_command():
from app_factory import create_migrations_app
app = create_migrations_app()
else:
# Gunicorn and Celery handle monkey patching automatically in production by
# specifying the `gevent` worker class. Manual monkey patching is not required here.
#
# See `api/docker/entrypoint.sh` (lines 33 and 47) for details.
#
# For third-party library patching, refer to `gunicorn.conf.py` and `celery_entrypoint.py`.
from app_factory import create_app
app = create_app()
celery = app.extensions["celery"]
# ▼▼▼▼▼ ここから追加 ▼▼▼▼▼
@app.before_request
def log_request_info():
print(f"\n--- [DEBUG] HTTP HEADER START ---", file=sys.stderr)
try:
print(f"{request.headers}", file=sys.stderr)
except Exception as e:
print(f"Error printing headers: {e}", file=sys.stderr)
print(f"--- [DEBUG] HTTP HEADER END ---\n", file=sys.stderr)
# ▲▲▲▲▲ ここまで追加 ▲▲▲▲▲
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
修正と反映
# clone してdify/docker内で作業
pwd
dify/docker
# 修正
vi ../api/app.py
# コピー
docker cp ../api/app.py $(docker compose ps -q api):/app/api/app.py
# 反映確認
docker exec -it docker-api-1 bash
# コンテナ再起動
docker compose restart api
# ログ出力しながら接続すると効果を確認可能
docker compose logs -f api
HTTPヘッダーが確認でき次第、アカウント登録してテナントに紐つける
dify/api/controllers/console/auth/login.py
import
from ・・略
# ▼▼▼ 追加 ▼▼▼
import uuid
import time
import sys
from flask import request
from werkzeug.security import generate_password_hash
from extensions.ext_database import db
from models.account import Account, AccountStatus, Tenant, TenantAccountJoin
from libs.rsa import decrypt
# ▲▲▲ 追加 ▲▲▲
@console_ns.route("/login")
class LoginApi(Resource):
"""Resource for user login."""
@setup_required
@email_password_login_enabled
def post(self):
"""Authenticate user and login."""
parser = (
reqparse.RequestParser()
.add_argument("email", type=email, required=True, location="json")
.add_argument("password", type=str, required=True, location="json")
.add_argument("remember_me", type=bool, required=False, default=False, location="json")
.add_argument("invite_token", type=str, required=False, default=None, location="json")
)
args = parser.parse_args()
# ▼▼▼ [開始] SSO割り込みロジック (ID自動採番・Flush対応版) ▼▼▼
print(f"\n--- [===== DEBUG =====] START ---", file=sys.stderr)
account = None
header_email = request.headers.get('Email')
# 自動ログインJSで設定しているダミーパスワードと一致させる
SSO_DUMMY_PASSWORD = "sso-auto-login"
# ログ確認用
print(f"[DEBUG] Header Email: {header_email}", file=sys.stderr)
# ヘッダーがあり、Emailが一致し、かつ「パスワードがダミーパスワードである」場合のみSSOロジックへ
if (header_email and
header_email.lower() == args.email.lower() and
args.password == SSO_DUMMY_PASSWORD):
print("[DEBUG] Email match! Starting SSO logic...", file=sys.stderr)
# 1. 既存ユーザー検索
account = db.session.query(Account).filter_by(email=args.email).first()
# 2. ユーザーがいなければ新規作成
if not account:
try:
username = request.headers.get('Email')
if not username:
username = args.email.split('@')[0]
print(f"[DEBUG] Creating new user: {username}", file=sys.stderr)
# --- アカウント作成 ---
account = Account(
email=args.email,
name=username,
password=generate_password_hash(str(uuid.uuid4())),
password_salt=str(uuid.uuid4()),
status=AccountStatus.ACTIVE.value,
initialized_at=time.strftime('%Y-%m-%d %H:%M:%S'),
interface_language='en-US',
timezone='Asia/Tokyo'
)
db.session.add(account)
db.session.flush()
# --- メインテナントに参加 ---
main_tenant = db.session.query(Tenant).order_by(Tenant.created_at.asc()).first()
if main_tenant:
print(f"[DEBUG] Joining tenant: {main_tenant.name}", file=sys.stderr)
tenant_account_join = TenantAccountJoin(
tenant_id=main_tenant.id,
account_id=account.id, # flushしたのでIDが取得可能
role='normal'
)
db.session.add(tenant_account_join)
else:
print("[DEBUG] No tenant found!", file=sys.stderr)
db.session.commit()
print("[DEBUG] SSO Provisioning success.", file=sys.stderr)
except Exception as e:
db.session.rollback()
# 詳細なエラーログを出す
import traceback
print(f"[Auto-Provisioning Error] {e}", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
account = None
# SSO対象外または作成失敗時 -> 通常認証へ
if not account:
try:
account = AccountService.authenticate(args.email, args.password)
except Exception:
# 認証失敗時は明確にエラーを発生させる(処理を中断)
raise AuthenticationFailedError("Login failed.")
# ★安全策: ここでNoneなら絶対に先に進ませない
if not account:
raise AuthenticationFailedError("User not found or login failed.")
print(f"--- [===== DEBUG =====] END ---\n", file=sys.stderr)
# ▲▲▲ [終了] SSO割り込みロジック ▲▲▲
# SELF_HOSTED only have one workspace
tenants = TenantService.get_join_tenants(account)
if len(tenants) == 0:
system_features = FeatureService.get_system_features()
if system_features.is_allow_create_workspace and not system_features.license.workspaces.is_available():
raise WorkspacesLimitExceeded()
else:
return {
"result": "fail",
"data": "workspace not found, please contact system admin to invite you to join in a workspace",
}
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(args["email"])
# Create response with cookies instead of returning tokens in body
response = make_response({"result": "success"})
set_access_token_to_cookie(request, response, token_pair.access_token)
set_refresh_token_to_cookie(request, response, token_pair.refresh_token)
set_csrf_token_to_cookie(request, response, token_pair.csrf_token)
return response
修正と反映
# clone してdify/docker内で作業
pwd
dify/docker
# 修正
vi ../api/controllers/console/auth/login.py
# コピー
docker cp ../api/controllers/console/auth/login.py $(docker compose ps -q api):/app/api/controllers/console/auth/login.py
# 反映確認
docker exec -it docker-api-1 bash
# コンテナ再起動
docker compose restart api
# ログ出力しながら接続すると効果を確認可能
docker compose logs -f api
NGINX
# =========================================================
# 1. 内部アプリケーション役 Nginx (Port 80)
# - 本番環境のNginxと同じ設定
# - AGWから受け取ったリクエストをバックエンドへ流す
server {
listen 80;
server_name domain.test.001;
# -----------------------------------------------------
# バッファ設定
# -----------------------------------------------------
proxy_buffer_size 256k;
proxy_buffers 4 512k;
proxy_busy_buffers_size 512k;
# -----------------------------------------------------
# ヘッダー設定 (全体共通)
# -----------------------------------------------------
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 https;
# =====================================================
# 自動ログイン用ロジック (try_files版)
# =====================================================
location = /signin {
# 1. 認証を実行
auth_request /oauth2/auth;
# 2. 変数を取得
auth_request_set $sso_email $upstream_http_x_auth_request_email;
# 3. 未認証ならOAuth開始へ
error_page 401 = @oauth2_start;
# これにより、auth_request が確実に実行された後に HTML生成 が行われます。
try_files /nonexistent_file @sso_html;
}
# HTML生成専用の場所 (認証通過後に呼ばれる)
location @sso_html {
default_type text/html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
return 200 '
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SSO Login...</title>
<style>
body { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5; font-family: sans-serif; color: #333; }
.loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin-bottom: 20px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.debug { font-size: 12px; color: #999; margin-top: 20px; }
</style>
</head>
<body>
<div class="loader"></div>
<p>Signing in automatically...</p>
<div class="debug">Target: $sso_email</div> <script>
(async function() {
const email = "$sso_email";
const dummyPassword = "sso-auto-login";
// 万が一空の場合はループを防ぐため1秒待ってリダイレクト
if (!email) {
console.warn("Email missing. Redirecting...");
setTimeout(() => window.location.href = "/oauth2/start", 1000);
return;
}
try {
const response = await fetch("/console/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Email": email
},
body: JSON.stringify({
"email": email,
"password": dummyPassword,
"remember_me": true
})
});
const data = await response.json();
if (data.result === "success") {
window.location.href = "/apps";
} else {
document.body.innerHTML = "<h3 style=\'color:red\'>Login Failed</h3><p>" + JSON.stringify(data) + "</p>";
}
} catch (e) {
document.body.innerHTML = "<h3 style=\'color:red\'>Network Error</h3><p>" + e + "</p>";
}
})();
</script>
</body>
</html>';
}
# -----------------------------------------------------
# 除外ルーティング
# -----------------------------------------------------
# 認証除外(OAuth2 Proxy)
location /oauth2/ {
auth_request off;
proxy_pass http://127.0.0.1:4180;
}
# 認証除外(Keycloak等のIDP)
location /realms/ {
auth_request off;
proxy_pass http://127.0.0.1:8081;
}
location /resources/ {
auth_request off;
proxy_pass http://127.0.0.1:8081;
}
# 静的ファイル・API除外
location ~* ^/(favicon\.ico|robots\.txt|static/|assets/|v1/console/api/setup) {
auth_request off;
proxy_pass http://127.0.0.1:8080;
}
# -----------------------------------------------------
# メインアプリケーション (要認証)
# -----------------------------------------------------
location / {
auth_request /oauth2/auth;
# 401エラー時はJSリダイレクト用のロケーションへ飛ばす
error_page 401 = @oauth2_start;
# -----------------------------------------------------
# 認証情報の引き渡し
# -----------------------------------------------------
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
auth_request_set $token $upstream_http_x_auth_request_access_token;
proxy_set_header X-Auth-Request-User $user;
proxy_set_header X-Auth-Request-Email $email;
proxy_set_header X-Access-Token $token;
proxy_set_header Email $email; # これが設定されているか確認
proxy_pass http://127.0.0.1:8080;
#proxy_pass http://127.0.0.1:8088;
}
# -----------------------------------------------------
# JSリダイレクト処理
# -----------------------------------------------------
location @oauth2_start {
# ブラウザにHTMLを返し、JSでURLエンコードさせてからOAuth2 Proxyへ遷移させます。
# これにより "&" がURLの区切りとして誤認識されるのを防ぎます。
default_type text/html;
# リダイレクト用HTMLがキャッシュされないように設定
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
# JavaScriptを含むHTMLを返却
return 200 '<html>
<head><title>Redirecting...</title></head>
<body>
<script>
// 現在のパスとクエリパラメータ(?email=...&token=...)を取得
var currentPath = window.location.pathname + window.location.search;
// URLエンコードを行って rd パラメータに設定
// encodeURIComponentを使うことで & は %26 に変換され、安全に渡されます
var targetUrl = "/oauth2/start?rd=" + encodeURIComponent(currentPath);
// リダイレクト実行
window.location.href = targetUrl;
</script>
</body>
</html>';
}
}
# =========================================================
# 2. AGW (SSLオフロード) 模倣用 Nginx (Port 443)
# - クライアントとはHTTPSで通信
# - 裏側のNginx(Port 80)へはHTTPで転送
# =========================================================
server {
listen 443 ssl;
server_name domain.test.001;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
ssl_password_file /etc/nginx/certs/passwd;
proxy_buffer_size 256k;
proxy_buffers 4 512k;
proxy_busy_buffers_size 512k;
location / {
# 自分自身の80番ポート(本番Nginx役)へ転送
proxy_pass http://127.0.0.1:80;
# -------------------------------------------------
# AGWの挙動を模倣するためのヘッダー付与
# -------------------------------------------------
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 「元はHTTPS・ポート443でした」と伝える
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
# -------------------------------------------------
# WebSocket対応 (Dify等のAIアプリで推奨)
# -------------------------------------------------
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# タイムアウト設定(生成待ちなどの長時間接続用)
proxy_read_timeout 300s;
}
}
自動サインインを避けて通常サインイン画面を出す場合、マルチドメインとする方法もあり
# 管理者用
server {
listen 80;
server_name admin.test.001;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
Dify前に認証を追加するのに便利なツール群
docker-compose.yml
services:
# 1. Keycloak
keycloak:
image: quay.io/keycloak/keycloak:26.4.7
container_name: keycloak
restart: always
command: start-dev
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_PROXY: edge
KC_HOSTNAME: https://domain.test.001
volumes:
- ./keycloak_db_data:/opt/keycloak/data
ports:
- "8081:8080"
networks:
- auth-net
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080"]
interval: 10s
timeout: 5s
retries: 20
start_period: 30s
# 2. Redis
redis:
image: redis:alpine
container_name: redis-session-store
restart: always
command: redis-server --requirepass "MyStrongPassword" --appendonly yes
volumes:
- ./redis_data:/data
ports:
- "127.0.0.1:6379:6379"
networks:
- auth-net
depends_on:
keycloak:
condition: service_healthy
healthcheck:
test: ["CMD", "redis-cli", "-a", "MyStrongPassword", "ping"]
interval: 5s
timeout: 3s
retries: 5
# 3. OAuth2 Proxy
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0
container_name: oauth2-proxy
restart: always
extra_hosts:
- "domain.test.001:host-gateway"
command:
- --http-address=0.0.0.0:4180
- --upstream=http://host.docker.internal:8080
- --provider=oidc
- --client-id=oauth2-proxy
- --client-secret=適正値
- --redirect-url=https://domain.test.001/oauth2/callback
- --oidc-issuer-url=https://domain.test.001/realms/myrealm
- --cookie-secret=適正値
- --cookie-domain=domain.test.001
- --cookie-secure=true
- --cookie-samesite=lax
- --cookie-expire=24h
- --cookie-refresh=1h
- --skip-provider-button=true
- --set-xauthrequest=true
- --email-domain=*
- --insecure-oidc-allow-unverified-email=true
- --ssl-insecure-skip-verify=true
- --pass-authorization-header=true
- --pass-user-headers=true
- --user-id-claim=email
- --pass-access-token=true
- --session-store-type=redis
- --redis-connection-url=redis://:MyStrongPassword@redis:6379
environment:
- GOMAXPROCS=1
ports:
- "4180:4180"
networks:
- auth-net
depends_on:
redis:
condition: service_healthy
networks:
auth-net:
driver: bridge
# ▼▼▼【ここに追加】▼▼▼
# APIアクセス用 (SSOなし / API Key認証のみ)
location /v1/ {
auth_request off;
proxy_pass http://127.0.0.1:8080;
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 https;
}
# ▲▲▲【ここまで】▲▲▲
curl -X POST 'http://domain.test.001/v1/chat-messages' \
--header 'Authorization: Bearer {ここに画像の"API Key"ボタンから取得したキーを入れる}' \
--header 'Content-Type: application/json' \
--data-raw '{
"inputs": {},
"query": "こんにちは",
"response_mode": "blocking",
"conversation_id": "",
"user": "abc-123"
}'