Dify Outlook 插件 OAuth 重定向 URI 问题完整解决方案
问题描述
在使用 Dify 的 Outlook 插件时,遇到 Azure AD OAuth 认证错误:
AADSTS90102: 'redirect_uri' value must be a valid absolute URI.
问题分析
经过排查发现,Dify 的 Outlook 插件在 OAuth 认证过程中,传入的 redirect_uri 是相对路径:
/console/api/oauth/plugin/langgenius/outlook/outlook/tool/callback
但 Azure AD 要求 redirect_uri 必须是完整的绝对 URI,如:
http://localhost/console/api/oauth/plugin/langgenius/outlook/outlook/tool/callback
解决方案
方案 1:修改 Outlook 插件源码(推荐)
文件位置:dify-main/services/app/tools/outlook/outlook.py
修改内容:在 OutlookProvider 类中添加 URL 转换方法
import os
import time
from typing import Any, Mapping
import requests
import secrets
import urllib.parse
from dify_plugin import ToolProvider
from dify_plugin.errors.tool import ToolProviderCredentialValidationError
from dify_plugin.entities.oauth import ToolOAuthCredentials
class OutlookProvider(ToolProvider):
_SCOPE = "Mail.Read Mail.Send Mail.ReadWrite offline_access"
def _ensure_absolute_redirect_uri(self, redirect_uri: str) -> str:
"""确保 redirect_uri 是绝对路径"""
if redirect_uri.startswith('/'):
# 从环境变量读取基础 URL,默认为 http://localhost
base_url = os.getenv('DIFY_BASE_URL', 'http://localhost')
# 确保没有重复的斜杠
if base_url.endswith('/'):
base_url = base_url.rstrip('/')
if redirect_uri.startswith('/'):
redirect_uri = redirect_uri.lstrip('/')
return f"{base_url}/{redirect_uri}"
return redirect_uri
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
"""Validate access token by calling Microsoft Graph API."""
if not credentials.get("access_token"):
raise ToolProviderCredentialValidationError("Microsoft Graph access token is required.")
headers = {"Authorization": f"Bearer {credentials['access_token']}"}
response = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers, timeout=30)
if response.status_code != 200:
raise ToolProviderCredentialValidationError("Invalid or expired access token.")
def _oauth_get_authorization_url(self, redirect_uri: str, system_credentials: Mapping[str, Any]) -> str:
"""Generate OAuth authorization URL."""
# 确保 redirect_uri 是绝对路径
redirect_uri = self._ensure_absolute_redirect_uri(redirect_uri)
tenant_id = system_credentials.get("tenant_id", "common")
auth_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize"
params = {
"client_id": system_credentials["client_id"],
"redirect_uri": redirect_uri,
"scope": system_credentials.get("scope", self._SCOPE),
"response_type": "code",
"state": secrets.token_urlsafe(16)
}
return f"{auth_url}?{urllib.parse.urlencode(params)}"
def _oauth_get_credentials(
self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Any
) -> ToolOAuthCredentials:
"""Exchange authorization code for access token."""
# 确保 redirect_uri 是绝对路径
redirect_uri = self._ensure_absolute_redirect_uri(redirect_uri)
# Get authorization code
code = request.args.get("code")
if not code:
raise ToolProviderCredentialValidationError("No authorization code provided")
# Exchange code for token
tenant_id = system_credentials.get("tenant_id", "common")
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
data = {
"client_id": system_credentials["client_id"],
"client_secret": system_credentials["client_secret"],
"code": code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code"
}
response = requests.post(token_url, data=data, timeout=30)
if response.status_code != 200:
raise ToolProviderCredentialValidationError(f"Token exchange failed: {response.text}")
token_data = response.json()
access_token = token_data.get("access_token")
refresh_token = token_data.get("refresh_token")
if not access_token or not refresh_token:
raise ToolProviderCredentialValidationError("No access token or refresh token in response")
return ToolOAuthCredentials(
credentials={"access_token": access_token, "refresh_token": refresh_token},
expires_at= token_data.get("expires_in", 3599) + int(time.time())
)
def oauth_refresh_credentials(
self, redirect_uri: str, system_credentials: Mapping[str, Any], credentials: Mapping[str, Any]
) -> ToolOAuthCredentials:
"""Refresh OAuth credentials."""
# 确保 redirect_uri 是绝对路径
redirect_uri = self._ensure_absolute_redirect_uri(redirect_uri)
tenant_id = system_credentials.get("tenant_id", "common")
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
data = {
"client_id": system_credentials["client_id"],
"client_secret": system_credentials["client_secret"],
"refresh_token": credentials.get("refresh_token"),
"redirect_uri": redirect_uri,
"grant_type": "refresh_token"
}
response = requests.post(token_url, data=data, timeout=30)
if response.status_code != 200:
raise ToolProviderCredentialValidationError(f"Token exchange failed: {response.text}")
token_data = response.json()
access_token = token_data.get("access_token")
refresh_token = token_data.get("refresh_token")
if not access_token or not refresh_token:
raise ToolProviderCredentialValidationError("No access token or refresh token in response")
return ToolOAuthCredentials(
credentials={"access_token": access_token, "refresh_token": refresh_token},
expires_at= token_data.get("expires_in", 3599) + int(time.time())
)
方案 2:环境变量配置
在 .env 文件中添加:
# Dify 基础 URL 配置
DIFY_BASE_URL=http://localhost
# Outlook 插件配置
OUTLOOK_CLIENT_ID=your_client_id
OUTLOOK_CLIENT_SECRET=your_client_secret
OUTLOOK_REDIRECT_URI=http://localhost/console/api/oauth/plugin/langgenius/outlook/outlook/tool/callback
方案 3:Azure 应用注册配置
在 Azure 门户中,确保重定向 URI 配置为:
http://localhost/console/api/oauth/plugin/langgenius/outlook/outlook/tool/callback
部署步骤
- 修改源码:按照方案 1 修改 outlook.py 文件
- 配置环境变量:在 .env 文件中添加 DIFY_BASE_URL
- 重新构建服务:
docker-compose down docker-compose build api docker-compose up -d - 验证修复:重新配置 Outlook 插件,应该能正常完成 OAuth 认证
技术要点
- 问题根源:Dify Outlook 插件内部使用了相对路径的 redirect_uri
- 解决方案:在 OAuth 流程中自动将相对路径转换为绝对路径
- 兼容性:修改后的代码同时支持相对路径和绝对路径
- 配置灵活性:通过环境变量支持不同部署环境
总结
这个问题的核心在于 Dify 插件系统与 Azure AD OAuth 规范的不兼容。通过源码级别的修改,我们实现了自动的 URL 转换,确保了 OAuth 认证流程的正常进行。这种解决方案也适用于其他类似的 OAuth 集成问题。
标签:#Dify #Outlook #OAuth #AzureAD #Docker #Python

1224

被折叠的 条评论
为什么被折叠?



