{
  "name": "AAD(n8n 自驗)→ level 授權路由 → Dify RAG (sample)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ask-rag",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "b1a11111-1111-1111-1111-111111111111",
      "name": "Webhook(agent 直連入口)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [220, 300],
      "webhookId": "ask-rag"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $env.ENTRA_JWKS_URL }}",
        "options": {}
      },
      "id": "a0a00000-0000-0000-0000-000000000000",
      "name": "取 Entra JWKS(公鑰)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, 300]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// n8n 自己驗 AAD:抓 Entra JWKS 對應 kid 的公鑰驗簽 → 檢查 exp/aud → 讀 roles 路由。\n// ⚠️ 需在 n8n 設 env:NODE_FUNCTION_ALLOW_BUILTIN=crypto\nconst crypto = require('crypto');\nconst req = $('Webhook(agent 直連入口)').first().json;\nconst token = (req.headers?.authorization || '').replace(/^Bearer\\s+/i, '');\nif (!token) { throw new Error('缺 Authorization Bearer token'); }\nconst [h, p, s] = token.split('.');\nconst header  = JSON.parse(Buffer.from(h, 'base64url').toString());\nconst payload = JSON.parse(Buffer.from(p, 'base64url').toString());\n\n// 1) 簽章:用 Entra JWKS 裡 kid 對應的公鑰驗\nconst jwks = $('取 Entra JWKS(公鑰)').first().json.keys || [];\nconst jwk = jwks.find(k => k.kid === header.kid);\nif (!jwk) { throw new Error('找不到對應公鑰(kid),token 可疑'); }\nconst pub = crypto.createPublicKey({ key: jwk, format: 'jwk' });\nconst ok = crypto.verify('RSA-SHA256', Buffer.from(h + '.' + p), pub, Buffer.from(s, 'base64url'));\nif (!ok) { throw new Error('JWT 簽章無效'); }\n\n// 2) 基本 claim 檢查(依你的 tenant / API 調整)\nif ((payload.exp || 0) * 1000 < Date.now()) { throw new Error('token 已過期'); }\nif ($env.EXPECTED_AUD && payload.aud !== $env.EXPECTED_AUD) { throw new Error('aud 不符,不是給這個 API 的 token'); }\n\n// 3) roles → level → 對應的 Dify key\nconst roles = payload.roles || [];\nlet level = 'general', keyEnv = 'DIFY_KEY_GENERAL';\nif (roles.includes('RAG.Sensitive'))    { level = 'sensitive'; keyEnv = 'DIFY_KEY_SENSITIVE'; }\nelse if (roles.includes('RAG.Manager')) { level = 'manager';   keyEnv = 'DIFY_KEY_MANAGER'; }\n\nreturn [{ json: {\n  level,\n  user: payload.preferred_username || payload.oid || 'agent',\n  query: req.body?.query || '',\n  difyKey: $env[keyEnv],\n  difyUrl: ($env.DIFY_BASE || 'https://dify.internal') + '/v1/chat-messages'\n} }];"
      },
      "id": "c2a22222-2222-2222-2222-222222222222",
      "name": "n8n 自驗 AAD + 依 level 挑 key(PEP)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [680, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.difyUrl }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Authorization", "value": "=Bearer {{ $json.difyKey }}" },
            { "name": "Content-Type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"inputs\": {},\n  \"query\": {{ JSON.stringify($json.query) }},\n  \"response_mode\": \"blocking\",\n  \"user\": {{ JSON.stringify($json.user) }}\n}",
        "options": {}
      },
      "id": "d3a33333-3333-3333-3333-333333333333",
      "name": "打對應的 Dify app(REST API,內部)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [920, 300]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { level: $('n8n 自驗 AAD + 依 level 挑 key(PEP)').first().json.level, answer: $json.answer } }}"
      },
      "id": "e4a44444-4444-4444-4444-444444444444",
      "name": "回傳",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [1160, 300]
    }
  ],
  "connections": {
    "Webhook(agent 直連入口)": { "main": [[{ "node": "取 Entra JWKS(公鑰)", "type": "main", "index": 0 }]] },
    "取 Entra JWKS(公鑰)": { "main": [[{ "node": "n8n 自驗 AAD + 依 level 挑 key(PEP)", "type": "main", "index": 0 }]] },
    "n8n 自驗 AAD + 依 level 挑 key(PEP)": { "main": [[{ "node": "打對應的 Dify app(REST API,內部)", "type": "main", "index": 0 }]] },
    "打對應的 Dify app(REST API,內部)": { "main": [[{ "node": "回傳", "type": "main", "index": 0 }]] }
  },
  "settings": {},
  "meta": {
    "_note": "agent 直連 n8n,n8n 自己驗 AAD(不需 gateway)。n8n env 需要:NODE_FUNCTION_ALLOW_BUILTIN=crypto、ENTRA_JWKS_URL(https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys)、EXPECTED_AUD、DIFY_BASE、DIFY_KEY_GENERAL/MANAGER/SENSITIVE。正式建議 JWKS 快取,不要每次打。"
  }
}
