From 14c3f861ea57ee0f32010226bbde8a29185245c6 Mon Sep 17 00:00:00 2001 From: moooyo Date: Wed, 17 Dec 2025 22:51:06 +0800 Subject: [PATCH 1/7] add custom oauth support --- internal/api/auth.go | 350 +++++++++++++++++- .../components/settings/security-settings.tsx | 333 ++++++++++++++++- web/src/pages/login/index.tsx | 26 +- 3 files changed, 697 insertions(+), 12 deletions(-) diff --git a/internal/api/auth.go b/internal/api/auth.go index c5d8ba47..50cdcb20 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -50,6 +50,8 @@ func SetupAuthRoutes(rg *gin.RouterGroup, authService *auth.Service) { rg.GET("/oauth2/config", authHandler.HandleOAuth2Config) rg.POST("/oauth2/config", authHandler.HandleOAuth2Config) rg.DELETE("/oauth2/config", authHandler.HandleOAuth2Config) + // OIDC Discovery + rg.GET("/oauth2/discover", authHandler.HandleOIDCDiscover) } // createProxyClient 创建支持系统代理的HTTP客户端 @@ -400,6 +402,8 @@ func (h *AuthHandler) HandleOAuth2Callback(c *gin.Context) { h.handleGitHubOAuth(c, code) case "cloudflare": h.handleCloudflareOAuth(c, code) + case "custom": + h.handleCustomOIDC(c, code) default: c.JSON(http.StatusOK, gin.H{ "success": false, @@ -896,7 +900,8 @@ func (h *AuthHandler) HandleOAuth2Login(c *gin.Context) { q.Set("scope", scopes) } - if provider == "cloudflare" { + // Cloudflare 和 Custom OIDC 需要设置 response_type=code(OIDC 标准) + if provider == "cloudflare" || provider == "custom" { q.Set("response_type", "code") } @@ -906,14 +911,355 @@ func (h *AuthHandler) HandleOAuth2Login(c *gin.Context) { c.Redirect(http.StatusFound, loginURL) } +// handleCustomOIDC 处理 Custom OIDC 回调 +func (h *AuthHandler) handleCustomOIDC(c *gin.Context, code string) { + // 读取配置 + cfgStr, err := h.authService.GetSystemConfig("oauth2_config") + if err != nil || cfgStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Custom OIDC 未配置"}) + return + } + + type customCfg struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + AuthURL string `json:"authUrl"` + TokenURL string `json:"tokenUrl"` + UserInfoURL string `json:"userInfoUrl"` + RedirectURI string `json:"redirectUri"` + Scopes []string `json:"scopes"` + UserIDPath string `json:"userIdPath"` + UsernamePath string `json:"usernamePath"` + DisplayName string `json:"displayName"` + } + var cfg customCfg + _ = json.Unmarshal([]byte(cfgStr), &cfg) + + if cfg.ClientID == "" || cfg.ClientSecret == "" || cfg.TokenURL == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Custom OIDC 配置不完整"}) + return + } + + // 设置默认值 + if cfg.UserIDPath == "" { + cfg.UserIDPath = "sub" + } + if cfg.UsernamePath == "" { + cfg.UsernamePath = "preferred_username" + } + if cfg.DisplayName == "" { + cfg.DisplayName = "OIDC" + } + + // 交换 access token + form := url.Values{} + form.Set("client_id", cfg.ClientID) + form.Set("client_secret", cfg.ClientSecret) + form.Set("code", code) + form.Set("grant_type", "authorization_code") + + // 设置 redirect_uri + redirectURI := cfg.RedirectURI + if redirectURI == "" { + baseURL := fmt.Sprintf("%s://%s", "http", c.Request.Host) + redirectURI = baseURL + "/api/oauth2/callback" + } + form.Set("redirect_uri", redirectURI) + + fmt.Printf("🔍 Custom OIDC Token 请求: token_url=%s, redirect_uri=%s\n", cfg.TokenURL, redirectURI) + + tokenReq, _ := http.NewRequest("POST", cfg.TokenURL, strings.NewReader(form.Encode())) + tokenReq.Header.Set("Accept", "application/json") + tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // 使用支持代理的HTTP客户端 + proxyClient := h.createProxyClient() + resp, err := proxyClient.Do(tokenReq) + if err != nil { + fmt.Printf("❌ Custom OIDC Token 请求错误: %v\n", err) + c.JSON(http.StatusBadGateway, gin.H{"error": "请求 OIDC Token 失败"}) + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := ioutil.ReadAll(resp.Body) + fmt.Printf("❌ Custom OIDC Token 错误 %d: %s\n", resp.StatusCode, string(bodyBytes)) + c.JSON(http.StatusBadGateway, gin.H{"error": "OIDC Token 接口返回错误"}) + return + } + + body, _ := ioutil.ReadAll(resp.Body) + fmt.Printf("🔑 Custom OIDC Token 响应: %s\n", string(body)) + + var tokenRes struct { + AccessToken string `json:"access_token"` + IdToken string `json:"id_token"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` + } + _ = json.Unmarshal(body, &tokenRes) + if tokenRes.AccessToken == "" { + c.JSON(http.StatusBadGateway, gin.H{"error": "获取 AccessToken 失败"}) + return + } + + var userData map[string]interface{} + + // 方式1: 通过 userinfo 端点获取用户信息 + if cfg.UserInfoURL != "" { + userReq, _ := http.NewRequest("GET", cfg.UserInfoURL, nil) + userReq.Header.Set("Authorization", "Bearer "+tokenRes.AccessToken) + userReq.Header.Set("Accept", "application/json") + + userResp, err := proxyClient.Do(userReq) + if err == nil { + defer userResp.Body.Close() + bodyBytes, _ := ioutil.ReadAll(userResp.Body) + _ = json.Unmarshal(bodyBytes, &userData) + fmt.Printf("👤 Custom OIDC 用户信息 (userinfo): %s\n", string(bodyBytes)) + } + } + + // 方式2: 若未获取到用户信息且 id_token 存在,则解析 id_token JWT payload + if len(userData) == 0 && tokenRes.IdToken != "" { + parts := strings.Split(tokenRes.IdToken, ".") + if len(parts) >= 2 { + payload, _ := base64.RawURLEncoding.DecodeString(parts[1]) + _ = json.Unmarshal(payload, &userData) + fmt.Printf("👤 Custom OIDC id_token payload: %s\n", string(payload)) + } + } + + if len(userData) == 0 { + c.JSON(http.StatusBadGateway, gin.H{"error": "无法获取 OIDC 用户信息"}) + return + } + + // 提取用户 ID(使用配置的 userIdPath) + providerID := h.extractFieldFromUserData(userData, cfg.UserIDPath) + if providerID == "" { + // 回退到常用字段 + providerID = h.extractFieldFromUserData(userData, "sub") + if providerID == "" { + providerID = h.extractFieldFromUserData(userData, "id") + } + } + + if providerID == "" { + c.JSON(http.StatusBadGateway, gin.H{"error": "无法获取 OIDC 用户唯一标识"}) + return + } + + // 提取用户名(使用配置的 usernamePath) + login := h.extractFieldFromUserData(userData, cfg.UsernamePath) + if login == "" { + // 回退到常用字段 + login = h.extractFieldFromUserData(userData, "preferred_username") + if login == "" { + login = h.extractFieldFromUserData(userData, "email") + } + if login == "" { + login = h.extractFieldFromUserData(userData, "name") + } + if login == "" { + login = providerID // 最后回退到使用 providerID + } + } + + username := "custom:" + login + + // 保存用户信息 + dataJSON, _ := json.Marshal(userData) + if err := h.authService.SaveOAuthUser("custom", providerID, username, string(dataJSON)); err != nil { + fmt.Printf("❌ 保存 Custom OIDC 用户失败: %v\n", err) + // 重定向到错误页面 + baseURL := "" + if cfg.RedirectURI != "" { + baseURL = strings.Replace(cfg.RedirectURI, "/api/oauth2/callback", "", 1) + } else { + scheme := "http" + if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { + scheme = "https" + } + baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host) + } + errorURL := fmt.Sprintf("%s/oauth-error?error=%s&provider=custom", + baseURL, url.QueryEscape(err.Error())) + c.Redirect(http.StatusFound, errorURL) + return + } + + // 创建会话 (24小时有效期) + sessionID, err := h.authService.CreateSession(username, 24*time.Hour) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建会话失败"}) + return + } + + // 设置 cookie + c.SetCookie("session", sessionID, 24*60*60, "/", "", false, true) + + // 重定向到 dashboard + redirectURL := c.Query("redirect") + if redirectURL == "" { + redirectURL = strings.Replace(cfg.RedirectURI, "/api/oauth2/callback", "/dashboard", 1) + } + + accept := c.GetHeader("Accept") + if strings.Contains(accept, "text/html") || strings.Contains(accept, "application/xhtml+xml") || redirectURL != "" { + c.Redirect(http.StatusFound, redirectURL) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "provider": "custom", + "username": username, + "message": "登录成功", + }) +} + +// extractFieldFromUserData 从用户数据中提取字段(支持简单的点号路径) +func (h *AuthHandler) extractFieldFromUserData(data map[string]interface{}, path string) string { + if path == "" { + return "" + } + + parts := strings.Split(path, ".") + current := data + + for i, part := range parts { + if val, ok := current[part]; ok { + if i == len(parts)-1 { + // 最后一个部分,转换为字符串 + return fmt.Sprintf("%v", val) + } + // 不是最后一个部分,继续深入 + if nested, ok := val.(map[string]interface{}); ok { + current = nested + } else { + return "" + } + } else { + return "" + } + } + return "" +} + // HandleOAuth2Provider 仅返回当前绑定的 OAuth2 provider(用于登录页) func (h *AuthHandler) HandleOAuth2Provider(c *gin.Context) { provider, _ := h.authService.GetSystemConfig("oauth2_provider") disableLogin, _ := h.authService.GetSystemConfig("disable_login") - c.JSON(http.StatusOK, gin.H{ + resp := gin.H{ "success": true, "provider": provider, "disableLogin": disableLogin == "true", + } + + // 如果是 custom provider,返回 displayName + if provider == "custom" { + cfgStr, _ := h.authService.GetSystemConfig("oauth2_config") + if cfgStr != "" { + var cfg map[string]interface{} + _ = json.Unmarshal([]byte(cfgStr), &cfg) + if displayName, ok := cfg["displayName"].(string); ok && displayName != "" { + resp["displayName"] = displayName + } + } + } + + c.JSON(http.StatusOK, resp) +} + +// HandleOIDCDiscover 处理 OIDC Discovery 请求 +// GET /api/oauth2/discover?issuer=https://auth.example.com +func (h *AuthHandler) HandleOIDCDiscover(c *gin.Context) { + issuerURL := c.Query("issuer") + if issuerURL == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "error": "缺少 issuer 参数", + }) + return + } + + // 确保 issuerURL 不以 / 结尾 + issuerURL = strings.TrimSuffix(issuerURL, "/") + + // 构建 well-known 地址 + discoveryURL := issuerURL + "/.well-known/openid-configuration" + + // 使用支持代理的 HTTP 客户端 + proxyClient := h.createProxyClient() + + req, err := http.NewRequest("GET", discoveryURL, nil) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "error": "无效的 Issuer URL", + }) + return + } + req.Header.Set("Accept", "application/json") + + resp, err := proxyClient.Do(req) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{ + "success": false, + "error": fmt.Sprintf("无法连接到 OIDC 服务器: %v", err), + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + c.JSON(http.StatusBadGateway, gin.H{ + "success": false, + "error": fmt.Sprintf("OIDC Discovery 失败,状态码: %d", resp.StatusCode), + }) + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "error": "读取响应失败", + }) + return + } + + var discoveryData map[string]interface{} + if err := json.Unmarshal(body, &discoveryData); err != nil { + c.JSON(http.StatusBadGateway, gin.H{ + "success": false, + "error": "解析 OIDC 配置失败", + }) + return + } + + // 提取关键端点 + authorizationEndpoint, _ := discoveryData["authorization_endpoint"].(string) + tokenEndpoint, _ := discoveryData["token_endpoint"].(string) + userinfoEndpoint, _ := discoveryData["userinfo_endpoint"].(string) + issuer, _ := discoveryData["issuer"].(string) + + if authorizationEndpoint == "" || tokenEndpoint == "" { + c.JSON(http.StatusBadGateway, gin.H{ + "success": false, + "error": "OIDC 配置不完整,缺少必要端点", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "issuer": issuer, + "authorizationEndpoint": authorizationEndpoint, + "tokenEndpoint": tokenEndpoint, + "userinfoEndpoint": userinfoEndpoint, }) } diff --git a/web/src/components/settings/security-settings.tsx b/web/src/components/settings/security-settings.tsx index 81d64f00..582f14c3 100644 --- a/web/src/components/settings/security-settings.tsx +++ b/web/src/components/settings/security-settings.tsx @@ -115,13 +115,37 @@ const SecuritySettings = forwardRef((props, ref) => { scopes: ["openid", "profile"], }); + // Custom OIDC 配置 + interface CustomOIDCConfig extends OAuth2Config { + issuerUrl: string; + displayName: string; + usernamePath: string; + } + + const [customConfig, setCustomConfig] = useState({ + issuerUrl: "", + clientId: "", + clientSecret: "", + authUrl: "", + tokenUrl: "", + userInfoUrl: "", + userIdPath: "sub", + usernamePath: "preferred_username", + scopes: ["openid", "profile", "email"], + displayName: "", + }); + // 模拟的配置状态(实际应该从后端获取) const [isGitHubConfigured, setIsGitHubConfigured] = useState(false); const [isCloudflareConfigured, setIsCloudflareConfigured] = useState(false); + const [isCustomConfigured, setIsCustomConfigured] = useState(false); + + // OIDC Discovery 加载状态 + const [isDiscovering, setIsDiscovering] = useState(false); // 在 state 部分添加 selectedProvider 和 provider select disclosure const [selectedProvider, setSelectedProvider] = useState< - "github" | "cloudflare" | null + "github" | "cloudflare" | "custom" | null >(null); const { isOpen: isSelectOpen, @@ -129,6 +153,13 @@ const SecuritySettings = forwardRef((props, ref) => { onOpenChange: onSelectOpenChange, } = useDisclosure(); + // Custom OIDC 配置模态框 + const { + isOpen: isCustomOpen, + onOpen: onCustomOpen, + onOpenChange: onCustomOpenChange, + } = useDisclosure(); + // 初始化表单 const { register, @@ -150,7 +181,7 @@ const SecuritySettings = forwardRef((props, ref) => { if (!data.success) return; - const curProvider = data.provider as "github" | "cloudflare" | ""; + const curProvider = data.provider as "github" | "cloudflare" | "custom" | ""; if (!curProvider) return; // 未绑定 @@ -162,6 +193,9 @@ const SecuritySettings = forwardRef((props, ref) => { } else if (curProvider === "cloudflare") { setCloudflareConfig((prev: any) => ({ ...prev, ...cfgData.config })); setIsCloudflareConfigured(true); + } else if (curProvider === "custom") { + setCustomConfig((prev: any) => ({ ...prev, ...cfgData.config })); + setIsCustomConfigured(true); } } catch (e) { console.error("初始化 OAuth2 配置失败", e); @@ -404,8 +438,127 @@ const SecuritySettings = forwardRef((props, ref) => { } }; + // OIDC Discovery 函数 + const handleDiscoverOIDC = async () => { + if (!customConfig.issuerUrl) { + addToast({ + title: "请输入 Issuer URL", + description: "Issuer URL 不能为空", + color: "warning", + }); + return; + } + + setIsDiscovering(true); + try { + const res = await fetch( + buildApiUrl(`/api/oauth2/discover?issuer=${encodeURIComponent(customConfig.issuerUrl)}`) + ); + const data = await res.json(); + + if (data.success) { + setCustomConfig((prev) => ({ + ...prev, + authUrl: data.authorizationEndpoint || "", + tokenUrl: data.tokenEndpoint || "", + userInfoUrl: data.userinfoEndpoint || "", + })); + addToast({ + title: "发现成功", + description: "已自动填充 OIDC 端点配置", + color: "success", + }); + } else { + addToast({ + title: "发现失败", + description: data.error || "无法获取 OIDC 配置", + color: "danger", + }); + } + } catch (e) { + console.error("OIDC Discovery 失败:", e); + addToast({ + title: "发现失败", + description: "无法连接到 OIDC 服务器", + color: "danger", + }); + } finally { + setIsDiscovering(false); + } + }; + + // Custom OIDC 配置保存 + const handleSaveCustomConfig = async () => { + // 验证必填字段 + if (!customConfig.clientId || !customConfig.clientSecret) { + addToast({ + title: "配置不完整", + description: "请填写 Client ID 和 Client Secret", + color: "warning", + }); + return; + } + + if (!customConfig.authUrl || !customConfig.tokenUrl) { + addToast({ + title: "配置不完整", + description: "请先使用「发现」按钮获取 OIDC 端点,或手动填写 Auth URL 和 Token URL", + color: "warning", + }); + return; + } + + if (!customConfig.displayName) { + addToast({ + title: "配置不完整", + description: "请填写显示名称(如 Keycloak、Authentik 等)", + color: "warning", + }); + return; + } + + try { + setIsSubmitting(true); + + const redirectUri = `${window.location.origin}/api/oauth2/callback`; + const payload = { + provider: "custom", + config: { + ...customConfig, + redirectUri, + }, + }; + + const res = await fetch(buildApiUrl("/api/oauth2/config"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!res.ok) throw new Error("保存失败"); + + addToast({ + title: "配置保存成功", + description: `${customConfig.displayName} OIDC 配置已成功保存`, + color: "success", + }); + + setIsCustomConfigured(true); + onCustomOpenChange(); + } catch (error) { + console.error("保存 Custom OIDC 配置失败:", error); + addToast({ + title: "保存失败", + description: "保存 Custom OIDC 配置时发生错误", + color: "danger", + }); + } finally { + setIsSubmitting(false); + } + }; + // 解绑处理 - const handleUnbindProvider = async (provider: "github" | "cloudflare") => { + const handleUnbindProvider = async (provider: "github" | "cloudflare" | "custom") => { try { setIsSubmitting(true); const res = await fetch(buildApiUrl("/api/oauth2/config"), { @@ -419,7 +572,8 @@ const SecuritySettings = forwardRef((props, ref) => { color: "success", }); if (provider === "github") setIsGitHubConfigured(false); - else setIsCloudflareConfigured(false); + else if (provider === "cloudflare") setIsCloudflareConfigured(false); + else if (provider === "custom") setIsCustomConfigured(false); } catch (e) { console.error("解绑失败", e); addToast({ @@ -675,7 +829,7 @@ const SecuritySettings = forwardRef((props, ref) => { - {isGitHubConfigured || isCloudflareConfigured ? ( + {isGitHubConfigured || isCloudflareConfigured || isCustomConfigured ? ( // 已绑定状态
@@ -702,6 +856,17 @@ const SecuritySettings = forwardRef((props, ref) => { Cloudflare{" "} )} + {isCustomConfigured && ( + <> + {" "} + {" "} + {customConfig.displayName || "Custom OIDC"}{" "} + + )} 已绑定 @@ -715,6 +880,7 @@ const SecuritySettings = forwardRef((props, ref) => { // 打开对应配置模态框 if (isGitHubConfigured) onGitHubOpen(); else if (isCloudflareConfigured) onCloudflareOpen(); + else if (isCustomConfigured) onCustomOpen(); }} > 配置 @@ -728,7 +894,7 @@ const SecuritySettings = forwardRef((props, ref) => { } onPress={() => handleUnbindProvider( - isGitHubConfigured ? "github" : "cloudflare", + isGitHubConfigured ? "github" : isCloudflareConfigured ? "cloudflare" : "custom", ) } > @@ -795,6 +961,20 @@ const SecuritySettings = forwardRef((props, ref) => { > Cloudflare +
@@ -974,6 +1154,147 @@ const SecuritySettings = forwardRef((props, ref) => { )} + + {/* Custom OIDC 配置模态框 */} + + + {(onClose) => ( + <> + + Custom OIDC 配置 + + +
+ {/* Issuer URL + 发现按钮 */} +
+ + setCustomConfig((prev) => ({ + ...prev, + issuerUrl: e.target.value, + })) + } + /> + +
+ + + + {/* 显示名称 */} + + setCustomConfig((prev) => ({ + ...prev, + displayName: e.target.value, + })) + } + /> + + {/* Client ID / Secret */} + + setCustomConfig((prev) => ({ + ...prev, + clientId: e.target.value, + })) + } + /> + + setCustomConfig((prev) => ({ + ...prev, + clientSecret: e.target.value, + })) + } + /> + + + + {/* OIDC 端点(通常由发现自动填充) */} +

+ 以下端点通常由「发现」自动填充,也可手动修改 +

+ + setCustomConfig((prev) => ({ + ...prev, + authUrl: e.target.value, + })) + } + /> + + setCustomConfig((prev) => ({ + ...prev, + tokenUrl: e.target.value, + })) + } + /> + + setCustomConfig((prev) => ({ + ...prev, + userInfoUrl: e.target.value, + })) + } + /> +
+
+ + + + + + )} +
+
); }); diff --git a/web/src/pages/login/index.tsx b/web/src/pages/login/index.tsx index 12a0f406..4a69a839 100644 --- a/web/src/pages/login/index.tsx +++ b/web/src/pages/login/index.tsx @@ -38,7 +38,8 @@ export default function LoginPage() { // OAuth2 配置状态 const [oauthProviders, setOauthProviders] = useState<{ - provider?: "github" | "cloudflare"; + provider?: "github" | "cloudflare" | "custom"; + displayName?: string; config?: any; }>({}); // 是否禁用用户名密码登录 @@ -58,7 +59,7 @@ export default function LoginPage() { */ const fetchCurrentProvider = async () => { try { - const res = await fetch("/api/auth/oauth2"); // 仅返回 provider 和 disableLogin + const res = await fetch("/api/auth/oauth2"); // 仅返回 provider、disableLogin 和 displayName(custom 时) const data = await res.json(); if (data.success) { @@ -66,9 +67,12 @@ export default function LoginPage() { const loginDisabled = data.disableLogin === true; if (data.provider) { - const cur = data.provider as "github" | "cloudflare"; + const cur = data.provider as "github" | "cloudflare" | "custom"; - setOauthProviders({ provider: cur }); + setOauthProviders({ + provider: cur, + displayName: data.displayName || undefined, + }); } // 设置是否禁用用户名密码登录 @@ -326,6 +330,20 @@ export default function LoginPage() { 使用 Cloudflare 登录 )} + {oauthProviders.provider === "custom" && ( + + )}
)} From 268dc11796bcc672d1ca1d35d7dffab9d8aeab92 Mon Sep 17 00:00:00 2001 From: Yu Leng Date: Thu, 18 Dec 2025 00:38:03 +0800 Subject: [PATCH 2/7] fix style issue --- internal/api/auth.go | 16 ++-- .../components/settings/security-settings.tsx | 78 +++++++++++++++++-- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/internal/api/auth.go b/internal/api/auth.go index 50cdcb20..a70367f9 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -1175,23 +1175,17 @@ func (h *AuthHandler) HandleOAuth2Provider(c *gin.Context) { } // HandleOIDCDiscover 处理 OIDC Discovery 请求 -// GET /api/oauth2/discover?issuer=https://auth.example.com +// GET /api/oauth2/discover?url=https://auth.example.com/.well-known/openid-configuration func (h *AuthHandler) HandleOIDCDiscover(c *gin.Context) { - issuerURL := c.Query("issuer") - if issuerURL == "" { + discoveryURL := c.Query("url") + if discoveryURL == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, - "error": "缺少 issuer 参数", + "error": "缺少 url 参数", }) return } - // 确保 issuerURL 不以 / 结尾 - issuerURL = strings.TrimSuffix(issuerURL, "/") - - // 构建 well-known 地址 - discoveryURL := issuerURL + "/.well-known/openid-configuration" - // 使用支持代理的 HTTP 客户端 proxyClient := h.createProxyClient() @@ -1199,7 +1193,7 @@ func (h *AuthHandler) HandleOIDCDiscover(c *gin.Context) { if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, - "error": "无效的 Issuer URL", + "error": "无效的 Discovery URL", }) return } diff --git a/web/src/components/settings/security-settings.tsx b/web/src/components/settings/security-settings.tsx index 582f14c3..0a3a4e2e 100644 --- a/web/src/components/settings/security-settings.tsx +++ b/web/src/components/settings/security-settings.tsx @@ -1,4 +1,6 @@ import { + Accordion, + AccordionItem, Button, Card, CardBody, @@ -442,8 +444,8 @@ const SecuritySettings = forwardRef((props, ref) => { const handleDiscoverOIDC = async () => { if (!customConfig.issuerUrl) { addToast({ - title: "请输入 Issuer URL", - description: "Issuer URL 不能为空", + title: "请输入 Discovery URL", + description: "Discovery URL 不能为空", color: "warning", }); return; @@ -452,7 +454,7 @@ const SecuritySettings = forwardRef((props, ref) => { setIsDiscovering(true); try { const res = await fetch( - buildApiUrl(`/api/oauth2/discover?issuer=${encodeURIComponent(customConfig.issuerUrl)}`) + buildApiUrl(`/api/oauth2/discover?url=${encodeURIComponent(customConfig.issuerUrl)}`) ); const data = await res.json(); @@ -1172,13 +1174,13 @@ const SecuritySettings = forwardRef((props, ref) => {
- {/* Issuer URL + 发现按钮 */} -
+ {/* Discovery URL + 发现按钮 */} +
setCustomConfig((prev) => ({ @@ -1188,6 +1190,7 @@ const SecuritySettings = forwardRef((props, ref) => { } />
From d6f80384fc5e9a929eb26ab1078031597d77c5fd Mon Sep 17 00:00:00 2001 From: Yu Leng Date: Thu, 18 Dec 2025 00:42:48 +0800 Subject: [PATCH 3/7] refactor form --- .../components/settings/security-settings.tsx | 157 ++++++++---------- 1 file changed, 68 insertions(+), 89 deletions(-) diff --git a/web/src/components/settings/security-settings.tsx b/web/src/components/settings/security-settings.tsx index 0a3a4e2e..4f128639 100644 --- a/web/src/components/settings/security-settings.tsx +++ b/web/src/components/settings/security-settings.tsx @@ -1,6 +1,4 @@ import { - Accordion, - AccordionItem, Button, Card, CardBody, @@ -1174,31 +1172,19 @@ const SecuritySettings = forwardRef((props, ref) => {
- {/* Discovery URL + 发现按钮 */} -
- - setCustomConfig((prev) => ({ - ...prev, - issuerUrl: e.target.value, - })) - } - /> - -
+ {/* Discovery URL */} + + setCustomConfig((prev) => ({ + ...prev, + issuerUrl: e.target.value, + })) + } + /> @@ -1243,10 +1229,7 @@ const SecuritySettings = forwardRef((props, ref) => { - {/* OIDC 端点(通常由发现自动填充) */} -

- 以下端点通常由「发现」自动填充,也可手动修改 -

+ {/* OIDC 端点 */} ((props, ref) => { } /> - {/* 高级配置 */} - - 高级配置 - } - subtitle={ - - Scopes、用户字段映射等 - - } - > -
- { - const scopesStr = e.target.value; - const scopesArr = scopesStr - .split(/\s+/) - .filter((s) => s.length > 0); - setCustomConfig((prev) => ({ - ...prev, - scopes: scopesArr.length > 0 ? scopesArr : ["openid", "profile", "email"], - })); - }} - /> - - setCustomConfig((prev) => ({ - ...prev, - userIdPath: e.target.value || "sub", - })) - } - /> - - setCustomConfig((prev) => ({ - ...prev, - usernamePath: e.target.value || "preferred_username", - })) - } - /> -
-
-
+ + + {/* Scopes 和字段映射 */} + { + const scopesStr = e.target.value; + const scopesArr = scopesStr + .split(/\s+/) + .filter((s) => s.length > 0); + setCustomConfig((prev) => ({ + ...prev, + scopes: + scopesArr.length > 0 + ? scopesArr + : ["openid", "profile", "email"], + })); + }} + /> + + setCustomConfig((prev) => ({ + ...prev, + userIdPath: e.target.value || "sub", + })) + } + /> + + setCustomConfig((prev) => ({ + ...prev, + usernamePath: e.target.value || "preferred_username", + })) + } + />
+ -