fix: 加固 LinuxDo OAuth 登录安全与配置校验

This commit is contained in:
shaw
2026-01-09 19:32:06 +08:00
parent 707061efac
commit f060db0b30
14 changed files with 184 additions and 205 deletions

View File

@@ -17,6 +17,7 @@ import (
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/imroc/req/v3"
@@ -66,7 +67,7 @@ func (e *linuxDoTokenExchangeError) Error() string {
return strings.Join(parts, " ")
}
// LinuxDoOAuthStart starts the LinuxDo Connect OAuth login flow.
// LinuxDoOAuthStart 启动 LinuxDo Connect OAuth 登录流程。
// GET /api/v1/auth/oauth/linuxdo/start?redirect=/dashboard
func (h *AuthHandler) LinuxDoOAuthStart(c *gin.Context) {
cfg, err := h.getLinuxDoOAuthConfig(c.Request.Context())
@@ -116,7 +117,7 @@ func (h *AuthHandler) LinuxDoOAuthStart(c *gin.Context) {
c.Redirect(http.StatusFound, authURL)
}
// LinuxDoOAuthCallback handles the OAuth callback, creates/logins the user, then redirects to frontend.
// LinuxDoOAuthCallback 处理 OAuth 回调:创建/登录用户,然后重定向到前端。
// GET /api/v1/auth/oauth/linuxdo/callback?code=...&state=...
func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
cfg, cfgErr := h.getLinuxDoOAuthConfig(c.Request.Context())
@@ -197,16 +198,22 @@ func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
return
}
email, username, _, err := linuxDoFetchUserInfo(c.Request.Context(), cfg, tokenResp)
email, username, subject, err := linuxDoFetchUserInfo(c.Request.Context(), cfg, tokenResp)
if err != nil {
log.Printf("[LinuxDo OAuth] userinfo fetch failed: %v", err)
redirectOAuthError(c, frontendCallback, "userinfo_failed", "failed to fetch user info", "")
return
}
// 安全考虑:不要把第三方返回的 email 直接映射到本地账号(可能与本地邮箱用户冲突导致账号被接管)。
// 统一使用基于 subject 的稳定合成邮箱来做账号绑定。
if subject != "" {
email = linuxDoSyntheticEmail(subject)
}
jwtToken, _, err := h.authService.LoginOrRegisterOAuth(c.Request.Context(), email, username)
if err != nil {
// Avoid leaking internal details to the client; keep structured reason for frontend.
// 避免把内部细节泄露给客户端;给前端保留结构化原因与提示信息即可。
redirectOAuthError(c, frontendCallback, "login_failed", infraerrors.Reason(err), infraerrors.Message(err))
return
}
@@ -352,9 +359,8 @@ func linuxDoParseUserInfo(body string, cfg config.LinuxDoConnectConfig) (email s
email = strings.TrimSpace(email)
if email == "" {
// LinuxDo Connect userinfo does not necessarily provide email. To keep compatibility with the
// existing user schema (email is required/unique), use a stable synthetic email.
email = fmt.Sprintf("linuxdo-%s@linuxdo-connect.invalid", subject)
// LinuxDo Connect userinfo 可能不提供 email。为兼容现有用户模型email 必填且唯一),使用稳定的合成邮箱。
email = linuxDoSyntheticEmail(subject)
}
username = strings.TrimSpace(username)
@@ -403,7 +409,7 @@ func redirectOAuthError(c *gin.Context, frontendCallback string, code string, me
func redirectWithFragment(c *gin.Context, frontendCallback string, fragment url.Values) {
u, err := url.Parse(frontendCallback)
if err != nil {
// Fallback: best-effort redirect.
// 兜底:尽力跳转到默认页面,避免卡死在回调页。
c.Redirect(http.StatusFound, linuxDoOAuthDefaultRedirectTo)
return
}
@@ -545,7 +551,7 @@ func sanitizeFrontendRedirectPath(path string) string {
if len(path) > linuxDoOAuthMaxRedirectLen {
return ""
}
// Only allow same-origin relative paths (avoid open redirect).
// 只允许同源相对路径(避免开放重定向)。
if !strings.HasPrefix(path, "/") {
return ""
}
@@ -663,3 +669,11 @@ func isSafeLinuxDoSubject(subject string) bool {
}
return true
}
func linuxDoSyntheticEmail(subject string) string {
subject = strings.TrimSpace(subject)
if subject == "" {
return ""
}
return "linuxdo-" + subject + service.LinuxDoConnectSyntheticEmailDomain
}