0
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 のデバッグメモ

Last updated at Posted at 2026-01-17

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"
}'
0
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
0
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?