auth_oauth_pending_flow.go 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066
  1. package handler
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. dbent "github.com/Wei-Shaw/sub2api/ent"
  13. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  14. "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
  15. "github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
  16. "github.com/Wei-Shaw/sub2api/ent/predicate"
  17. dbuser "github.com/Wei-Shaw/sub2api/ent/user"
  18. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  19. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  20. "github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
  21. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  22. "github.com/Wei-Shaw/sub2api/internal/service"
  23. entsql "entgo.io/ent/dialect/sql"
  24. "github.com/gin-gonic/gin"
  25. )
  26. const (
  27. oauthPendingBrowserCookiePath = "/api/v1/auth/oauth"
  28. oauthPendingBrowserCookieName = "oauth_pending_browser_session"
  29. oauthPendingSessionCookiePath = "/api/v1/auth/oauth"
  30. oauthPendingSessionCookieName = "oauth_pending_session"
  31. oauthPromoCodeCookieName = "oauth_promo_code"
  32. oauthPendingCookieMaxAgeSec = 10 * 60
  33. oauthPendingChoiceStep = "choose_account_action_required"
  34. oauthCompletionResponseKey = "completion_response"
  35. oauthPromoCodeStateKey = "promo_code"
  36. )
  37. var pendingOAuthCreateAccountPreCommitHook func(context.Context, *dbent.PendingAuthSession) error
  38. type oauthPendingSessionPayload struct {
  39. Intent string
  40. Identity service.PendingAuthIdentityKey
  41. TargetUserID *int64
  42. ResolvedEmail string
  43. RedirectTo string
  44. BrowserSessionKey string
  45. UpstreamIdentityClaims map[string]any
  46. CompletionResponse map[string]any
  47. }
  48. type oauthAdoptionDecisionRequest struct {
  49. AdoptDisplayName *bool `json:"adopt_display_name,omitempty"`
  50. AdoptAvatar *bool `json:"adopt_avatar,omitempty"`
  51. }
  52. type bindPendingOAuthLoginRequest struct {
  53. Email string `json:"email" binding:"required,email"`
  54. Password string `json:"password" binding:"required"`
  55. AdoptDisplayName *bool `json:"adopt_display_name,omitempty"`
  56. AdoptAvatar *bool `json:"adopt_avatar,omitempty"`
  57. }
  58. type createPendingOAuthAccountRequest struct {
  59. Email string `json:"email" binding:"required,email"`
  60. VerifyCode string `json:"verify_code,omitempty"`
  61. Password string `json:"password" binding:"required,min=6"`
  62. TurnstileToken string `json:"turnstile_token,omitempty"`
  63. TencentCaptchaTicket string `json:"tencent_captcha_ticket,omitempty"`
  64. TencentCaptchaRandstr string `json:"tencent_captcha_randstr,omitempty"`
  65. InvitationCode string `json:"invitation_code,omitempty"`
  66. AffCode string `json:"aff_code,omitempty"`
  67. AdoptDisplayName *bool `json:"adopt_display_name,omitempty"`
  68. AdoptAvatar *bool `json:"adopt_avatar,omitempty"`
  69. }
  70. type sendPendingOAuthVerifyCodeRequest struct {
  71. Email string `json:"email" binding:"required,email"`
  72. TurnstileToken string `json:"turnstile_token,omitempty"`
  73. TencentCaptchaTicket string `json:"tencent_captcha_ticket,omitempty"`
  74. TencentCaptchaRandstr string `json:"tencent_captcha_randstr,omitempty"`
  75. PendingAuthToken string `json:"pending_auth_token,omitempty"`
  76. PendingOAuthToken string `json:"pending_oauth_token,omitempty"`
  77. }
  78. func (r bindPendingOAuthLoginRequest) adoptionDecision() oauthAdoptionDecisionRequest {
  79. return oauthAdoptionDecisionRequest{
  80. AdoptDisplayName: r.AdoptDisplayName,
  81. AdoptAvatar: r.AdoptAvatar,
  82. }
  83. }
  84. func (r createPendingOAuthAccountRequest) adoptionDecision() oauthAdoptionDecisionRequest {
  85. return oauthAdoptionDecisionRequest{
  86. AdoptDisplayName: r.AdoptDisplayName,
  87. AdoptAvatar: r.AdoptAvatar,
  88. }
  89. }
  90. func (h *AuthHandler) pendingIdentityService() (*service.AuthPendingIdentityService, error) {
  91. if h == nil || h.authService == nil || h.authService.EntClient() == nil {
  92. return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
  93. }
  94. return service.NewAuthPendingIdentityService(h.authService.EntClient()), nil
  95. }
  96. func generateOAuthPendingBrowserSession() (string, error) {
  97. return oauth.GenerateState()
  98. }
  99. func setOAuthPendingBrowserCookie(c *gin.Context, sessionKey string, secure bool) {
  100. http.SetCookie(c.Writer, &http.Cookie{
  101. Name: oauthPendingBrowserCookieName,
  102. Value: encodeCookieValue(sessionKey),
  103. Path: oauthPendingBrowserCookiePath,
  104. MaxAge: oauthPendingCookieMaxAgeSec,
  105. HttpOnly: true,
  106. Secure: secure,
  107. SameSite: http.SameSiteLaxMode,
  108. })
  109. }
  110. func clearOAuthPendingBrowserCookie(c *gin.Context, secure bool) {
  111. http.SetCookie(c.Writer, &http.Cookie{
  112. Name: oauthPendingBrowserCookieName,
  113. Value: "",
  114. Path: oauthPendingBrowserCookiePath,
  115. MaxAge: -1,
  116. HttpOnly: true,
  117. Secure: secure,
  118. SameSite: http.SameSiteLaxMode,
  119. })
  120. }
  121. func readOAuthPendingBrowserCookie(c *gin.Context) (string, error) {
  122. return readCookieDecoded(c, oauthPendingBrowserCookieName)
  123. }
  124. func setOAuthPendingSessionCookie(c *gin.Context, sessionToken string, secure bool) {
  125. http.SetCookie(c.Writer, &http.Cookie{
  126. Name: oauthPendingSessionCookieName,
  127. Value: encodeCookieValue(sessionToken),
  128. Path: oauthPendingSessionCookiePath,
  129. MaxAge: oauthPendingCookieMaxAgeSec,
  130. HttpOnly: true,
  131. Secure: secure,
  132. SameSite: http.SameSiteLaxMode,
  133. })
  134. }
  135. func clearOAuthPendingSessionCookie(c *gin.Context, secure bool) {
  136. http.SetCookie(c.Writer, &http.Cookie{
  137. Name: oauthPendingSessionCookieName,
  138. Value: "",
  139. Path: oauthPendingSessionCookiePath,
  140. MaxAge: -1,
  141. HttpOnly: true,
  142. Secure: secure,
  143. SameSite: http.SameSiteLaxMode,
  144. })
  145. }
  146. func readOAuthPendingSessionCookie(c *gin.Context) (string, error) {
  147. return readCookieDecoded(c, oauthPendingSessionCookieName)
  148. }
  149. func captureOAuthPromoCode(c *gin.Context, secure bool) {
  150. promoCode := strings.TrimSpace(c.Query("promo_code"))
  151. if promoCode == "" {
  152. clearOAuthPromoCodeCookie(c, secure)
  153. return
  154. }
  155. http.SetCookie(c.Writer, &http.Cookie{
  156. Name: oauthPromoCodeCookieName,
  157. Value: encodeCookieValue(promoCode),
  158. Path: oauthPendingBrowserCookiePath,
  159. MaxAge: oauthPendingCookieMaxAgeSec,
  160. HttpOnly: true,
  161. Secure: secure,
  162. SameSite: http.SameSiteLaxMode,
  163. })
  164. }
  165. func clearOAuthPromoCodeCookie(c *gin.Context, secure bool) {
  166. http.SetCookie(c.Writer, &http.Cookie{
  167. Name: oauthPromoCodeCookieName,
  168. Value: "",
  169. Path: oauthPendingBrowserCookiePath,
  170. MaxAge: -1,
  171. HttpOnly: true,
  172. Secure: secure,
  173. SameSite: http.SameSiteLaxMode,
  174. })
  175. }
  176. func readOAuthPromoCode(c *gin.Context) string {
  177. if c == nil {
  178. return ""
  179. }
  180. promoCode, err := readCookieDecoded(c, oauthPromoCodeCookieName)
  181. if err != nil {
  182. return ""
  183. }
  184. return strings.TrimSpace(promoCode)
  185. }
  186. func pendingOAuthPromoCode(session *dbent.PendingAuthSession) string {
  187. if session == nil {
  188. return ""
  189. }
  190. return pendingSessionStringValue(session.LocalFlowState, oauthPromoCodeStateKey)
  191. }
  192. func redirectToFrontendCallback(c *gin.Context, frontendCallback string) {
  193. u, err := url.Parse(frontendCallback)
  194. if err != nil {
  195. c.Redirect(http.StatusFound, linuxDoOAuthDefaultRedirectTo)
  196. return
  197. }
  198. if u.Scheme != "" && !strings.EqualFold(u.Scheme, "http") && !strings.EqualFold(u.Scheme, "https") {
  199. c.Redirect(http.StatusFound, linuxDoOAuthDefaultRedirectTo)
  200. return
  201. }
  202. u.Fragment = ""
  203. c.Header("Cache-Control", "no-store")
  204. c.Header("Pragma", "no-cache")
  205. c.Redirect(http.StatusFound, u.String())
  206. }
  207. func (h *AuthHandler) createOAuthPendingSession(c *gin.Context, payload oauthPendingSessionPayload) error {
  208. svc, err := h.pendingIdentityService()
  209. if err != nil {
  210. return err
  211. }
  212. localFlowState := map[string]any{
  213. oauthCompletionResponseKey: payload.CompletionResponse,
  214. }
  215. if promoCode := readOAuthPromoCode(c); promoCode != "" {
  216. localFlowState[oauthPromoCodeStateKey] = promoCode
  217. }
  218. session, err := svc.CreatePendingSession(c.Request.Context(), service.CreatePendingAuthSessionInput{
  219. Intent: strings.TrimSpace(payload.Intent),
  220. Identity: payload.Identity,
  221. TargetUserID: payload.TargetUserID,
  222. ResolvedEmail: strings.TrimSpace(payload.ResolvedEmail),
  223. RedirectTo: strings.TrimSpace(payload.RedirectTo),
  224. BrowserSessionKey: strings.TrimSpace(payload.BrowserSessionKey),
  225. UpstreamIdentityClaims: payload.UpstreamIdentityClaims,
  226. LocalFlowState: localFlowState,
  227. })
  228. if err != nil {
  229. slog.Error("pending auth session create failed",
  230. "intent", strings.TrimSpace(payload.Intent),
  231. "provider_type", strings.TrimSpace(payload.Identity.ProviderType),
  232. "provider_key", strings.TrimSpace(payload.Identity.ProviderKey),
  233. "provider_subject_len", len(strings.TrimSpace(payload.Identity.ProviderSubject)),
  234. "resolved_email_len", len(strings.TrimSpace(payload.ResolvedEmail)),
  235. "has_target_user", payload.TargetUserID != nil,
  236. "error", err.Error())
  237. return infraerrors.InternalServer("PENDING_AUTH_SESSION_CREATE_FAILED", "failed to create pending auth session").WithCause(err)
  238. }
  239. setOAuthPendingSessionCookie(c, session.SessionToken, isRequestHTTPS(c))
  240. return nil
  241. }
  242. func readCompletionResponse(session map[string]any) (map[string]any, bool) {
  243. if len(session) == 0 {
  244. return nil, false
  245. }
  246. value, ok := session[oauthCompletionResponseKey]
  247. if !ok {
  248. return nil, false
  249. }
  250. result, ok := value.(map[string]any)
  251. if !ok {
  252. return nil, false
  253. }
  254. return result, true
  255. }
  256. func clonePendingMap(values map[string]any) map[string]any {
  257. if len(values) == 0 {
  258. return map[string]any{}
  259. }
  260. cloned := make(map[string]any, len(values))
  261. for key, value := range values {
  262. cloned[key] = value
  263. }
  264. return cloned
  265. }
  266. func mergePendingCompletionResponse(session *dbent.PendingAuthSession, overrides map[string]any) map[string]any {
  267. payload, _ := readCompletionResponse(session.LocalFlowState)
  268. merged := clonePendingMap(payload)
  269. if strings.TrimSpace(session.RedirectTo) != "" {
  270. if _, exists := merged["redirect"]; !exists {
  271. merged["redirect"] = session.RedirectTo
  272. }
  273. }
  274. for key, value := range overrides {
  275. if value == nil {
  276. delete(merged, key)
  277. continue
  278. }
  279. merged[key] = value
  280. }
  281. applySuggestedProfileToCompletionResponse(merged, session.UpstreamIdentityClaims)
  282. return merged
  283. }
  284. func pendingSessionStringValue(values map[string]any, key string) string {
  285. if len(values) == 0 {
  286. return ""
  287. }
  288. raw, ok := values[key]
  289. if !ok {
  290. return ""
  291. }
  292. value, ok := raw.(string)
  293. if !ok {
  294. return ""
  295. }
  296. return strings.TrimSpace(value)
  297. }
  298. func pendingSessionWantsInvitation(payload map[string]any) bool {
  299. return strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(payload, "error")), "invitation_required")
  300. }
  301. // pendingSessionRequiresEmailCompletion 判断 callback 写入的 completion payload 是否处于"补邮箱"状态。
  302. // 钉钉跨组织/staff 邮箱缺失时进入此状态:前端跳到补邮箱页,exchange 不应走 adoption apply。
  303. func pendingSessionRequiresEmailCompletion(payload map[string]any) bool {
  304. if v, ok := payload["requires_email_completion"].(bool); ok && v {
  305. return true
  306. }
  307. return strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(payload, "step")), "email_completion")
  308. }
  309. // pendingSessionRequiresBindLogin 判断 callback 写入的 completion payload 是否处于"必须绑定已有账户"状态。
  310. // 钉钉 signupBlocked=true(注册关 + 钉钉企业豁免关)时进入此状态:前端渲染 bind_login 表单,
  311. // exchange 不应消费 session,否则后续 /pending/bind-login 找不到 session。
  312. func pendingSessionRequiresBindLogin(payload map[string]any) bool {
  313. return strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(payload, "step")), "bind_login_required")
  314. }
  315. func pendingOAuthCompletionCanIssueTokenPair(session *dbent.PendingAuthSession, payload map[string]any) bool {
  316. if session == nil {
  317. return false
  318. }
  319. if !strings.EqualFold(strings.TrimSpace(session.Intent), oauthIntentLogin) {
  320. return false
  321. }
  322. if session.TargetUserID == nil || *session.TargetUserID <= 0 {
  323. return false
  324. }
  325. if pendingSessionWantsInvitation(payload) {
  326. return false
  327. }
  328. return strings.TrimSpace(pendingSessionStringValue(payload, "step")) == ""
  329. }
  330. func ensurePendingOAuthCompleteRegistrationSession(session *dbent.PendingAuthSession) error {
  331. if session == nil {
  332. return infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  333. }
  334. if strings.TrimSpace(session.Intent) != oauthIntentLogin {
  335. return infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  336. }
  337. if session.TargetUserID != nil && *session.TargetUserID > 0 {
  338. return infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  339. }
  340. payload, _ := readCompletionResponse(session.LocalFlowState)
  341. if strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(payload, "step")), "bind_login_required") {
  342. return infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  343. }
  344. return nil
  345. }
  346. func buildLegacyCompleteRegistrationPendingResponse(
  347. session *dbent.PendingAuthSession,
  348. forceEmailOnSignup bool,
  349. emailVerificationRequired bool,
  350. ) map[string]any {
  351. completionResponse := normalizePendingOAuthCompletionResponse(mergePendingCompletionResponse(session, map[string]any{
  352. "step": oauthPendingChoiceStep,
  353. "adoption_required": true,
  354. "create_account_allowed": true,
  355. "force_email_on_signup": forceEmailOnSignup,
  356. }))
  357. if email := strings.TrimSpace(session.ResolvedEmail); email != "" {
  358. if _, exists := completionResponse["email"]; !exists {
  359. completionResponse["email"] = email
  360. }
  361. if _, exists := completionResponse["resolved_email"]; !exists {
  362. completionResponse["resolved_email"] = email
  363. }
  364. }
  365. if _, exists := completionResponse["choice_reason"]; !exists {
  366. switch {
  367. case forceEmailOnSignup:
  368. completionResponse["choice_reason"] = "force_email_on_signup"
  369. case emailVerificationRequired:
  370. completionResponse["choice_reason"] = "email_verification_required"
  371. default:
  372. completionResponse["choice_reason"] = "third_party_signup"
  373. }
  374. }
  375. return completionResponse
  376. }
  377. func (h *AuthHandler) legacyCompleteRegistrationSessionStatus(
  378. c *gin.Context,
  379. session *dbent.PendingAuthSession,
  380. ) (*dbent.PendingAuthSession, bool, error) {
  381. if session == nil {
  382. return nil, false, infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  383. }
  384. payload := normalizePendingOAuthCompletionResponse(mergePendingCompletionResponse(session, nil))
  385. if step := pendingSessionStringValue(payload, "step"); step != "" {
  386. return session, true, nil
  387. }
  388. emailVerificationRequired := h != nil && h.authService != nil && h.authService.IsEmailVerifyEnabled(c.Request.Context())
  389. forceEmailOnSignup := h.isForceEmailOnThirdPartySignup(c.Request.Context())
  390. if !emailVerificationRequired && !forceEmailOnSignup {
  391. return session, false, nil
  392. }
  393. client := h.entClient()
  394. if client == nil {
  395. return nil, false, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
  396. }
  397. updatedSession, err := updatePendingOAuthSessionProgress(
  398. c.Request.Context(),
  399. client,
  400. session,
  401. strings.TrimSpace(session.Intent),
  402. strings.TrimSpace(session.ResolvedEmail),
  403. nil,
  404. buildLegacyCompleteRegistrationPendingResponse(session, forceEmailOnSignup, emailVerificationRequired),
  405. )
  406. if err != nil {
  407. return nil, false, infraerrors.InternalServer("PENDING_AUTH_SESSION_UPDATE_FAILED", "failed to update pending oauth session").WithCause(err)
  408. }
  409. return updatedSession, true, nil
  410. }
  411. func (r oauthAdoptionDecisionRequest) hasDecision() bool {
  412. return r.AdoptDisplayName != nil || r.AdoptAvatar != nil
  413. }
  414. func bindOptionalOAuthAdoptionDecision(c *gin.Context) (oauthAdoptionDecisionRequest, error) {
  415. var req oauthAdoptionDecisionRequest
  416. if c == nil || c.Request == nil || c.Request.Body == nil {
  417. return req, nil
  418. }
  419. if err := c.ShouldBindJSON(&req); err != nil {
  420. if errors.Is(err, io.EOF) {
  421. return req, nil
  422. }
  423. return req, err
  424. }
  425. return req, nil
  426. }
  427. func cloneOAuthMetadata(values map[string]any) map[string]any {
  428. if len(values) == 0 {
  429. return map[string]any{}
  430. }
  431. cloned := make(map[string]any, len(values))
  432. for key, value := range values {
  433. cloned[key] = value
  434. }
  435. return cloned
  436. }
  437. func mergeOAuthMetadata(base map[string]any, overlay map[string]any) map[string]any {
  438. merged := cloneOAuthMetadata(base)
  439. for key, value := range overlay {
  440. merged[key] = value
  441. }
  442. return merged
  443. }
  444. func normalizeAdoptedOAuthDisplayName(value string) string {
  445. value = strings.TrimSpace(value)
  446. if len([]rune(value)) > 100 {
  447. value = string([]rune(value)[:100])
  448. }
  449. return value
  450. }
  451. func (h *AuthHandler) entClient() *dbent.Client {
  452. if h == nil || h.authService == nil {
  453. return nil
  454. }
  455. return h.authService.EntClient()
  456. }
  457. func (h *AuthHandler) isForceEmailOnThirdPartySignup(ctx context.Context) bool {
  458. if h == nil || h.settingSvc == nil {
  459. return false
  460. }
  461. defaults, err := h.settingSvc.GetAuthSourceDefaultSettings(ctx)
  462. if err != nil || defaults == nil {
  463. return false
  464. }
  465. return defaults.ForceEmailOnThirdPartySignup
  466. }
  467. func (h *AuthHandler) findOAuthIdentityUser(ctx context.Context, identity service.PendingAuthIdentityKey) (*dbent.User, error) {
  468. client := h.entClient()
  469. if client == nil {
  470. return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
  471. }
  472. record, err := client.AuthIdentity.Query().
  473. Where(
  474. authidentity.ProviderTypeEQ(strings.TrimSpace(identity.ProviderType)),
  475. authidentity.ProviderKeyEQ(strings.TrimSpace(identity.ProviderKey)),
  476. authidentity.ProviderSubjectEQ(strings.TrimSpace(identity.ProviderSubject)),
  477. ).
  478. Only(ctx)
  479. if err != nil {
  480. if dbent.IsNotFound(err) {
  481. return nil, nil
  482. }
  483. return nil, infraerrors.InternalServer("AUTH_IDENTITY_LOOKUP_FAILED", "failed to inspect auth identity ownership").WithCause(err)
  484. }
  485. return findActiveUserByID(ctx, client, record.UserID)
  486. }
  487. func (h *AuthHandler) BindLinuxDoOAuthLogin(c *gin.Context) { h.bindPendingOAuthLogin(c, "linuxdo") }
  488. func (h *AuthHandler) BindOIDCOAuthLogin(c *gin.Context) { h.bindPendingOAuthLogin(c, "oidc") }
  489. func (h *AuthHandler) BindWeChatOAuthLogin(c *gin.Context) { h.bindPendingOAuthLogin(c, "wechat") }
  490. func (h *AuthHandler) BindPendingOAuthLogin(c *gin.Context) { h.bindPendingOAuthLogin(c, "") }
  491. func (h *AuthHandler) CreateLinuxDoOAuthAccount(c *gin.Context) {
  492. h.createPendingOAuthAccount(c, "linuxdo")
  493. }
  494. func (h *AuthHandler) CreateOIDCOAuthAccount(c *gin.Context) { h.createPendingOAuthAccount(c, "oidc") }
  495. func (h *AuthHandler) CreateWeChatOAuthAccount(c *gin.Context) {
  496. h.createPendingOAuthAccount(c, "wechat")
  497. }
  498. func (h *AuthHandler) CreatePendingOAuthAccount(c *gin.Context) {
  499. h.createPendingOAuthAccount(c, "")
  500. }
  501. // SendPendingOAuthVerifyCode sends a verification code for a browser-bound
  502. // pending OAuth account-creation flow.
  503. // POST /api/v1/auth/oauth/pending/send-verify-code
  504. func (h *AuthHandler) SendPendingOAuthVerifyCode(c *gin.Context) {
  505. var req sendPendingOAuthVerifyCodeRequest
  506. if err := c.ShouldBindJSON(&req); err != nil {
  507. response.BadRequest(c, "Invalid request: "+err.Error())
  508. return
  509. }
  510. proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
  511. if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
  512. response.ErrorFrom(c, err)
  513. return
  514. }
  515. _, session, _, err := readPendingOAuthBrowserSession(c, h)
  516. if err != nil {
  517. response.ErrorFrom(c, err)
  518. return
  519. }
  520. if err := ensurePendingOAuthCompleteRegistrationSession(session); err != nil {
  521. response.ErrorFrom(c, err)
  522. return
  523. }
  524. client := h.entClient()
  525. if client == nil {
  526. response.ErrorFrom(c, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready"))
  527. return
  528. }
  529. email := strings.TrimSpace(strings.ToLower(req.Email))
  530. if existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email); err == nil && existingUser != nil {
  531. session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, existingUser, email)
  532. if err != nil {
  533. response.ErrorFrom(c, err)
  534. return
  535. }
  536. c.JSON(http.StatusOK, buildPendingOAuthSessionStatusPayload(session))
  537. return
  538. } else if err != nil && !errors.Is(err, service.ErrUserNotFound) {
  539. response.ErrorFrom(c, err)
  540. return
  541. }
  542. result, err := h.authService.SendPendingOAuthVerifyCode(c.Request.Context(), req.Email, c.GetHeader("Accept-Language"))
  543. if err != nil {
  544. response.ErrorFrom(c, err)
  545. return
  546. }
  547. response.Success(c, SendVerifyCodeResponse{
  548. Message: "Verification code sent successfully",
  549. Countdown: result.Countdown,
  550. })
  551. }
  552. func (h *AuthHandler) upsertPendingOAuthAdoptionDecision(
  553. c *gin.Context,
  554. sessionID int64,
  555. req oauthAdoptionDecisionRequest,
  556. ) (*dbent.IdentityAdoptionDecision, error) {
  557. client := h.entClient()
  558. if client == nil {
  559. return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
  560. }
  561. existing, err := client.IdentityAdoptionDecision.Query().
  562. Where(identityadoptiondecision.PendingAuthSessionIDEQ(sessionID)).
  563. Only(c.Request.Context())
  564. if err != nil && !dbent.IsNotFound(err) {
  565. return nil, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_LOAD_FAILED", "failed to load oauth profile adoption decision").WithCause(err)
  566. }
  567. if existing != nil && !req.hasDecision() {
  568. return existing, nil
  569. }
  570. if existing == nil && !req.hasDecision() {
  571. return nil, nil
  572. }
  573. input := service.PendingIdentityAdoptionDecisionInput{
  574. PendingAuthSessionID: sessionID,
  575. }
  576. if existing != nil {
  577. input.AdoptDisplayName = existing.AdoptDisplayName
  578. input.AdoptAvatar = existing.AdoptAvatar
  579. input.IdentityID = existing.IdentityID
  580. }
  581. if req.AdoptDisplayName != nil {
  582. input.AdoptDisplayName = *req.AdoptDisplayName
  583. }
  584. if req.AdoptAvatar != nil {
  585. input.AdoptAvatar = *req.AdoptAvatar
  586. }
  587. svc, err := h.pendingIdentityService()
  588. if err != nil {
  589. return nil, err
  590. }
  591. decision, err := svc.UpsertAdoptionDecision(c.Request.Context(), input)
  592. if err != nil {
  593. return nil, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_SAVE_FAILED", "failed to save oauth profile adoption decision").WithCause(err)
  594. }
  595. return decision, nil
  596. }
  597. func (h *AuthHandler) ensurePendingOAuthAdoptionDecision(
  598. c *gin.Context,
  599. sessionID int64,
  600. req oauthAdoptionDecisionRequest,
  601. ) (*dbent.IdentityAdoptionDecision, error) {
  602. decision, err := h.upsertPendingOAuthAdoptionDecision(c, sessionID, req)
  603. if err != nil {
  604. return nil, err
  605. }
  606. if decision != nil {
  607. return decision, nil
  608. }
  609. svc, err := h.pendingIdentityService()
  610. if err != nil {
  611. return nil, err
  612. }
  613. decision, err = svc.UpsertAdoptionDecision(c.Request.Context(), service.PendingIdentityAdoptionDecisionInput{
  614. PendingAuthSessionID: sessionID,
  615. })
  616. if err != nil {
  617. return nil, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_SAVE_FAILED", "failed to save oauth profile adoption decision").WithCause(err)
  618. }
  619. return decision, nil
  620. }
  621. func updatePendingOAuthSessionProgress(
  622. ctx context.Context,
  623. client *dbent.Client,
  624. session *dbent.PendingAuthSession,
  625. intent string,
  626. resolvedEmail string,
  627. targetUserID *int64,
  628. completionResponse map[string]any,
  629. ) (*dbent.PendingAuthSession, error) {
  630. if client == nil || session == nil {
  631. return nil, infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth session is invalid")
  632. }
  633. localFlowState := clonePendingMap(session.LocalFlowState)
  634. localFlowState[oauthCompletionResponseKey] = clonePendingMap(completionResponse)
  635. update := client.PendingAuthSession.UpdateOneID(session.ID).
  636. SetIntent(strings.TrimSpace(intent)).
  637. SetResolvedEmail(strings.TrimSpace(resolvedEmail)).
  638. SetLocalFlowState(localFlowState)
  639. if targetUserID != nil && *targetUserID > 0 {
  640. update = update.SetTargetUserID(*targetUserID)
  641. } else {
  642. update = update.ClearTargetUserID()
  643. }
  644. return update.Save(ctx)
  645. }
  646. func resolvePendingOAuthTargetUserID(ctx context.Context, client *dbent.Client, session *dbent.PendingAuthSession) (int64, error) {
  647. if session == nil {
  648. return 0, infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth session is invalid")
  649. }
  650. if session.TargetUserID != nil && *session.TargetUserID > 0 {
  651. return *session.TargetUserID, nil
  652. }
  653. email := strings.TrimSpace(session.ResolvedEmail)
  654. if email == "" {
  655. return 0, infraerrors.BadRequest("PENDING_AUTH_TARGET_USER_MISSING", "pending auth target user is missing")
  656. }
  657. userEntity, err := findUserByNormalizedEmail(ctx, client, email)
  658. if err != nil {
  659. if errors.Is(err, service.ErrUserNotFound) {
  660. return 0, infraerrors.InternalServer("PENDING_AUTH_TARGET_USER_NOT_FOUND", "pending auth target user was not found")
  661. }
  662. return 0, err
  663. }
  664. return userEntity.ID, nil
  665. }
  666. func userNormalizedEmailPredicate(email string) predicate.User {
  667. normalized := strings.ToLower(strings.TrimSpace(email))
  668. if normalized == "" {
  669. return dbuser.EmailEQ(email)
  670. }
  671. return predicate.User(func(s *entsql.Selector) {
  672. s.Where(entsql.P(func(b *entsql.Builder) {
  673. b.WriteString("LOWER(TRIM(").
  674. Ident(s.C(dbuser.FieldEmail)).
  675. WriteString(")) = ").
  676. Arg(normalized)
  677. }))
  678. })
  679. }
  680. func findUserByNormalizedEmail(ctx context.Context, client *dbent.Client, email string) (*dbent.User, error) {
  681. if client == nil {
  682. return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
  683. }
  684. matches, err := client.User.Query().
  685. Where(userNormalizedEmailPredicate(email)).
  686. Order(dbent.Asc(dbuser.FieldID)).
  687. All(ctx)
  688. if err != nil {
  689. return nil, err
  690. }
  691. if len(matches) == 0 {
  692. return nil, service.ErrUserNotFound
  693. }
  694. if len(matches) > 1 {
  695. return nil, infraerrors.Conflict("USER_EMAIL_CONFLICT", "normalized email matched multiple users")
  696. }
  697. return matches[0], nil
  698. }
  699. func ensurePendingOAuthRegistrationIdentityAvailable(ctx context.Context, client *dbent.Client, session *dbent.PendingAuthSession) error {
  700. if client == nil || session == nil {
  701. return infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  702. }
  703. identity, err := client.AuthIdentity.Query().
  704. Where(
  705. authidentity.ProviderTypeEQ(strings.TrimSpace(session.ProviderType)),
  706. authidentity.ProviderKeyEQ(strings.TrimSpace(session.ProviderKey)),
  707. authidentity.ProviderSubjectEQ(strings.TrimSpace(session.ProviderSubject)),
  708. ).
  709. Only(ctx)
  710. if err != nil {
  711. if dbent.IsNotFound(err) {
  712. return nil
  713. }
  714. return err
  715. }
  716. if identity == nil || identity.UserID <= 0 {
  717. return nil
  718. }
  719. activeOwner, err := findActiveUserByID(ctx, client, identity.UserID)
  720. if err != nil {
  721. return err
  722. }
  723. if activeOwner != nil {
  724. return infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user")
  725. }
  726. return nil
  727. }
  728. func oauthIdentityIssuer(session *dbent.PendingAuthSession) *string {
  729. if session == nil {
  730. return nil
  731. }
  732. switch strings.TrimSpace(session.ProviderType) {
  733. case "oidc":
  734. issuer := strings.TrimSpace(session.ProviderKey)
  735. if issuer == "" {
  736. issuer = pendingSessionStringValue(session.UpstreamIdentityClaims, "issuer")
  737. }
  738. if issuer == "" {
  739. return nil
  740. }
  741. return &issuer
  742. default:
  743. issuer := pendingSessionStringValue(session.UpstreamIdentityClaims, "issuer")
  744. if issuer == "" {
  745. return nil
  746. }
  747. return &issuer
  748. }
  749. }
  750. func ensurePendingOAuthIdentityForUser(ctx context.Context, tx *dbent.Tx, session *dbent.PendingAuthSession, userID int64) (*dbent.AuthIdentity, error) {
  751. if session != nil && strings.EqualFold(strings.TrimSpace(session.ProviderType), "wechat") {
  752. return ensurePendingWeChatOAuthIdentityForUser(ctx, tx, session, userID)
  753. }
  754. client := tx.Client()
  755. identity, err := client.AuthIdentity.Query().
  756. Where(
  757. authidentity.ProviderTypeEQ(strings.TrimSpace(session.ProviderType)),
  758. authidentity.ProviderKeyEQ(strings.TrimSpace(session.ProviderKey)),
  759. authidentity.ProviderSubjectEQ(strings.TrimSpace(session.ProviderSubject)),
  760. ).
  761. Only(ctx)
  762. if err != nil && !dbent.IsNotFound(err) {
  763. return nil, err
  764. }
  765. if identity != nil {
  766. if identity.UserID != userID {
  767. activeOwner, err := findActiveUserByID(ctx, client, identity.UserID)
  768. if err != nil {
  769. return nil, err
  770. }
  771. if activeOwner != nil {
  772. return nil, infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user")
  773. }
  774. return client.AuthIdentity.UpdateOneID(identity.ID).
  775. SetUserID(userID).
  776. Save(ctx)
  777. }
  778. return identity, nil
  779. }
  780. create := client.AuthIdentity.Create().
  781. SetUserID(userID).
  782. SetProviderType(strings.TrimSpace(session.ProviderType)).
  783. SetProviderKey(strings.TrimSpace(session.ProviderKey)).
  784. SetProviderSubject(strings.TrimSpace(session.ProviderSubject)).
  785. SetMetadata(cloneOAuthMetadata(session.UpstreamIdentityClaims))
  786. if issuer := oauthIdentityIssuer(session); issuer != nil {
  787. create = create.SetIssuer(strings.TrimSpace(*issuer))
  788. }
  789. return create.Save(ctx)
  790. }
  791. func ensurePendingWeChatOAuthIdentityForUser(ctx context.Context, tx *dbent.Tx, session *dbent.PendingAuthSession, userID int64) (*dbent.AuthIdentity, error) {
  792. client := tx.Client()
  793. providerType := strings.TrimSpace(session.ProviderType)
  794. providerKey := strings.TrimSpace(session.ProviderKey)
  795. providerSubject := strings.TrimSpace(session.ProviderSubject)
  796. providerKeys := wechatCompatibleProviderKeys(providerKey)
  797. channel := strings.TrimSpace(pendingSessionStringValue(session.UpstreamIdentityClaims, "channel"))
  798. channelAppID := strings.TrimSpace(pendingSessionStringValue(session.UpstreamIdentityClaims, "channel_app_id"))
  799. channelSubject := strings.TrimSpace(pendingSessionStringValue(session.UpstreamIdentityClaims, "channel_subject"))
  800. metadata := cloneOAuthMetadata(session.UpstreamIdentityClaims)
  801. identityRecords, err := client.AuthIdentity.Query().
  802. Where(
  803. authidentity.ProviderTypeEQ(providerType),
  804. authidentity.ProviderKeyIn(providerKeys...),
  805. authidentity.ProviderSubjectEQ(providerSubject),
  806. ).
  807. All(ctx)
  808. if err != nil {
  809. return nil, err
  810. }
  811. identity, hasCanonicalKey, err := chooseWeChatIdentityForUser(ctx, client, identityRecords, userID, providerKey)
  812. if err != nil {
  813. return nil, err
  814. }
  815. var legacyOpenIDIdentity *dbent.AuthIdentity
  816. if channelSubject != "" && channelSubject != providerSubject {
  817. legacyOpenIDRecords, err := client.AuthIdentity.Query().
  818. Where(
  819. authidentity.ProviderTypeEQ(providerType),
  820. authidentity.ProviderKeyIn(providerKeys...),
  821. authidentity.ProviderSubjectEQ(channelSubject),
  822. ).
  823. All(ctx)
  824. if err != nil {
  825. return nil, err
  826. }
  827. legacyOpenIDIdentity, _, err = chooseWeChatIdentityForUser(ctx, client, legacyOpenIDRecords, userID, providerKey)
  828. if err != nil {
  829. return nil, err
  830. }
  831. }
  832. switch {
  833. case identity != nil:
  834. update := client.AuthIdentity.UpdateOneID(identity.ID).
  835. SetMetadata(mergeOAuthMetadata(identity.Metadata, metadata))
  836. if identity.UserID != userID {
  837. update = update.SetUserID(userID)
  838. }
  839. if !strings.EqualFold(strings.TrimSpace(identity.ProviderKey), providerKey) && !hasCanonicalKey {
  840. update = update.SetProviderKey(providerKey)
  841. }
  842. if issuer := oauthIdentityIssuer(session); issuer != nil {
  843. update = update.SetIssuer(strings.TrimSpace(*issuer))
  844. }
  845. identity, err = update.Save(ctx)
  846. if err != nil {
  847. return nil, err
  848. }
  849. case legacyOpenIDIdentity != nil:
  850. update := client.AuthIdentity.UpdateOneID(legacyOpenIDIdentity.ID).
  851. SetProviderKey(providerKey).
  852. SetProviderSubject(providerSubject).
  853. SetMetadata(mergeOAuthMetadata(legacyOpenIDIdentity.Metadata, metadata))
  854. if issuer := oauthIdentityIssuer(session); issuer != nil {
  855. update = update.SetIssuer(strings.TrimSpace(*issuer))
  856. }
  857. identity, err = update.Save(ctx)
  858. if err != nil {
  859. return nil, err
  860. }
  861. default:
  862. create := client.AuthIdentity.Create().
  863. SetUserID(userID).
  864. SetProviderType(providerType).
  865. SetProviderKey(providerKey).
  866. SetProviderSubject(providerSubject).
  867. SetMetadata(metadata)
  868. if issuer := oauthIdentityIssuer(session); issuer != nil {
  869. create = create.SetIssuer(strings.TrimSpace(*issuer))
  870. }
  871. identity, err = create.Save(ctx)
  872. if err != nil {
  873. return nil, err
  874. }
  875. }
  876. if channel == "" || channelAppID == "" || channelSubject == "" {
  877. return identity, nil
  878. }
  879. channelRecords, err := client.AuthIdentityChannel.Query().
  880. Where(
  881. authidentitychannel.ProviderTypeEQ(providerType),
  882. authidentitychannel.ProviderKeyIn(providerKeys...),
  883. authidentitychannel.ChannelEQ(channel),
  884. authidentitychannel.ChannelAppIDEQ(channelAppID),
  885. authidentitychannel.ChannelSubjectEQ(channelSubject),
  886. ).
  887. WithIdentity().
  888. All(ctx)
  889. if err != nil {
  890. return nil, err
  891. }
  892. channelRecord, hasCanonicalChannelKey, err := chooseWeChatChannelForUser(ctx, client, channelRecords, userID, providerKey)
  893. if err != nil {
  894. return nil, err
  895. }
  896. channelMetadata := mergeOAuthMetadata(channelRecordMetadata(channelRecord), metadata)
  897. if channelRecord == nil {
  898. if _, err := client.AuthIdentityChannel.Create().
  899. SetIdentityID(identity.ID).
  900. SetProviderType(providerType).
  901. SetProviderKey(providerKey).
  902. SetChannel(channel).
  903. SetChannelAppID(channelAppID).
  904. SetChannelSubject(channelSubject).
  905. SetMetadata(channelMetadata).
  906. Save(ctx); err != nil {
  907. return nil, err
  908. }
  909. return identity, nil
  910. }
  911. updateChannel := client.AuthIdentityChannel.UpdateOneID(channelRecord.ID).
  912. SetIdentityID(identity.ID).
  913. SetMetadata(channelMetadata)
  914. if !strings.EqualFold(strings.TrimSpace(channelRecord.ProviderKey), providerKey) && !hasCanonicalChannelKey {
  915. updateChannel = updateChannel.SetProviderKey(providerKey)
  916. }
  917. _, err = updateChannel.Save(ctx)
  918. if err != nil {
  919. return nil, err
  920. }
  921. return identity, nil
  922. }
  923. func chooseWeChatIdentityForUser(ctx context.Context, client *dbent.Client, records []*dbent.AuthIdentity, userID int64, preferredProviderKey string) (*dbent.AuthIdentity, bool, error) {
  924. var preferred *dbent.AuthIdentity
  925. var fallback *dbent.AuthIdentity
  926. hasCanonicalKey := false
  927. for _, record := range records {
  928. if record == nil {
  929. continue
  930. }
  931. if record.UserID != userID {
  932. activeOwner, err := findActiveUserByID(ctx, client, record.UserID)
  933. if err != nil {
  934. return nil, false, err
  935. }
  936. if activeOwner != nil {
  937. return nil, false, infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user")
  938. }
  939. }
  940. if strings.EqualFold(strings.TrimSpace(record.ProviderKey), preferredProviderKey) {
  941. hasCanonicalKey = true
  942. if preferred == nil {
  943. preferred = record
  944. }
  945. continue
  946. }
  947. if fallback == nil {
  948. fallback = record
  949. }
  950. }
  951. if preferred != nil {
  952. return preferred, hasCanonicalKey, nil
  953. }
  954. return fallback, hasCanonicalKey, nil
  955. }
  956. func chooseWeChatChannelForUser(ctx context.Context, client *dbent.Client, records []*dbent.AuthIdentityChannel, userID int64, preferredProviderKey string) (*dbent.AuthIdentityChannel, bool, error) {
  957. var preferred *dbent.AuthIdentityChannel
  958. var fallback *dbent.AuthIdentityChannel
  959. hasCanonicalKey := false
  960. for _, record := range records {
  961. if record == nil {
  962. continue
  963. }
  964. if record.Edges.Identity != nil && record.Edges.Identity.UserID != userID {
  965. activeOwner, err := findActiveUserByID(ctx, client, record.Edges.Identity.UserID)
  966. if err != nil {
  967. return nil, false, err
  968. }
  969. if activeOwner != nil {
  970. return nil, false, infraerrors.Conflict("AUTH_IDENTITY_CHANNEL_OWNERSHIP_CONFLICT", "auth identity channel already belongs to another user")
  971. }
  972. }
  973. if strings.EqualFold(strings.TrimSpace(record.ProviderKey), preferredProviderKey) {
  974. hasCanonicalKey = true
  975. if preferred == nil {
  976. preferred = record
  977. }
  978. continue
  979. }
  980. if fallback == nil {
  981. fallback = record
  982. }
  983. }
  984. if preferred != nil {
  985. return preferred, hasCanonicalKey, nil
  986. }
  987. return fallback, hasCanonicalKey, nil
  988. }
  989. func findActiveUserByID(ctx context.Context, client *dbent.Client, userID int64) (*dbent.User, error) {
  990. if client == nil || userID <= 0 {
  991. return nil, nil
  992. }
  993. userEntity, err := client.User.Get(ctx, userID)
  994. if err != nil {
  995. if dbent.IsNotFound(err) {
  996. return nil, nil
  997. }
  998. return nil, infraerrors.InternalServer("AUTH_IDENTITY_USER_LOOKUP_FAILED", "failed to load auth identity user").WithCause(err)
  999. }
  1000. if !strings.EqualFold(strings.TrimSpace(userEntity.Status), service.StatusActive) {
  1001. return nil, service.ErrUserNotActive
  1002. }
  1003. return userEntity, nil
  1004. }
  1005. func channelRecordMetadata(channel *dbent.AuthIdentityChannel) map[string]any {
  1006. if channel == nil {
  1007. return map[string]any{}
  1008. }
  1009. return cloneOAuthMetadata(channel.Metadata)
  1010. }
  1011. func shouldBindPendingOAuthIdentity(session *dbent.PendingAuthSession, decision *dbent.IdentityAdoptionDecision) bool {
  1012. if session == nil || decision == nil {
  1013. return false
  1014. }
  1015. switch strings.ToLower(strings.TrimSpace(session.Intent)) {
  1016. case "bind_current_user", "login", "adopt_existing_user_by_email":
  1017. return true
  1018. default:
  1019. return decision.AdoptDisplayName || decision.AdoptAvatar
  1020. }
  1021. }
  1022. func shouldSkipAvatarAdoption(err error) bool {
  1023. return errors.Is(err, service.ErrAvatarInvalid) ||
  1024. errors.Is(err, service.ErrAvatarTooLarge) ||
  1025. errors.Is(err, service.ErrAvatarNotImage)
  1026. }
  1027. func applyPendingOAuthBinding(
  1028. ctx context.Context,
  1029. client *dbent.Client,
  1030. authService *service.AuthService,
  1031. userService *service.UserService,
  1032. session *dbent.PendingAuthSession,
  1033. decision *dbent.IdentityAdoptionDecision,
  1034. overrideUserID *int64,
  1035. forceBind bool,
  1036. applyFirstBindDefaults bool,
  1037. ) error {
  1038. if client == nil || session == nil {
  1039. return nil
  1040. }
  1041. if !forceBind && !shouldBindPendingOAuthIdentity(session, decision) {
  1042. return nil
  1043. }
  1044. if tx := dbent.TxFromContext(ctx); tx != nil {
  1045. return applyPendingOAuthBindingTx(ctx, tx, authService, userService, session, decision, overrideUserID, forceBind, applyFirstBindDefaults)
  1046. }
  1047. tx, err := client.Tx(ctx)
  1048. if err != nil {
  1049. return err
  1050. }
  1051. defer func() { _ = tx.Rollback() }()
  1052. txCtx := dbent.NewTxContext(ctx, tx)
  1053. if err := applyPendingOAuthBindingTx(txCtx, tx, authService, userService, session, decision, overrideUserID, forceBind, applyFirstBindDefaults); err != nil {
  1054. return err
  1055. }
  1056. return tx.Commit()
  1057. }
  1058. func applyPendingOAuthBindingTx(
  1059. ctx context.Context,
  1060. tx *dbent.Tx,
  1061. authService *service.AuthService,
  1062. userService *service.UserService,
  1063. session *dbent.PendingAuthSession,
  1064. decision *dbent.IdentityAdoptionDecision,
  1065. overrideUserID *int64,
  1066. forceBind bool,
  1067. applyFirstBindDefaults bool,
  1068. ) error {
  1069. if tx == nil || session == nil {
  1070. return nil
  1071. }
  1072. if !forceBind && !shouldBindPendingOAuthIdentity(session, decision) {
  1073. return nil
  1074. }
  1075. targetUserID := int64(0)
  1076. if overrideUserID != nil && *overrideUserID > 0 {
  1077. targetUserID = *overrideUserID
  1078. } else {
  1079. resolvedUserID, err := resolvePendingOAuthTargetUserID(ctx, tx.Client(), session)
  1080. if err != nil {
  1081. return err
  1082. }
  1083. targetUserID = resolvedUserID
  1084. }
  1085. adoptedDisplayName := ""
  1086. if decision != nil && decision.AdoptDisplayName {
  1087. adoptedDisplayName = normalizeAdoptedOAuthDisplayName(pendingSessionStringValue(session.UpstreamIdentityClaims, "suggested_display_name"))
  1088. }
  1089. adoptedAvatarURL := ""
  1090. if decision != nil && decision.AdoptAvatar {
  1091. adoptedAvatarURL = pendingSessionStringValue(session.UpstreamIdentityClaims, "suggested_avatar_url")
  1092. }
  1093. shouldAdoptAvatar := false
  1094. if decision != nil && decision.AdoptAvatar && adoptedAvatarURL != "" {
  1095. if err := service.ValidateUserAvatar(adoptedAvatarURL); err == nil {
  1096. shouldAdoptAvatar = true
  1097. } else if !shouldSkipAvatarAdoption(err) {
  1098. return err
  1099. }
  1100. }
  1101. if decision != nil && decision.AdoptDisplayName && adoptedDisplayName != "" {
  1102. if err := tx.Client().User.UpdateOneID(targetUserID).
  1103. SetUsername(adoptedDisplayName).
  1104. Exec(ctx); err != nil {
  1105. return err
  1106. }
  1107. }
  1108. identity, err := ensurePendingOAuthIdentityForUser(ctx, tx, session, targetUserID)
  1109. if err != nil {
  1110. return err
  1111. }
  1112. metadata := cloneOAuthMetadata(identity.Metadata)
  1113. for key, value := range session.UpstreamIdentityClaims {
  1114. metadata[key] = value
  1115. }
  1116. if decision != nil && decision.AdoptDisplayName && adoptedDisplayName != "" {
  1117. metadata["display_name"] = adoptedDisplayName
  1118. }
  1119. if shouldAdoptAvatar {
  1120. metadata["avatar_url"] = adoptedAvatarURL
  1121. }
  1122. updateIdentity := tx.Client().AuthIdentity.UpdateOneID(identity.ID).SetMetadata(metadata)
  1123. if issuer := oauthIdentityIssuer(session); issuer != nil {
  1124. updateIdentity = updateIdentity.SetIssuer(strings.TrimSpace(*issuer))
  1125. }
  1126. if _, err := updateIdentity.Save(ctx); err != nil {
  1127. return err
  1128. }
  1129. if decision != nil && (decision.IdentityID == nil || *decision.IdentityID != identity.ID) {
  1130. if _, err := tx.Client().IdentityAdoptionDecision.Update().
  1131. Where(
  1132. identityadoptiondecision.IdentityIDEQ(identity.ID),
  1133. identityadoptiondecision.IDNEQ(decision.ID),
  1134. ).
  1135. ClearIdentityID().
  1136. Save(ctx); err != nil {
  1137. return err
  1138. }
  1139. if _, err := tx.Client().IdentityAdoptionDecision.UpdateOneID(decision.ID).
  1140. SetIdentityID(identity.ID).
  1141. Save(ctx); err != nil {
  1142. return err
  1143. }
  1144. }
  1145. if applyFirstBindDefaults && authService != nil {
  1146. if err := authService.ApplyProviderDefaultSettingsOnFirstBind(ctx, targetUserID, session.ProviderType); err != nil {
  1147. return err
  1148. }
  1149. }
  1150. if shouldAdoptAvatar && userService != nil {
  1151. if _, err := userService.SetAvatar(ctx, targetUserID, adoptedAvatarURL); err != nil {
  1152. return err
  1153. }
  1154. }
  1155. return nil
  1156. }
  1157. func consumePendingOAuthBrowserSessionTx(
  1158. ctx context.Context,
  1159. tx *dbent.Tx,
  1160. session *dbent.PendingAuthSession,
  1161. ) error {
  1162. if tx == nil || session == nil {
  1163. return service.ErrPendingAuthSessionNotFound
  1164. }
  1165. storedSession, err := tx.Client().PendingAuthSession.Get(ctx, session.ID)
  1166. if err != nil {
  1167. if dbent.IsNotFound(err) {
  1168. return service.ErrPendingAuthSessionNotFound
  1169. }
  1170. return err
  1171. }
  1172. now := time.Now().UTC()
  1173. if storedSession.ConsumedAt != nil {
  1174. return service.ErrPendingAuthSessionConsumed
  1175. }
  1176. if !storedSession.ExpiresAt.IsZero() && now.After(storedSession.ExpiresAt) {
  1177. return service.ErrPendingAuthSessionExpired
  1178. }
  1179. if strings.TrimSpace(storedSession.BrowserSessionKey) != "" &&
  1180. strings.TrimSpace(storedSession.BrowserSessionKey) != strings.TrimSpace(session.BrowserSessionKey) {
  1181. return service.ErrPendingAuthBrowserMismatch
  1182. }
  1183. if _, err := tx.Client().PendingAuthSession.UpdateOneID(storedSession.ID).
  1184. SetConsumedAt(now).
  1185. SetCompletionCodeHash("").
  1186. ClearCompletionCodeExpiresAt().
  1187. Save(ctx); err != nil {
  1188. return err
  1189. }
  1190. return nil
  1191. }
  1192. func applyPendingOAuthAdoptionAndConsumeSession(
  1193. ctx context.Context,
  1194. client *dbent.Client,
  1195. authService *service.AuthService,
  1196. userService *service.UserService,
  1197. session *dbent.PendingAuthSession,
  1198. decision *dbent.IdentityAdoptionDecision,
  1199. userID int64,
  1200. ) error {
  1201. if client == nil {
  1202. return infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
  1203. }
  1204. if session == nil || userID <= 0 {
  1205. return infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth registration context is invalid")
  1206. }
  1207. tx, err := client.Tx(ctx)
  1208. if err != nil {
  1209. return err
  1210. }
  1211. defer func() { _ = tx.Rollback() }()
  1212. txCtx := dbent.NewTxContext(ctx, tx)
  1213. if err := applyPendingOAuthAdoption(txCtx, client, authService, userService, session, decision, &userID); err != nil {
  1214. return err
  1215. }
  1216. if err := consumePendingOAuthBrowserSessionTx(txCtx, tx, session); err != nil {
  1217. return err
  1218. }
  1219. return tx.Commit()
  1220. }
  1221. func applyPendingOAuthAdoption(
  1222. ctx context.Context,
  1223. client *dbent.Client,
  1224. authService *service.AuthService,
  1225. userService *service.UserService,
  1226. session *dbent.PendingAuthSession,
  1227. decision *dbent.IdentityAdoptionDecision,
  1228. overrideUserID *int64,
  1229. ) error {
  1230. return applyPendingOAuthBinding(
  1231. ctx,
  1232. client,
  1233. authService,
  1234. userService,
  1235. session,
  1236. decision,
  1237. overrideUserID,
  1238. false,
  1239. strings.EqualFold(strings.TrimSpace(session.Intent), "bind_current_user"),
  1240. )
  1241. }
  1242. func applySuggestedProfileToCompletionResponse(payload map[string]any, upstream map[string]any) {
  1243. if len(payload) == 0 || len(upstream) == 0 {
  1244. return
  1245. }
  1246. displayName := pendingSessionStringValue(upstream, "suggested_display_name")
  1247. avatarURL := pendingSessionStringValue(upstream, "suggested_avatar_url")
  1248. if displayName != "" {
  1249. if _, exists := payload["suggested_display_name"]; !exists {
  1250. payload["suggested_display_name"] = displayName
  1251. }
  1252. }
  1253. if avatarURL != "" {
  1254. if _, exists := payload["suggested_avatar_url"]; !exists {
  1255. payload["suggested_avatar_url"] = avatarURL
  1256. }
  1257. }
  1258. if displayName != "" || avatarURL != "" {
  1259. payload["adoption_required"] = true
  1260. }
  1261. }
  1262. func pendingOAuthIdentityExistsForUser(
  1263. ctx context.Context,
  1264. client *dbent.Client,
  1265. session *dbent.PendingAuthSession,
  1266. userID int64,
  1267. ) (bool, error) {
  1268. if client == nil || session == nil || userID <= 0 {
  1269. return false, nil
  1270. }
  1271. providerType := strings.TrimSpace(session.ProviderType)
  1272. providerKey := strings.TrimSpace(session.ProviderKey)
  1273. providerSubject := strings.TrimSpace(session.ProviderSubject)
  1274. if providerType == "" || providerSubject == "" {
  1275. return false, nil
  1276. }
  1277. query := client.AuthIdentity.Query().
  1278. Where(
  1279. authidentity.ProviderTypeEQ(providerType),
  1280. authidentity.ProviderSubjectEQ(providerSubject),
  1281. authidentity.UserIDEQ(userID),
  1282. )
  1283. if strings.EqualFold(providerType, "wechat") {
  1284. query = query.Where(authidentity.ProviderKeyIn(wechatCompatibleProviderKeys(providerKey)...))
  1285. } else if providerKey != "" {
  1286. query = query.Where(authidentity.ProviderKeyEQ(providerKey))
  1287. }
  1288. count, err := query.Count(ctx)
  1289. if err != nil {
  1290. return false, infraerrors.InternalServer("AUTH_IDENTITY_LOOKUP_FAILED", "failed to inspect auth identity ownership").WithCause(err)
  1291. }
  1292. return count > 0, nil
  1293. }
  1294. func (h *AuthHandler) shouldSkipPendingOAuthAdoptionPrompt(
  1295. ctx context.Context,
  1296. session *dbent.PendingAuthSession,
  1297. payload map[string]any,
  1298. ) (bool, error) {
  1299. if session == nil || len(payload) == 0 {
  1300. return false, nil
  1301. }
  1302. if !pendingOAuthCompletionCanIssueTokenPair(session, payload) {
  1303. return false, nil
  1304. }
  1305. if pendingSessionStringValue(session.UpstreamIdentityClaims, "suggested_display_name") == "" &&
  1306. pendingSessionStringValue(session.UpstreamIdentityClaims, "suggested_avatar_url") == "" {
  1307. return false, nil
  1308. }
  1309. return pendingOAuthIdentityExistsForUser(ctx, h.entClient(), session, *session.TargetUserID)
  1310. }
  1311. func readPendingOAuthBrowserSession(c *gin.Context, h *AuthHandler) (*service.AuthPendingIdentityService, *dbent.PendingAuthSession, func(), error) {
  1312. secureCookie := isRequestHTTPS(c)
  1313. clearCookies := func() {
  1314. clearOAuthPendingSessionCookie(c, secureCookie)
  1315. clearOAuthPendingBrowserCookie(c, secureCookie)
  1316. }
  1317. sessionToken, err := readOAuthPendingSessionCookie(c)
  1318. if err != nil || strings.TrimSpace(sessionToken) == "" {
  1319. clearCookies()
  1320. return nil, nil, clearCookies, service.ErrPendingAuthSessionNotFound
  1321. }
  1322. browserSessionKey, err := readOAuthPendingBrowserCookie(c)
  1323. if err != nil || strings.TrimSpace(browserSessionKey) == "" {
  1324. clearCookies()
  1325. return nil, nil, clearCookies, service.ErrPendingAuthBrowserMismatch
  1326. }
  1327. svc, err := h.pendingIdentityService()
  1328. if err != nil {
  1329. clearCookies()
  1330. return nil, nil, clearCookies, err
  1331. }
  1332. session, err := svc.GetBrowserSession(c.Request.Context(), sessionToken, browserSessionKey)
  1333. if err != nil {
  1334. clearCookies()
  1335. return nil, nil, clearCookies, err
  1336. }
  1337. return svc, session, clearCookies, nil
  1338. }
  1339. func (h *AuthHandler) consumePendingOAuthSessionOnLogout(c *gin.Context) {
  1340. if c == nil || c.Request == nil {
  1341. return
  1342. }
  1343. sessionToken, err := readOAuthPendingSessionCookie(c)
  1344. if err != nil || strings.TrimSpace(sessionToken) == "" {
  1345. return
  1346. }
  1347. browserSessionKey, err := readOAuthPendingBrowserCookie(c)
  1348. if err != nil || strings.TrimSpace(browserSessionKey) == "" {
  1349. return
  1350. }
  1351. svc, err := h.pendingIdentityService()
  1352. if err != nil {
  1353. return
  1354. }
  1355. _, _ = svc.ConsumeBrowserSession(c.Request.Context(), sessionToken, browserSessionKey)
  1356. }
  1357. func clearOAuthLogoutCookies(c *gin.Context) {
  1358. secureCookie := isRequestHTTPS(c)
  1359. clearOAuthPendingSessionCookie(c, secureCookie)
  1360. clearOAuthPendingBrowserCookie(c, secureCookie)
  1361. clearOAuthBindAccessTokenCookie(c, secureCookie)
  1362. clearCookie(c, linuxDoOAuthStateCookieName, secureCookie)
  1363. clearCookie(c, linuxDoOAuthVerifierCookie, secureCookie)
  1364. clearCookie(c, linuxDoOAuthRedirectCookie, secureCookie)
  1365. clearCookie(c, linuxDoOAuthIntentCookieName, secureCookie)
  1366. clearCookie(c, linuxDoOAuthBindUserCookieName, secureCookie)
  1367. oidcClearCookie(c, oidcOAuthStateCookieName, secureCookie)
  1368. oidcClearCookie(c, oidcOAuthVerifierCookie, secureCookie)
  1369. oidcClearCookie(c, oidcOAuthRedirectCookie, secureCookie)
  1370. oidcClearCookie(c, oidcOAuthNonceCookie, secureCookie)
  1371. oidcClearCookie(c, oidcOAuthIntentCookieName, secureCookie)
  1372. oidcClearCookie(c, oidcOAuthBindUserCookieName, secureCookie)
  1373. wechatClearCookie(c, wechatOAuthStateCookieName, secureCookie)
  1374. wechatClearCookie(c, wechatOAuthRedirectCookieName, secureCookie)
  1375. wechatClearCookie(c, wechatOAuthIntentCookieName, secureCookie)
  1376. wechatClearCookie(c, wechatOAuthModeCookieName, secureCookie)
  1377. wechatClearCookie(c, wechatOAuthBindUserCookieName, secureCookie)
  1378. wechatPaymentClearCookie(c, wechatPaymentOAuthStateName, secureCookie)
  1379. wechatPaymentClearCookie(c, wechatPaymentOAuthRedirect, secureCookie)
  1380. wechatPaymentClearCookie(c, wechatPaymentOAuthContextName, secureCookie)
  1381. wechatPaymentClearCookie(c, wechatPaymentOAuthScope, secureCookie)
  1382. }
  1383. func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gin.H {
  1384. completionResponse := normalizePendingOAuthCompletionResponse(mergePendingCompletionResponse(session, nil))
  1385. payload := gin.H{
  1386. "auth_result": "pending_session",
  1387. "provider": strings.TrimSpace(session.ProviderType),
  1388. "intent": strings.TrimSpace(session.Intent),
  1389. }
  1390. for key, value := range completionResponse {
  1391. payload[key] = value
  1392. }
  1393. if email := strings.TrimSpace(session.ResolvedEmail); email != "" {
  1394. payload["email"] = email
  1395. }
  1396. return payload
  1397. }
  1398. func normalizePendingOAuthCompletionResponse(payload map[string]any) map[string]any {
  1399. normalized := clonePendingMap(payload)
  1400. for _, key := range []string{"access_token", "refresh_token", "expires_in", "token_type"} {
  1401. delete(normalized, key)
  1402. }
  1403. step := strings.ToLower(strings.TrimSpace(pendingSessionStringValue(normalized, "step")))
  1404. // 把多种 choice 别名归一为 oauthPendingChoiceStep;bind_login_required 是独立终态
  1405. // (前端渲染 needsBindLogin 而非 needsChooser),故不能并入归一化列表。
  1406. switch step {
  1407. case "choice", "choose_account_action", "choose_account", "choose", "email_required":
  1408. normalized["step"] = oauthPendingChoiceStep
  1409. }
  1410. if strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(normalized, "step")), oauthPendingChoiceStep) {
  1411. normalized["adoption_required"] = true
  1412. }
  1413. if _, exists := normalized["adoption_required"]; !exists {
  1414. if _, hasChoiceFields := normalized["email_binding_required"]; hasChoiceFields {
  1415. normalized["adoption_required"] = true
  1416. }
  1417. }
  1418. return normalized
  1419. }
  1420. func pendingOAuthChoiceCompletionResponse(session *dbent.PendingAuthSession, email string) map[string]any {
  1421. response := mergePendingCompletionResponse(session, map[string]any{
  1422. "step": oauthPendingChoiceStep,
  1423. "adoption_required": true,
  1424. "force_email_on_signup": true,
  1425. "email_binding_required": true,
  1426. "existing_account_bindable": true,
  1427. })
  1428. if email = strings.TrimSpace(email); email != "" {
  1429. response["email"] = email
  1430. response["resolved_email"] = email
  1431. }
  1432. return response
  1433. }
  1434. func (h *AuthHandler) transitionPendingOAuthAccountToChoiceState(
  1435. c *gin.Context,
  1436. client *dbent.Client,
  1437. session *dbent.PendingAuthSession,
  1438. targetUser *dbent.User,
  1439. email string,
  1440. ) (*dbent.PendingAuthSession, error) {
  1441. completionResponse := pendingOAuthChoiceCompletionResponse(session, email)
  1442. var targetUserID *int64
  1443. if targetUser != nil && targetUser.ID > 0 {
  1444. targetUserID = &targetUser.ID
  1445. }
  1446. session, err := updatePendingOAuthSessionProgress(
  1447. c.Request.Context(),
  1448. client,
  1449. session,
  1450. strings.TrimSpace(session.Intent),
  1451. email,
  1452. targetUserID,
  1453. completionResponse,
  1454. )
  1455. if err != nil {
  1456. return nil, infraerrors.InternalServer("PENDING_AUTH_SESSION_UPDATE_FAILED", "failed to update pending oauth session").WithCause(err)
  1457. }
  1458. return session, nil
  1459. }
  1460. func writeOAuthTokenPairResponse(c *gin.Context, tokenPair *service.TokenPair) {
  1461. c.JSON(http.StatusOK, gin.H{
  1462. "access_token": tokenPair.AccessToken,
  1463. "refresh_token": tokenPair.RefreshToken,
  1464. "expires_in": tokenPair.ExpiresIn,
  1465. "token_type": "Bearer",
  1466. })
  1467. }
  1468. func (h *AuthHandler) bindPendingOAuthLogin(c *gin.Context, provider string) {
  1469. var req bindPendingOAuthLoginRequest
  1470. if err := c.ShouldBindJSON(&req); err != nil {
  1471. response.BadRequest(c, "Invalid request: "+err.Error())
  1472. return
  1473. }
  1474. pendingSvc, session, clearCookies, err := readPendingOAuthBrowserSession(c, h)
  1475. if err != nil {
  1476. response.ErrorFrom(c, err)
  1477. return
  1478. }
  1479. if strings.TrimSpace(provider) != "" && !strings.EqualFold(strings.TrimSpace(session.ProviderType), provider) {
  1480. response.BadRequest(c, "Pending oauth session provider mismatch")
  1481. return
  1482. }
  1483. user, err := h.authService.ValidatePasswordCredentials(c.Request.Context(), strings.TrimSpace(req.Email), req.Password)
  1484. if err != nil {
  1485. response.ErrorFrom(c, err)
  1486. return
  1487. }
  1488. if session.TargetUserID != nil && *session.TargetUserID > 0 && user.ID != *session.TargetUserID {
  1489. response.ErrorFrom(c, infraerrors.Conflict("PENDING_AUTH_TARGET_USER_MISMATCH", "pending oauth session must be completed by the targeted user"))
  1490. return
  1491. }
  1492. if err := h.ensureBackendModeAllowsUser(c.Request.Context(), user); err != nil {
  1493. response.ErrorFrom(c, err)
  1494. return
  1495. }
  1496. decision, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, req.adoptionDecision())
  1497. if err != nil {
  1498. response.ErrorFrom(c, err)
  1499. return
  1500. }
  1501. if h.totpService != nil && h.settingSvc.IsTotpEnabled(c.Request.Context()) && user.TotpEnabled {
  1502. tempToken, err := h.totpService.CreatePendingOAuthBindLoginSession(
  1503. c.Request.Context(),
  1504. user.ID,
  1505. user.Email,
  1506. session.SessionToken,
  1507. session.BrowserSessionKey,
  1508. )
  1509. if err != nil {
  1510. response.InternalError(c, "Failed to create 2FA session")
  1511. return
  1512. }
  1513. response.Success(c, TotpLoginResponse{
  1514. Requires2FA: true,
  1515. TempToken: tempToken,
  1516. UserEmailMasked: service.MaskEmail(user.Email),
  1517. })
  1518. return
  1519. }
  1520. if err := applyPendingOAuthBinding(c.Request.Context(), h.entClient(), h.authService, h.userService, session, decision, &user.ID, true, true); err != nil {
  1521. respondPendingOAuthBindingApplyError(c, err)
  1522. return
  1523. }
  1524. h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
  1525. // bindPendingOAuthLogin = 绑定已有账户登录,不动 users.username(用户已有自己的名字)
  1526. h.maybeSyncDingTalkAfterLogin(c.Request.Context(), session, user.ID)
  1527. tokenPair, err := h.authService.GenerateTokenPair(c.Request.Context(), user, "")
  1528. if err != nil {
  1529. response.InternalError(c, "Failed to generate token pair")
  1530. return
  1531. }
  1532. if _, err := pendingSvc.ConsumeBrowserSession(c.Request.Context(), session.SessionToken, session.BrowserSessionKey); err != nil {
  1533. clearCookies()
  1534. response.ErrorFrom(c, err)
  1535. return
  1536. }
  1537. clearCookies()
  1538. writeOAuthTokenPairResponse(c, tokenPair)
  1539. }
  1540. func respondPendingOAuthBindingApplyError(c *gin.Context, err error) {
  1541. if code := infraerrors.Code(err); code >= http.StatusBadRequest && code < http.StatusInternalServerError {
  1542. response.ErrorFrom(c, err)
  1543. return
  1544. }
  1545. response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_BIND_APPLY_FAILED", "failed to bind pending oauth identity").WithCause(err))
  1546. }
  1547. func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string) {
  1548. var req createPendingOAuthAccountRequest
  1549. if err := c.ShouldBindJSON(&req); err != nil {
  1550. response.BadRequest(c, "Invalid request: "+err.Error())
  1551. return
  1552. }
  1553. _, session, clearCookies, err := readPendingOAuthBrowserSession(c, h)
  1554. if err != nil {
  1555. response.ErrorFrom(c, err)
  1556. return
  1557. }
  1558. if err := ensurePendingOAuthCompleteRegistrationSession(session); err != nil {
  1559. response.ErrorFrom(c, err)
  1560. return
  1561. }
  1562. if strings.TrimSpace(provider) != "" && !strings.EqualFold(strings.TrimSpace(session.ProviderType), provider) {
  1563. response.BadRequest(c, "Pending oauth session provider mismatch")
  1564. return
  1565. }
  1566. client := h.entClient()
  1567. if client == nil {
  1568. response.ErrorFrom(c, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready"))
  1569. return
  1570. }
  1571. email := strings.TrimSpace(strings.ToLower(req.Email))
  1572. existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email)
  1573. if err != nil {
  1574. switch {
  1575. case errors.Is(err, service.ErrUserNotFound):
  1576. existingUser = nil
  1577. case infraerrors.Code(err) >= http.StatusBadRequest && infraerrors.Code(err) < http.StatusInternalServerError:
  1578. response.ErrorFrom(c, err)
  1579. return
  1580. default:
  1581. response.ErrorFrom(c, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable"))
  1582. return
  1583. }
  1584. }
  1585. if existingUser != nil {
  1586. session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, existingUser, email)
  1587. if err != nil {
  1588. response.ErrorFrom(c, err)
  1589. return
  1590. }
  1591. c.JSON(http.StatusOK, buildPendingOAuthSessionStatusPayload(session))
  1592. return
  1593. }
  1594. if err := h.ensureBackendModeAllowsNewUserLogin(c.Request.Context()); err != nil {
  1595. response.ErrorFrom(c, err)
  1596. return
  1597. }
  1598. proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
  1599. if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
  1600. response.ErrorFrom(c, err)
  1601. return
  1602. }
  1603. tokenPair, user, err := h.authService.RegisterOAuthEmailAccount(
  1604. c.Request.Context(),
  1605. email,
  1606. req.Password,
  1607. strings.TrimSpace(req.VerifyCode),
  1608. strings.TrimSpace(req.InvitationCode),
  1609. strings.TrimSpace(session.ProviderType),
  1610. )
  1611. if err != nil {
  1612. if errors.Is(err, service.ErrEmailExists) {
  1613. existingUser, lookupErr := findUserByNormalizedEmail(c.Request.Context(), client, email)
  1614. if lookupErr != nil {
  1615. response.ErrorFrom(c, lookupErr)
  1616. return
  1617. }
  1618. session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, existingUser, email)
  1619. if err != nil {
  1620. response.ErrorFrom(c, err)
  1621. return
  1622. }
  1623. c.JSON(http.StatusOK, buildPendingOAuthSessionStatusPayload(session))
  1624. return
  1625. }
  1626. response.ErrorFrom(c, err)
  1627. return
  1628. }
  1629. rollbackCreatedUser := func(originalErr error) bool {
  1630. if user == nil || user.ID <= 0 {
  1631. return false
  1632. }
  1633. if rollbackErr := h.authService.RollbackOAuthEmailAccountCreation(
  1634. c.Request.Context(),
  1635. user.ID,
  1636. strings.TrimSpace(req.InvitationCode),
  1637. ); rollbackErr != nil {
  1638. response.ErrorFrom(c, infraerrors.InternalServer(
  1639. "PENDING_AUTH_ACCOUNT_ROLLBACK_FAILED",
  1640. "failed to rollback pending oauth account creation",
  1641. ).WithCause(fmt.Errorf("original error: %w; rollback error: %v", originalErr, rollbackErr)))
  1642. return true
  1643. }
  1644. user = nil
  1645. return false
  1646. }
  1647. decision, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, req.adoptionDecision())
  1648. if err != nil {
  1649. if rollbackCreatedUser(err) {
  1650. return
  1651. }
  1652. response.ErrorFrom(c, err)
  1653. return
  1654. }
  1655. tx, err := client.Tx(c.Request.Context())
  1656. if err != nil {
  1657. if rollbackCreatedUser(err) {
  1658. return
  1659. }
  1660. response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_BIND_APPLY_FAILED", "failed to bind pending oauth identity").WithCause(err))
  1661. return
  1662. }
  1663. defer func() { _ = tx.Rollback() }()
  1664. txCtx := dbent.NewTxContext(c.Request.Context(), tx)
  1665. if err := applyPendingOAuthBinding(txCtx, client, h.authService, h.userService, session, decision, &user.ID, true, false); err != nil {
  1666. _ = tx.Rollback()
  1667. if rollbackCreatedUser(err) {
  1668. return
  1669. }
  1670. respondPendingOAuthBindingApplyError(c, err)
  1671. return
  1672. }
  1673. if err := h.authService.FinalizeOAuthEmailAccount(
  1674. txCtx,
  1675. user,
  1676. strings.TrimSpace(req.InvitationCode),
  1677. strings.TrimSpace(session.ProviderType),
  1678. strings.TrimSpace(req.AffCode),
  1679. ); err != nil {
  1680. _ = tx.Rollback()
  1681. if rollbackCreatedUser(err) {
  1682. return
  1683. }
  1684. response.ErrorFrom(c, err)
  1685. return
  1686. }
  1687. if err := consumePendingOAuthBrowserSessionTx(txCtx, tx, session); err != nil {
  1688. _ = tx.Rollback()
  1689. if rollbackCreatedUser(err) {
  1690. return
  1691. }
  1692. clearCookies()
  1693. response.ErrorFrom(c, err)
  1694. return
  1695. }
  1696. if pendingOAuthCreateAccountPreCommitHook != nil {
  1697. if err := pendingOAuthCreateAccountPreCommitHook(txCtx, session); err != nil {
  1698. _ = tx.Rollback()
  1699. if rollbackCreatedUser(err) {
  1700. return
  1701. }
  1702. respondPendingOAuthBindingApplyError(c, err)
  1703. return
  1704. }
  1705. }
  1706. if err := tx.Commit(); err != nil {
  1707. if rollbackCreatedUser(err) {
  1708. return
  1709. }
  1710. response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_BIND_APPLY_FAILED", "failed to bind pending oauth identity").WithCause(err))
  1711. return
  1712. }
  1713. h.authService.ApplyOAuthSignupPromoCode(c.Request.Context(), user.ID, pendingOAuthPromoCode(session))
  1714. h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
  1715. // createPendingOAuthAccount = 注册新账户,需要把钉钉昵称同步到 users.username 作为初始值
  1716. h.maybeSyncDingTalkAfterRegistration(c.Request.Context(), session, user.ID)
  1717. clearCookies()
  1718. writeOAuthTokenPairResponse(c, tokenPair)
  1719. }
  1720. // ExchangePendingOAuthCompletion redeems a pending OAuth browser session into a frontend-safe payload.
  1721. // POST /api/v1/auth/oauth/pending/exchange
  1722. func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
  1723. secureCookie := isRequestHTTPS(c)
  1724. clearCookies := func() {
  1725. clearOAuthPendingSessionCookie(c, secureCookie)
  1726. clearOAuthPendingBrowserCookie(c, secureCookie)
  1727. }
  1728. adoptionDecision, err := bindOptionalOAuthAdoptionDecision(c)
  1729. if err != nil {
  1730. response.BadRequest(c, "Invalid request: "+err.Error())
  1731. return
  1732. }
  1733. sessionToken, err := readOAuthPendingSessionCookie(c)
  1734. if err != nil || strings.TrimSpace(sessionToken) == "" {
  1735. clearCookies()
  1736. response.ErrorFrom(c, service.ErrPendingAuthSessionNotFound)
  1737. return
  1738. }
  1739. browserSessionKey, err := readOAuthPendingBrowserCookie(c)
  1740. if err != nil || strings.TrimSpace(browserSessionKey) == "" {
  1741. clearCookies()
  1742. response.ErrorFrom(c, service.ErrPendingAuthBrowserMismatch)
  1743. return
  1744. }
  1745. svc, err := h.pendingIdentityService()
  1746. if err != nil {
  1747. clearCookies()
  1748. response.ErrorFrom(c, err)
  1749. return
  1750. }
  1751. session, err := svc.GetBrowserSession(c.Request.Context(), sessionToken, browserSessionKey)
  1752. if err != nil {
  1753. clearCookies()
  1754. response.ErrorFrom(c, err)
  1755. return
  1756. }
  1757. payload, ok := readCompletionResponse(session.LocalFlowState)
  1758. if !ok {
  1759. clearCookies()
  1760. response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_COMPLETION_INVALID", "pending auth completion payload is invalid"))
  1761. return
  1762. }
  1763. payload = normalizePendingOAuthCompletionResponse(payload)
  1764. if strings.TrimSpace(session.RedirectTo) != "" {
  1765. if _, exists := payload["redirect"]; !exists {
  1766. payload["redirect"] = session.RedirectTo
  1767. }
  1768. }
  1769. applySuggestedProfileToCompletionResponse(payload, session.UpstreamIdentityClaims)
  1770. canIssueTokenPair := pendingOAuthCompletionCanIssueTokenPair(session, payload)
  1771. var loginUser *service.User
  1772. if canIssueTokenPair {
  1773. loginUser, err = h.userService.GetByID(c.Request.Context(), *session.TargetUserID)
  1774. if err != nil {
  1775. clearCookies()
  1776. response.ErrorFrom(c, err)
  1777. return
  1778. }
  1779. if err := ensureLoginUserActive(loginUser); err != nil {
  1780. clearCookies()
  1781. response.ErrorFrom(c, err)
  1782. return
  1783. }
  1784. if err := h.ensureBackendModeAllowsUser(c.Request.Context(), loginUser); err != nil {
  1785. clearCookies()
  1786. response.ErrorFrom(c, err)
  1787. return
  1788. }
  1789. }
  1790. skipAdoptionPrompt, err := h.shouldSkipPendingOAuthAdoptionPrompt(c.Request.Context(), session, payload)
  1791. if err != nil {
  1792. clearCookies()
  1793. response.ErrorFrom(c, err)
  1794. return
  1795. }
  1796. if skipAdoptionPrompt {
  1797. delete(payload, "adoption_required")
  1798. }
  1799. if pendingSessionWantsInvitation(payload) {
  1800. if adoptionDecision.hasDecision() {
  1801. decision, err := h.upsertPendingOAuthAdoptionDecision(c, session.ID, adoptionDecision)
  1802. if err != nil {
  1803. response.ErrorFrom(c, err)
  1804. return
  1805. }
  1806. _ = decision
  1807. }
  1808. response.Success(c, payload)
  1809. return
  1810. }
  1811. if pendingSessionRequiresEmailCompletion(payload) {
  1812. response.Success(c, payload)
  1813. return
  1814. }
  1815. if pendingSessionRequiresBindLogin(payload) {
  1816. response.Success(c, payload)
  1817. return
  1818. }
  1819. // ─── 安全修复(账号接管 0day)────────────────────────────────────────────
  1820. // 非终态 session(如 choose_account_action_required)的 TargetUserID 可能来自
  1821. // 攻击者提交的他人邮箱:createPendingOAuthAccount / SendPendingOAuthVerifyCode
  1822. // 发现邮箱已存在时会把本 session 指向该邮箱用户,全程无密码、无邮箱验证码、
  1823. // 无账号所有权证明。若此时带着 adoption decision 继续执行,下方的
  1824. // applyPendingOAuthAdoption 会把本 OAuth identity 直接绑定到 TargetUserID,
  1825. // 攻击者随后再次 OAuth 登录即被系统识别为受害者本人(完整账号接管)。
  1826. // 只有两类 session 允许在此处执行 adoption/binding:
  1827. // 1. canIssueTokenPair == true —— 登录终态,identity 已安全绑定该用户;
  1828. // 2. intent == bind_current_user —— 已登录用户主动发起绑定(绑定目标来自登录态 cookie)。
  1829. // 其余状态一律只返回 payload,不绑定、不消费 session。
  1830. if !canIssueTokenPair && !strings.EqualFold(strings.TrimSpace(session.Intent), oauthIntentBindCurrentUser) {
  1831. response.Success(c, payload)
  1832. return
  1833. }
  1834. if !adoptionDecision.hasDecision() {
  1835. adoptionRequired, _ := payload["adoption_required"].(bool)
  1836. if adoptionRequired {
  1837. response.Success(c, payload)
  1838. return
  1839. }
  1840. }
  1841. decisionReq := adoptionDecision
  1842. if !decisionReq.hasDecision() {
  1843. adoptDisplayName := false
  1844. adoptAvatar := false
  1845. decisionReq = oauthAdoptionDecisionRequest{
  1846. AdoptDisplayName: &adoptDisplayName,
  1847. AdoptAvatar: &adoptAvatar,
  1848. }
  1849. }
  1850. decision, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, decisionReq)
  1851. if err != nil {
  1852. response.ErrorFrom(c, err)
  1853. return
  1854. }
  1855. if err := applyPendingOAuthAdoption(c.Request.Context(), h.entClient(), h.authService, h.userService, session, decision, session.TargetUserID); err != nil {
  1856. response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_APPLY_FAILED", "failed to apply oauth profile adoption").WithCause(err))
  1857. return
  1858. }
  1859. if _, err := svc.ConsumeBrowserSession(c.Request.Context(), sessionToken, browserSessionKey); err != nil {
  1860. clearCookies()
  1861. response.ErrorFrom(c, err)
  1862. return
  1863. }
  1864. if canIssueTokenPair {
  1865. tokenPair, err := h.authService.GenerateTokenPair(c.Request.Context(), loginUser, "")
  1866. if err != nil {
  1867. clearCookies()
  1868. response.InternalError(c, "Failed to generate token pair")
  1869. return
  1870. }
  1871. h.authService.RecordSuccessfulLogin(c.Request.Context(), loginUser.ID)
  1872. payload["access_token"] = tokenPair.AccessToken
  1873. payload["refresh_token"] = tokenPair.RefreshToken
  1874. payload["expires_in"] = tokenPair.ExpiresIn
  1875. payload["token_type"] = "Bearer"
  1876. }
  1877. clearCookies()
  1878. response.Success(c, payload)
  1879. }