feat: add Feishu OAuth integration

Implement Feishu OAuth provider using standard client:
- Set up Feishu-specific endpoints for authorization, token, and userinfo
- Use user_id as sub claim for Feishu user identification
- Extract correct user information from nested 'data' field in Feishu responses

Configuration requirements:
- Set FEISHU_CLIENT_ID and FEISHU_CLIENT_SECRET environment variables to enable Feishu OAuth
- Set ENABLE_OAUTH_SIGNUP=true to allow automatic user creation after OAuth login
- Set DEFAULT_USER_ROLE=user to grant immediate access after OAuth registration
- Set OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true to enable merging of existing user accounts with matching emails
This commit is contained in:
Xie Yanbo 2025-07-30 16:00:15 +08:00
parent 4a76bea80c
commit ee82439e67
4 changed files with 65 additions and 1 deletions

View File

@ -1,3 +1,3 @@
export CORS_ALLOW_ORIGIN="http://localhost:5173" export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080"
PORT="${PORT:-8080}" PORT="${PORT:-8080}"
uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload

View File

@ -513,6 +513,30 @@ OAUTH_GROUPS_CLAIM = PersistentConfig(
os.environ.get("OAUTH_GROUPS_CLAIM", os.environ.get("OAUTH_GROUP_CLAIM", "groups")), os.environ.get("OAUTH_GROUPS_CLAIM", os.environ.get("OAUTH_GROUP_CLAIM", "groups")),
) )
FEISHU_CLIENT_ID = PersistentConfig(
"FEISHU_CLIENT_ID",
"oauth.feishu.client_id",
os.environ.get("FEISHU_CLIENT_ID", ""),
)
FEISHU_CLIENT_SECRET = PersistentConfig(
"FEISHU_CLIENT_SECRET",
"oauth.feishu.client_secret",
os.environ.get("FEISHU_CLIENT_SECRET", ""),
)
FEISHU_OAUTH_SCOPE = PersistentConfig(
"FEISHU_OAUTH_SCOPE",
"oauth.feishu.scope",
os.environ.get("FEISHU_OAUTH_SCOPE", "contact:user.base:readonly"),
)
FEISHU_REDIRECT_URI = PersistentConfig(
"FEISHU_REDIRECT_URI",
"oauth.feishu.redirect_uri",
os.environ.get("FEISHU_REDIRECT_URI", ""),
)
ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig( ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig(
"ENABLE_OAUTH_ROLE_MANAGEMENT", "ENABLE_OAUTH_ROLE_MANAGEMENT",
"oauth.enable_role_mapping", "oauth.enable_role_mapping",
@ -705,6 +729,32 @@ def load_oauth_providers():
"register": oidc_oauth_register, "register": oidc_oauth_register,
} }
if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value:
def feishu_oauth_register(client: OAuth):
client.register(
name="feishu",
client_id=FEISHU_CLIENT_ID.value,
client_secret=FEISHU_CLIENT_SECRET.value,
access_token_url="https://open.feishu.cn/open-apis/authen/v2/oauth/token",
authorize_url="https://accounts.feishu.cn/open-apis/authen/v1/authorize",
api_base_url="https://open.feishu.cn/open-apis",
userinfo_endpoint="https://open.feishu.cn/open-apis/authen/v1/user_info",
client_kwargs={
"scope": FEISHU_OAUTH_SCOPE.value,
**(
{"timeout": int(OAUTH_TIMEOUT.value)}
if OAUTH_TIMEOUT.value
else {}
),
},
redirect_uri=FEISHU_REDIRECT_URI.value,
)
OAUTH_PROVIDERS["feishu"] = {
"register": feishu_oauth_register,
"sub_claim": "user_id",
}
configured_providers = [] configured_providers = []
if GOOGLE_CLIENT_ID.value: if GOOGLE_CLIENT_ID.value:
configured_providers.append("Google") configured_providers.append("Google")
@ -712,6 +762,8 @@ def load_oauth_providers():
configured_providers.append("Microsoft") configured_providers.append("Microsoft")
if GITHUB_CLIENT_ID.value: if GITHUB_CLIENT_ID.value:
configured_providers.append("GitHub") configured_providers.append("GitHub")
if FEISHU_CLIENT_ID.value:
configured_providers.append("Feishu")
if configured_providers and not OPENID_PROVIDER_URL.value: if configured_providers and not OPENID_PROVIDER_URL.value:
provider_list = ", ".join(configured_providers) provider_list = ", ".join(configured_providers)

View File

@ -602,6 +602,8 @@ class OAuthManager:
or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data) or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data)
): ):
user_data: UserInfo = await client.userinfo(token=token) user_data: UserInfo = await client.userinfo(token=token)
if provider == "feishu" and isinstance(user_data, dict) and "data" in user_data:
user_data = user_data["data"]
if not user_data: if not user_data:
log.warning(f"OAuth callback failed, user data is missing: {token}") log.warning(f"OAuth callback failed, user data is missing: {token}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)

View File

@ -523,6 +523,16 @@
> >
</button> </button>
{/if} {/if}
{#if $config?.oauth?.providers?.feishu}
<button
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-medium text-sm py-2.5"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/feishu/login`;
}}
>
<span>{$i18n.t('Continue with {{provider}}', { provider: 'Feishu' })}</span>
</button>
{/if}
</div> </div>
{/if} {/if}