-
Notifications
You must be signed in to change notification settings - Fork 83
feat: Add Custom OIDC support for oauth configuration #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
14c3f86
268dc11
d6f8038
36ae2d8
2d5b986
9732b49
9fcc4e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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,349 @@ 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseURL := fmt.Sprintf("%s://%s", "http", c.Request.Host) | |
| // 根据请求信息推断协议,默认使用 HTTPS 以避免降级 | |
| scheme := "https" | |
| if proto := c.Request.Header.Get("X-Forwarded-Proto"); proto != "" { | |
| scheme = proto | |
| } else if c.Request.TLS == nil { | |
| // 在本地开发等纯 HTTP 场景下,兼容使用 HTTP | |
| if strings.HasPrefix(c.Request.Host, "localhost") || strings.HasPrefix(c.Request.Host, "127.0.0.1") { | |
| scheme = "http" | |
| } | |
| } | |
| baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host) |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The complete token response (which may contain sensitive information like access tokens or id tokens) is logged to stdout. This could expose credentials in log files. Consider logging only metadata like status codes or redacting sensitive fields from the logged response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using fmt.Printf for logging in production code is not a best practice. These debug statements will always output to stdout regardless of log level configuration. Consider using a proper logging framework (like logrus, zap, or the standard log package) that supports log levels and structured logging, allowing these debug messages to be disabled in production.
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scheme detection logic defaults to "http" when TLS is not detected. This could be problematic in deployments behind reverse proxies that don't set the X-Forwarded-Proto header. Consider checking additional headers like X-Forwarded-Ssl or Forwarded, or falling back to a safer default based on the deployment environment configuration.
| if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { | |
| scheme = "https" | |
| } | |
| xForwardedProto := strings.ToLower(c.GetHeader("X-Forwarded-Proto")) | |
| xForwardedSsl := strings.ToLower(c.GetHeader("X-Forwarded-Ssl")) | |
| forwarded := c.GetHeader("Forwarded") | |
| if c.Request.TLS != nil || xForwardedProto == "https" || xForwardedSsl == "on" { | |
| scheme = "https" | |
| } else if forwarded != "" { | |
| // Parse the standardized Forwarded header, e.g. "proto=https;host=example.com" | |
| for _, part := range strings.Split(forwarded, ";") { | |
| for _, item := range strings.Split(part, ",") { | |
| item = strings.TrimSpace(strings.ToLower(item)) | |
| if strings.HasPrefix(item, "proto=") { | |
| protoVal := strings.TrimPrefix(item, "proto=") | |
| if protoVal == "https" { | |
| scheme = "https" | |
| } | |
| break | |
| } | |
| } | |
| if scheme == "https" { | |
| break | |
| } | |
| } | |
| } |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extractFieldFromUserData function uses fmt.Sprintf to convert the final value to a string, which will convert any type to its string representation. However, this might not be appropriate for all data types. For example, if the field contains a boolean true, it will become the string "true". Consider validating that the extracted value is actually a string type, or document this behavior clearly.
| // 最后一个部分,转换为字符串 | |
| return fmt.Sprintf("%v", val) | |
| // 最后一个部分,仅当值为字符串时返回 | |
| if s, ok := val.(string); ok { | |
| return s | |
| } | |
| return "" |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deprecated ioutil.ReadAll is used instead of the recommended io.ReadAll. Since Go 1.16, io.ReadAll should be used as ioutil functions have been deprecated and moved to the io package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error from json.Unmarshal is silently ignored. If the configuration JSON is malformed, this could lead to using zero values in the cfg struct, resulting in unclear error messages later. Consider checking the unmarshal error and returning a specific error message indicating the configuration is corrupted or invalid.