auth_wechat_oauth_test.go 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  1. //go:build unit
  2. package handler
  3. import (
  4. "bytes"
  5. "context"
  6. "database/sql"
  7. "net/http"
  8. "net/http/httptest"
  9. "net/url"
  10. "strings"
  11. "testing"
  12. "time"
  13. dbent "github.com/Wei-Shaw/sub2api/ent"
  14. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  15. "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
  16. "github.com/Wei-Shaw/sub2api/ent/enttest"
  17. "github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
  18. "github.com/Wei-Shaw/sub2api/ent/pendingauthsession"
  19. dbuser "github.com/Wei-Shaw/sub2api/ent/user"
  20. "github.com/Wei-Shaw/sub2api/internal/config"
  21. "github.com/Wei-Shaw/sub2api/internal/payment"
  22. "github.com/Wei-Shaw/sub2api/internal/repository"
  23. "github.com/Wei-Shaw/sub2api/internal/service"
  24. "github.com/gin-gonic/gin"
  25. "github.com/stretchr/testify/require"
  26. "entgo.io/ent/dialect"
  27. entsql "entgo.io/ent/dialect/sql"
  28. _ "modernc.org/sqlite"
  29. )
  30. func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
  31. gin.SetMode(gin.TestMode)
  32. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, map[string]string{
  33. service.SettingKeyWeChatConnectEnabled: "true",
  34. service.SettingKeyWeChatConnectAppID: "wx-open-app",
  35. service.SettingKeyWeChatConnectAppSecret: "wx-open-secret",
  36. service.SettingKeyWeChatConnectMode: "open",
  37. service.SettingKeyWeChatConnectScopes: "snsapi_login",
  38. service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  39. service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
  40. })
  41. defer client.Close()
  42. recorder := httptest.NewRecorder()
  43. c, _ := gin.CreateTestContext(recorder)
  44. c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing", nil)
  45. c.Request.Host = "api.example.com"
  46. handler.WeChatOAuthStart(c)
  47. require.Equal(t, http.StatusFound, recorder.Code)
  48. location := recorder.Header().Get("Location")
  49. require.NotEmpty(t, location)
  50. require.Contains(t, location, "open.weixin.qq.com")
  51. require.Contains(t, location, "appid=wx-open-app")
  52. require.Contains(t, location, "scope=snsapi_login")
  53. cookies := recorder.Result().Cookies()
  54. require.NotEmpty(t, findCookie(cookies, wechatOAuthStateCookieName))
  55. require.NotEmpty(t, findCookie(cookies, wechatOAuthRedirectCookieName))
  56. require.NotEmpty(t, findCookie(cookies, wechatOAuthModeCookieName))
  57. require.NotEmpty(t, findCookie(cookies, oauthPendingBrowserCookieName))
  58. }
  59. func TestWeChatOAuthStart_AllowsOpenModeWhenBothCapabilitiesEnabled(t *testing.T) {
  60. gin.SetMode(gin.TestMode)
  61. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, map[string]string{
  62. service.SettingKeyWeChatConnectEnabled: "true",
  63. service.SettingKeyWeChatConnectAppID: "wx-shared-app",
  64. service.SettingKeyWeChatConnectAppSecret: "wx-shared-secret",
  65. service.SettingKeyWeChatConnectMode: "mp",
  66. service.SettingKeyWeChatConnectScopes: "snsapi_base",
  67. service.SettingKeyWeChatConnectOpenEnabled: "true",
  68. service.SettingKeyWeChatConnectMPEnabled: "true",
  69. service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  70. service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
  71. })
  72. defer client.Close()
  73. recorder := httptest.NewRecorder()
  74. c, _ := gin.CreateTestContext(recorder)
  75. c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing", nil)
  76. c.Request.Host = "api.example.com"
  77. handler.WeChatOAuthStart(c)
  78. require.Equal(t, http.StatusFound, recorder.Code)
  79. location := recorder.Header().Get("Location")
  80. require.NotEmpty(t, location)
  81. require.Contains(t, location, "open.weixin.qq.com")
  82. require.Contains(t, location, "connect/qrconnect")
  83. require.Contains(t, location, "scope=snsapi_login")
  84. }
  85. func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
  86. originalAccessTokenURL := wechatOAuthAccessTokenURL
  87. originalUserInfoURL := wechatOAuthUserInfoURL
  88. t.Cleanup(func() {
  89. wechatOAuthAccessTokenURL = originalAccessTokenURL
  90. wechatOAuthUserInfoURL = originalUserInfoURL
  91. })
  92. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  93. switch {
  94. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  95. w.Header().Set("Content-Type", "application/json")
  96. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  97. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  98. w.Header().Set("Content-Type", "application/json")
  99. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"WeChat Nick","headimgurl":"https://cdn.example/avatar.png"}`))
  100. default:
  101. http.NotFound(w, r)
  102. }
  103. }))
  104. defer upstream.Close()
  105. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  106. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  107. handler, client := newWeChatOAuthTestHandler(t, false)
  108. defer client.Close()
  109. recorder := httptest.NewRecorder()
  110. c, _ := gin.CreateTestContext(recorder)
  111. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  112. req.Host = "api.example.com"
  113. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  114. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  115. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  116. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  117. c.Request = req
  118. handler.WeChatOAuthCallback(c)
  119. require.Equal(t, http.StatusFound, recorder.Code)
  120. require.Equal(t, "/auth/wechat/callback", recorder.Header().Get("Location"))
  121. sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
  122. require.NotNil(t, sessionCookie)
  123. ctx := context.Background()
  124. session, err := client.PendingAuthSession.Query().
  125. Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
  126. Only(ctx)
  127. require.NoError(t, err)
  128. require.Equal(t, "wechat", session.ProviderType)
  129. require.Equal(t, "wechat-main", session.ProviderKey)
  130. require.Equal(t, "union-456", session.ProviderSubject)
  131. require.Equal(t, "wechat-union-456@wechat-connect.invalid", session.ResolvedEmail)
  132. require.Equal(t, "WeChat Nick", session.UpstreamIdentityClaims["suggested_display_name"])
  133. require.Equal(t, "https://cdn.example/avatar.png", session.UpstreamIdentityClaims["suggested_avatar_url"])
  134. require.Equal(t, "union-456", session.UpstreamIdentityClaims["unionid"])
  135. require.Equal(t, "openid-123", session.UpstreamIdentityClaims["openid"])
  136. }
  137. func TestWeChatOAuthCallbackFallsBackToOpenIDWhenUnionIDMissingInSingleChannelMode(t *testing.T) {
  138. originalAccessTokenURL := wechatOAuthAccessTokenURL
  139. originalUserInfoURL := wechatOAuthUserInfoURL
  140. t.Cleanup(func() {
  141. wechatOAuthAccessTokenURL = originalAccessTokenURL
  142. wechatOAuthUserInfoURL = originalUserInfoURL
  143. })
  144. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  145. switch {
  146. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  147. w.Header().Set("Content-Type", "application/json")
  148. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","scope":"snsapi_login"}`))
  149. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  150. w.Header().Set("Content-Type", "application/json")
  151. _, _ = w.Write([]byte(`{"openid":"openid-123","nickname":"WeChat Nick","headimgurl":"https://cdn.example/avatar.png"}`))
  152. default:
  153. http.NotFound(w, r)
  154. }
  155. }))
  156. defer upstream.Close()
  157. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  158. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  159. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "https://app.example.com/auth/wechat/callback"))
  160. defer client.Close()
  161. recorder := httptest.NewRecorder()
  162. c, _ := gin.CreateTestContext(recorder)
  163. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  164. req.Host = "api.example.com"
  165. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  166. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  167. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  168. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  169. c.Request = req
  170. handler.WeChatOAuthCallback(c)
  171. require.Equal(t, http.StatusFound, recorder.Code)
  172. require.Equal(t, "https://app.example.com/auth/wechat/callback", recorder.Header().Get("Location"))
  173. sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
  174. require.NotNil(t, sessionCookie)
  175. session, err := client.PendingAuthSession.Query().
  176. Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
  177. Only(context.Background())
  178. require.NoError(t, err)
  179. require.Equal(t, oauthIntentLogin, session.Intent)
  180. require.Equal(t, "openid-123", session.ProviderSubject)
  181. require.Equal(t, wechatSyntheticEmail("openid-123"), session.ResolvedEmail)
  182. completion := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
  183. require.Equal(t, oauthPendingChoiceStep, completion["step"])
  184. require.Equal(t, "third_party_signup", completion["choice_reason"])
  185. }
  186. func TestWeChatOAuthCallbackCreatesLoginPendingSessionForExistingIdentityUserWithoutStoredTokens(t *testing.T) {
  187. originalAccessTokenURL := wechatOAuthAccessTokenURL
  188. originalUserInfoURL := wechatOAuthUserInfoURL
  189. t.Cleanup(func() {
  190. wechatOAuthAccessTokenURL = originalAccessTokenURL
  191. wechatOAuthUserInfoURL = originalUserInfoURL
  192. })
  193. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  194. switch {
  195. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  196. w.Header().Set("Content-Type", "application/json")
  197. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  198. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  199. w.Header().Set("Content-Type", "application/json")
  200. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"WeChat Display","headimgurl":"https://cdn.example/wechat-login.png"}`))
  201. default:
  202. http.NotFound(w, r)
  203. }
  204. }))
  205. defer upstream.Close()
  206. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  207. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  208. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "https://app.example.com/auth/wechat/callback"))
  209. defer client.Close()
  210. ctx := context.Background()
  211. existingUser, err := client.User.Create().
  212. SetEmail(wechatSyntheticEmail("union-456")).
  213. SetUsername("wechat-existing-user").
  214. SetPasswordHash("hash").
  215. SetRole(service.RoleUser).
  216. SetStatus(service.StatusActive).
  217. Save(ctx)
  218. require.NoError(t, err)
  219. _, err = client.AuthIdentity.Create().
  220. SetUserID(existingUser.ID).
  221. SetProviderType("wechat").
  222. SetProviderKey(wechatOAuthProviderKey).
  223. SetProviderSubject("union-456").
  224. SetMetadata(map[string]any{"username": "wechat-existing-user"}).
  225. Save(ctx)
  226. require.NoError(t, err)
  227. recorder := httptest.NewRecorder()
  228. c, _ := gin.CreateTestContext(recorder)
  229. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  230. req.Host = "api.example.com"
  231. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  232. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  233. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  234. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  235. c.Request = req
  236. handler.WeChatOAuthCallback(c)
  237. require.Equal(t, http.StatusFound, recorder.Code)
  238. require.Equal(t, "https://app.example.com/auth/wechat/callback", recorder.Header().Get("Location"))
  239. sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
  240. require.NotNil(t, sessionCookie)
  241. session, err := client.PendingAuthSession.Query().
  242. Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
  243. Only(ctx)
  244. require.NoError(t, err)
  245. require.Equal(t, oauthIntentLogin, session.Intent)
  246. require.NotNil(t, session.TargetUserID)
  247. require.Equal(t, existingUser.ID, *session.TargetUserID)
  248. require.Equal(t, existingUser.Email, session.ResolvedEmail)
  249. completion := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
  250. require.Equal(t, "/dashboard", completion["redirect"])
  251. _, hasAccessToken := completion["access_token"]
  252. require.False(t, hasAccessToken)
  253. _, hasRefreshToken := completion["refresh_token"]
  254. require.False(t, hasRefreshToken)
  255. }
  256. func TestWeChatOAuthCallbackRejectsDisabledExistingIdentityUser(t *testing.T) {
  257. originalAccessTokenURL := wechatOAuthAccessTokenURL
  258. originalUserInfoURL := wechatOAuthUserInfoURL
  259. t.Cleanup(func() {
  260. wechatOAuthAccessTokenURL = originalAccessTokenURL
  261. wechatOAuthUserInfoURL = originalUserInfoURL
  262. })
  263. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  264. switch {
  265. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  266. w.Header().Set("Content-Type", "application/json")
  267. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-disabled","unionid":"union-disabled","scope":"snsapi_login"}`))
  268. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  269. w.Header().Set("Content-Type", "application/json")
  270. _, _ = w.Write([]byte(`{"openid":"openid-disabled","unionid":"union-disabled","nickname":"Disabled WeChat","headimgurl":"https://cdn.example/disabled.png"}`))
  271. default:
  272. http.NotFound(w, r)
  273. }
  274. }))
  275. defer upstream.Close()
  276. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  277. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  278. handler, client := newWeChatOAuthTestHandler(t, false)
  279. defer client.Close()
  280. ctx := context.Background()
  281. existingUser, err := client.User.Create().
  282. SetEmail(wechatSyntheticEmail("union-disabled")).
  283. SetUsername("disabled-user").
  284. SetPasswordHash("hash").
  285. SetRole(service.RoleUser).
  286. SetStatus(service.StatusDisabled).
  287. Save(ctx)
  288. require.NoError(t, err)
  289. _, err = client.AuthIdentity.Create().
  290. SetUserID(existingUser.ID).
  291. SetProviderType("wechat").
  292. SetProviderKey(wechatOAuthProviderKey).
  293. SetProviderSubject("union-disabled").
  294. Save(ctx)
  295. require.NoError(t, err)
  296. recorder := httptest.NewRecorder()
  297. c, _ := gin.CreateTestContext(recorder)
  298. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-disabled", nil)
  299. req.Host = "api.example.com"
  300. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-disabled"))
  301. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  302. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  303. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-disabled"))
  304. c.Request = req
  305. handler.WeChatOAuthCallback(c)
  306. require.Equal(t, http.StatusFound, recorder.Code)
  307. require.Nil(t, findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName))
  308. assertOAuthRedirectError(t, recorder.Header().Get("Location"), "session_error", "USER_NOT_ACTIVE")
  309. count, err := client.PendingAuthSession.Query().Count(ctx)
  310. require.NoError(t, err)
  311. require.Zero(t, count)
  312. }
  313. func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T) {
  314. originalAccessTokenURL := wechatOAuthAccessTokenURL
  315. t.Cleanup(func() {
  316. wechatOAuthAccessTokenURL = originalAccessTokenURL
  317. })
  318. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  319. if strings.Contains(r.URL.Path, "/sns/oauth2/access_token") {
  320. w.Header().Set("Content-Type", "application/json")
  321. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","scope":"snsapi_base"}`))
  322. return
  323. }
  324. http.NotFound(w, r)
  325. }))
  326. defer upstream.Close()
  327. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  328. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("mp", "wx-mp-app", "wx-mp-secret", "/auth/wechat/callback"))
  329. defer client.Close()
  330. handler.cfg.Totp.EncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  331. handler.cfg.Totp.EncryptionKeyConfigured = true
  332. recorder := httptest.NewRecorder()
  333. c, _ := gin.CreateTestContext(recorder)
  334. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/payment/callback?code=wechat-code&state=state-123", nil)
  335. req.Host = "api.example.com"
  336. req.AddCookie(encodedCookie(wechatPaymentOAuthStateName, "state-123"))
  337. req.AddCookie(encodedCookie(wechatPaymentOAuthRedirect, "/purchase?from=wechat"))
  338. req.AddCookie(encodedCookie(wechatPaymentOAuthContextName, `{"payment_type":"wxpay","amount":"12.5","order_type":"subscription","plan_id":7}`))
  339. req.AddCookie(encodedCookie(wechatPaymentOAuthScope, "snsapi_base"))
  340. c.Request = req
  341. handler.WeChatPaymentOAuthCallback(c)
  342. require.Equal(t, http.StatusFound, recorder.Code)
  343. location := recorder.Header().Get("Location")
  344. parsed, err := url.Parse(location)
  345. require.NoError(t, err)
  346. fragment, err := url.ParseQuery(parsed.Fragment)
  347. require.NoError(t, err)
  348. require.Equal(t, "/purchase?from=wechat", fragment.Get("redirect"))
  349. require.NotEmpty(t, fragment.Get("wechat_resume_token"))
  350. require.Empty(t, fragment.Get("openid"))
  351. require.Empty(t, fragment.Get("payment_type"))
  352. require.Empty(t, fragment.Get("amount"))
  353. require.Empty(t, fragment.Get("order_type"))
  354. require.Empty(t, fragment.Get("plan_id"))
  355. claims, err := handler.wechatPaymentResumeService().ParseWeChatPaymentResumeToken(fragment.Get("wechat_resume_token"))
  356. require.NoError(t, err)
  357. require.Equal(t, "openid-123", claims.OpenID)
  358. require.Equal(t, payment.TypeWxpay, claims.PaymentType)
  359. require.Equal(t, "12.5", claims.Amount)
  360. require.Equal(t, payment.OrderTypeSubscription, claims.OrderType)
  361. require.EqualValues(t, 7, claims.PlanID)
  362. require.Equal(t, "/purchase?from=wechat", claims.RedirectTo)
  363. }
  364. func TestWeChatPaymentOAuthCallbackUsesExplicitPaymentResumeSigningKeyWhenMixedKeysConfigured(t *testing.T) {
  365. originalAccessTokenURL := wechatOAuthAccessTokenURL
  366. t.Cleanup(func() {
  367. wechatOAuthAccessTokenURL = originalAccessTokenURL
  368. })
  369. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  370. if strings.Contains(r.URL.Path, "/sns/oauth2/access_token") {
  371. w.Header().Set("Content-Type", "application/json")
  372. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-mixed-key","scope":"snsapi_base"}`))
  373. return
  374. }
  375. http.NotFound(w, r)
  376. }))
  377. defer upstream.Close()
  378. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  379. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("mp", "wx-mp-app", "wx-mp-secret", "/auth/wechat/callback"))
  380. defer client.Close()
  381. legacyKeyHex := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  382. explicitSigningKey := "explicit-payment-resume-signing-key"
  383. t.Setenv("PAYMENT_RESUME_SIGNING_KEY", explicitSigningKey)
  384. handler.cfg.Totp.EncryptionKey = legacyKeyHex
  385. handler.cfg.Totp.EncryptionKeyConfigured = true
  386. recorder := httptest.NewRecorder()
  387. c, _ := gin.CreateTestContext(recorder)
  388. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/payment/callback?code=wechat-code&state=state-mixed", nil)
  389. req.Host = "api.example.com"
  390. req.AddCookie(encodedCookie(wechatPaymentOAuthStateName, "state-mixed"))
  391. req.AddCookie(encodedCookie(wechatPaymentOAuthRedirect, "/purchase?from=wechat"))
  392. req.AddCookie(encodedCookie(wechatPaymentOAuthContextName, `{"payment_type":"wxpay","amount":"18.8","order_type":"subscription","plan_id":9}`))
  393. req.AddCookie(encodedCookie(wechatPaymentOAuthScope, "snsapi_base"))
  394. c.Request = req
  395. handler.WeChatPaymentOAuthCallback(c)
  396. require.Equal(t, http.StatusFound, recorder.Code)
  397. location := recorder.Header().Get("Location")
  398. parsed, err := url.Parse(location)
  399. require.NoError(t, err)
  400. fragment, err := url.ParseQuery(parsed.Fragment)
  401. require.NoError(t, err)
  402. token := fragment.Get("wechat_resume_token")
  403. require.NotEmpty(t, token)
  404. claims, err := service.NewPaymentResumeService([]byte(explicitSigningKey)).ParseWeChatPaymentResumeToken(token)
  405. require.NoError(t, err)
  406. require.Equal(t, "openid-mixed-key", claims.OpenID)
  407. require.Equal(t, payment.TypeWxpay, claims.PaymentType)
  408. require.Equal(t, "18.8", claims.Amount)
  409. require.Equal(t, payment.OrderTypeSubscription, claims.OrderType)
  410. require.EqualValues(t, 9, claims.PlanID)
  411. require.Equal(t, "/purchase?from=wechat", claims.RedirectTo)
  412. _, err = service.NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef")).ParseWeChatPaymentResumeToken(token)
  413. require.Error(t, err)
  414. }
  415. func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *testing.T) {
  416. testCases := []struct {
  417. name string
  418. mode string
  419. appID string
  420. appSecret string
  421. openID string
  422. }{
  423. {
  424. name: "open",
  425. mode: "open",
  426. appID: "wx-open-app",
  427. appSecret: "wx-open-secret",
  428. openID: "openid-open-123",
  429. },
  430. {
  431. name: "mp",
  432. mode: "mp",
  433. appID: "wx-mp-app",
  434. appSecret: "wx-mp-secret",
  435. openID: "openid-mp-123",
  436. },
  437. }
  438. for _, tc := range testCases {
  439. t.Run(tc.name, func(t *testing.T) {
  440. originalAccessTokenURL := wechatOAuthAccessTokenURL
  441. originalUserInfoURL := wechatOAuthUserInfoURL
  442. t.Cleanup(func() {
  443. wechatOAuthAccessTokenURL = originalAccessTokenURL
  444. wechatOAuthUserInfoURL = originalUserInfoURL
  445. })
  446. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  447. switch {
  448. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  449. w.Header().Set("Content-Type", "application/json")
  450. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"` + tc.openID + `","unionid":"union-456","scope":"snsapi_login"}`))
  451. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  452. w.Header().Set("Content-Type", "application/json")
  453. _, _ = w.Write([]byte(`{"openid":"` + tc.openID + `","unionid":"union-456","nickname":"Bind Nick","headimgurl":"https://cdn.example/bind.png"}`))
  454. default:
  455. http.NotFound(w, r)
  456. }
  457. }))
  458. defer upstream.Close()
  459. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  460. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  461. handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings(tc.mode, tc.appID, tc.appSecret, "/auth/wechat/callback"))
  462. defer client.Close()
  463. currentUser, err := client.User.Create().
  464. SetEmail("current@example.com").
  465. SetUsername("current-user").
  466. SetPasswordHash("hash").
  467. SetRole(service.RoleUser).
  468. SetStatus(service.StatusActive).
  469. Save(context.Background())
  470. require.NoError(t, err)
  471. recorder := httptest.NewRecorder()
  472. c, _ := gin.CreateTestContext(recorder)
  473. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  474. req.Host = "api.example.com"
  475. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  476. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  477. req.AddCookie(encodedCookie(wechatOAuthIntentCookieName, wechatOAuthIntentBind))
  478. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, tc.mode))
  479. req.AddCookie(encodedCookie(wechatOAuthBindUserCookieName, buildEncodedOAuthBindUserCookie(t, currentUser.ID, "test-secret")))
  480. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  481. c.Request = req
  482. handler.WeChatOAuthCallback(c)
  483. require.Equal(t, http.StatusFound, recorder.Code)
  484. require.Equal(t, "/auth/wechat/callback", recorder.Header().Get("Location"))
  485. sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
  486. require.NotNil(t, sessionCookie)
  487. session, err := client.PendingAuthSession.Query().
  488. Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
  489. Only(context.Background())
  490. require.NoError(t, err)
  491. require.Equal(t, wechatOAuthIntentBind, session.Intent)
  492. require.NotNil(t, session.TargetUserID)
  493. require.Equal(t, currentUser.ID, *session.TargetUserID)
  494. require.Equal(t, currentUser.Email, session.ResolvedEmail)
  495. require.Equal(t, "union-456", session.ProviderSubject)
  496. require.Equal(t, "union-456", session.UpstreamIdentityClaims["subject"])
  497. require.Equal(t, "union-456", session.UpstreamIdentityClaims["unionid"])
  498. require.Equal(t, tc.openID, session.UpstreamIdentityClaims["openid"])
  499. require.Equal(t, tc.mode, session.UpstreamIdentityClaims["channel"])
  500. require.Equal(t, tc.appID, session.UpstreamIdentityClaims["channel_app_id"])
  501. require.Equal(t, tc.openID, session.UpstreamIdentityClaims["channel_subject"])
  502. completionResponse := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
  503. require.Equal(t, "/dashboard", completionResponse["redirect"])
  504. _, hasAccessToken := completionResponse["access_token"]
  505. require.False(t, hasAccessToken)
  506. })
  507. }
  508. }
  509. func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T) {
  510. originalAccessTokenURL := wechatOAuthAccessTokenURL
  511. originalUserInfoURL := wechatOAuthUserInfoURL
  512. t.Cleanup(func() {
  513. wechatOAuthAccessTokenURL = originalAccessTokenURL
  514. wechatOAuthUserInfoURL = originalUserInfoURL
  515. })
  516. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  517. switch {
  518. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  519. w.Header().Set("Content-Type", "application/json")
  520. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  521. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  522. w.Header().Set("Content-Type", "application/json")
  523. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"Conflict Nick","headimgurl":"https://cdn.example/conflict.png"}`))
  524. default:
  525. http.NotFound(w, r)
  526. }
  527. }))
  528. defer upstream.Close()
  529. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  530. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  531. handler, client := newWeChatOAuthTestHandler(t, false)
  532. defer client.Close()
  533. ctx := context.Background()
  534. owner, err := client.User.Create().
  535. SetEmail("owner@example.com").
  536. SetUsername("owner").
  537. SetPasswordHash("hash").
  538. SetRole(service.RoleUser).
  539. SetStatus(service.StatusActive).
  540. Save(ctx)
  541. require.NoError(t, err)
  542. currentUser, err := client.User.Create().
  543. SetEmail("current@example.com").
  544. SetUsername("current").
  545. SetPasswordHash("hash").
  546. SetRole(service.RoleUser).
  547. SetStatus(service.StatusActive).
  548. Save(ctx)
  549. require.NoError(t, err)
  550. _, err = client.AuthIdentity.Create().
  551. SetUserID(owner.ID).
  552. SetProviderType("wechat").
  553. SetProviderKey(wechatOAuthProviderKey).
  554. SetProviderSubject("union-456").
  555. SetMetadata(map[string]any{"unionid": "union-456"}).
  556. Save(ctx)
  557. require.NoError(t, err)
  558. recorder := httptest.NewRecorder()
  559. c, _ := gin.CreateTestContext(recorder)
  560. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  561. req.Host = "api.example.com"
  562. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  563. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  564. req.AddCookie(encodedCookie(wechatOAuthIntentCookieName, wechatOAuthIntentBind))
  565. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  566. req.AddCookie(encodedCookie(wechatOAuthBindUserCookieName, buildEncodedOAuthBindUserCookie(t, currentUser.ID, "test-secret")))
  567. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  568. c.Request = req
  569. handler.WeChatOAuthCallback(c)
  570. require.Equal(t, http.StatusFound, recorder.Code)
  571. require.Nil(t, findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName))
  572. assertOAuthRedirectError(t, recorder.Header().Get("Location"), "ownership_conflict", "AUTH_IDENTITY_OWNERSHIP_CONFLICT")
  573. count, err := client.PendingAuthSession.Query().Count(ctx)
  574. require.NoError(t, err)
  575. require.Zero(t, count)
  576. }
  577. func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
  578. originalAccessTokenURL := wechatOAuthAccessTokenURL
  579. originalUserInfoURL := wechatOAuthUserInfoURL
  580. t.Cleanup(func() {
  581. wechatOAuthAccessTokenURL = originalAccessTokenURL
  582. wechatOAuthUserInfoURL = originalUserInfoURL
  583. })
  584. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  585. switch {
  586. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  587. w.Header().Set("Content-Type", "application/json")
  588. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  589. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  590. w.Header().Set("Content-Type", "application/json")
  591. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"Conflict Nick","headimgurl":"https://cdn.example/conflict.png"}`))
  592. default:
  593. http.NotFound(w, r)
  594. }
  595. }))
  596. defer upstream.Close()
  597. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  598. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  599. handler, client := newWeChatOAuthTestHandler(t, false)
  600. defer client.Close()
  601. ctx := context.Background()
  602. owner, err := client.User.Create().
  603. SetEmail("owner@example.com").
  604. SetUsername("owner").
  605. SetPasswordHash("hash").
  606. SetRole(service.RoleUser).
  607. SetStatus(service.StatusActive).
  608. Save(ctx)
  609. require.NoError(t, err)
  610. currentUser, err := client.User.Create().
  611. SetEmail("current@example.com").
  612. SetUsername("current").
  613. SetPasswordHash("hash").
  614. SetRole(service.RoleUser).
  615. SetStatus(service.StatusActive).
  616. Save(ctx)
  617. require.NoError(t, err)
  618. ownerIdentity, err := client.AuthIdentity.Create().
  619. SetUserID(owner.ID).
  620. SetProviderType("wechat").
  621. SetProviderKey(wechatOAuthProviderKey).
  622. SetProviderSubject("union-owner").
  623. SetMetadata(map[string]any{"unionid": "union-owner"}).
  624. Save(ctx)
  625. require.NoError(t, err)
  626. _, err = client.AuthIdentityChannel.Create().
  627. SetIdentityID(ownerIdentity.ID).
  628. SetProviderType("wechat").
  629. SetProviderKey(wechatOAuthProviderKey).
  630. SetChannel("open").
  631. SetChannelAppID("wx-open-app").
  632. SetChannelSubject("openid-123").
  633. SetMetadata(map[string]any{"openid": "openid-123"}).
  634. Save(ctx)
  635. require.NoError(t, err)
  636. recorder := httptest.NewRecorder()
  637. c, _ := gin.CreateTestContext(recorder)
  638. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  639. req.Host = "api.example.com"
  640. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  641. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  642. req.AddCookie(encodedCookie(wechatOAuthIntentCookieName, wechatOAuthIntentBind))
  643. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  644. req.AddCookie(encodedCookie(wechatOAuthBindUserCookieName, buildEncodedOAuthBindUserCookie(t, currentUser.ID, "test-secret")))
  645. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  646. c.Request = req
  647. handler.WeChatOAuthCallback(c)
  648. require.Equal(t, http.StatusFound, recorder.Code)
  649. require.Nil(t, findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName))
  650. assertOAuthRedirectError(t, recorder.Header().Get("Location"), "ownership_conflict", "AUTH_IDENTITY_CHANNEL_OWNERSHIP_CONFLICT")
  651. count, err := client.PendingAuthSession.Query().Count(ctx)
  652. require.NoError(t, err)
  653. require.Zero(t, count)
  654. }
  655. func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *testing.T) {
  656. originalAccessTokenURL := wechatOAuthAccessTokenURL
  657. originalUserInfoURL := wechatOAuthUserInfoURL
  658. t.Cleanup(func() {
  659. wechatOAuthAccessTokenURL = originalAccessTokenURL
  660. wechatOAuthUserInfoURL = originalUserInfoURL
  661. })
  662. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  663. switch {
  664. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  665. w.Header().Set("Content-Type", "application/json")
  666. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  667. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  668. w.Header().Set("Content-Type", "application/json")
  669. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"Conflict Nick","headimgurl":"https://cdn.example/conflict.png"}`))
  670. default:
  671. http.NotFound(w, r)
  672. }
  673. }))
  674. defer upstream.Close()
  675. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  676. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  677. handler, client := newWeChatOAuthTestHandler(t, false)
  678. defer client.Close()
  679. ctx := context.Background()
  680. owner, err := client.User.Create().
  681. SetEmail("owner@example.com").
  682. SetUsername("owner").
  683. SetPasswordHash("hash").
  684. SetRole(service.RoleUser).
  685. SetStatus(service.StatusActive).
  686. Save(ctx)
  687. require.NoError(t, err)
  688. currentUser, err := client.User.Create().
  689. SetEmail("current@example.com").
  690. SetUsername("current").
  691. SetPasswordHash("hash").
  692. SetRole(service.RoleUser).
  693. SetStatus(service.StatusActive).
  694. Save(ctx)
  695. require.NoError(t, err)
  696. _, err = client.AuthIdentity.Create().
  697. SetUserID(owner.ID).
  698. SetProviderType("wechat").
  699. SetProviderKey(wechatOAuthLegacyProviderKey).
  700. SetProviderSubject("union-456").
  701. SetMetadata(map[string]any{"unionid": "union-456"}).
  702. Save(ctx)
  703. require.NoError(t, err)
  704. recorder := httptest.NewRecorder()
  705. c, _ := gin.CreateTestContext(recorder)
  706. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  707. req.Host = "api.example.com"
  708. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  709. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  710. req.AddCookie(encodedCookie(wechatOAuthIntentCookieName, wechatOAuthIntentBind))
  711. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  712. req.AddCookie(encodedCookie(wechatOAuthBindUserCookieName, buildEncodedOAuthBindUserCookie(t, currentUser.ID, "test-secret")))
  713. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  714. c.Request = req
  715. handler.WeChatOAuthCallback(c)
  716. require.Equal(t, http.StatusFound, recorder.Code)
  717. require.Nil(t, findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName))
  718. assertOAuthRedirectError(t, recorder.Header().Get("Location"), "ownership_conflict", "AUTH_IDENTITY_OWNERSHIP_CONFLICT")
  719. count, err := client.PendingAuthSession.Query().Count(ctx)
  720. require.NoError(t, err)
  721. require.Zero(t, count)
  722. }
  723. func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSessionReturnsPendingSession(t *testing.T) {
  724. originalAccessTokenURL := wechatOAuthAccessTokenURL
  725. originalUserInfoURL := wechatOAuthUserInfoURL
  726. t.Cleanup(func() {
  727. wechatOAuthAccessTokenURL = originalAccessTokenURL
  728. wechatOAuthUserInfoURL = originalUserInfoURL
  729. })
  730. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  731. switch {
  732. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  733. w.Header().Set("Content-Type", "application/json")
  734. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  735. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  736. w.Header().Set("Content-Type", "application/json")
  737. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"WeChat Display","headimgurl":"https://cdn.example/wechat.png"}`))
  738. default:
  739. http.NotFound(w, r)
  740. }
  741. }))
  742. defer upstream.Close()
  743. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  744. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  745. handler, client := newWeChatOAuthTestHandler(t, true)
  746. defer client.Close()
  747. ctx := context.Background()
  748. redeemRepo := repository.NewRedeemCodeRepository(client)
  749. require.NoError(t, redeemRepo.Create(ctx, &service.RedeemCode{
  750. Code: "invite-1",
  751. Type: service.RedeemTypeInvitation,
  752. Status: service.StatusUnused,
  753. }))
  754. callbackRecorder := httptest.NewRecorder()
  755. callbackCtx, _ := gin.CreateTestContext(callbackRecorder)
  756. callbackReq := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  757. callbackReq.Host = "api.example.com"
  758. callbackReq.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  759. callbackReq.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  760. callbackReq.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  761. callbackReq.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  762. callbackCtx.Request = callbackReq
  763. handler.WeChatOAuthCallback(callbackCtx)
  764. require.Equal(t, http.StatusFound, callbackRecorder.Code)
  765. require.Equal(t, "/auth/wechat/callback", callbackRecorder.Header().Get("Location"))
  766. sessionCookie := findCookie(callbackRecorder.Result().Cookies(), oauthPendingSessionCookieName)
  767. require.NotNil(t, sessionCookie)
  768. sessionToken := decodeCookieValueForTest(t, sessionCookie.Value)
  769. pendingSession, err := client.PendingAuthSession.Query().
  770. Where(pendingauthsession.SessionTokenEQ(sessionToken)).
  771. Only(ctx)
  772. require.NoError(t, err)
  773. require.Equal(t, oauthPendingChoiceStep, pendingSession.LocalFlowState[oauthCompletionResponseKey].(map[string]any)["step"])
  774. body := bytes.NewBufferString(`{"invitation_code":"invite-1","adopt_display_name":true,"adopt_avatar":true}`)
  775. completeRecorder := httptest.NewRecorder()
  776. completeCtx, _ := gin.CreateTestContext(completeRecorder)
  777. completeReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/wechat/complete-registration", body)
  778. completeReq.Header.Set("Content-Type", "application/json")
  779. completeReq.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(sessionToken)})
  780. completeReq.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("browser-123")})
  781. completeCtx.Request = completeReq
  782. handler.CompleteWeChatOAuthRegistration(completeCtx)
  783. require.Equal(t, http.StatusOK, completeRecorder.Code)
  784. responseData := decodeJSONBody(t, completeRecorder)
  785. require.Equal(t, "pending_session", responseData["auth_result"])
  786. require.Equal(t, oauthPendingChoiceStep, responseData["step"])
  787. require.Equal(t, true, responseData["adoption_required"])
  788. require.Empty(t, responseData["access_token"])
  789. consumed, err := client.PendingAuthSession.Query().
  790. Where(pendingauthsession.IDEQ(pendingSession.ID)).
  791. Only(ctx)
  792. require.NoError(t, err)
  793. require.Nil(t, consumed.ConsumedAt)
  794. userCount, err := client.User.Query().Count(ctx)
  795. require.NoError(t, err)
  796. require.Zero(t, userCount)
  797. identityCount, err := client.AuthIdentity.Query().
  798. Where(
  799. authidentity.ProviderTypeEQ("wechat"),
  800. authidentity.ProviderKeyEQ("wechat-main"),
  801. authidentity.ProviderSubjectEQ("union-456"),
  802. ).
  803. Count(ctx)
  804. require.NoError(t, err)
  805. require.Zero(t, identityCount)
  806. channelCount, err := client.AuthIdentityChannel.Query().
  807. Where(
  808. authidentitychannel.ProviderTypeEQ("wechat"),
  809. authidentitychannel.ProviderKeyEQ("wechat-main"),
  810. authidentitychannel.ChannelEQ("open"),
  811. authidentitychannel.ChannelAppIDEQ("wx-open-app"),
  812. authidentitychannel.ChannelSubjectEQ("openid-123"),
  813. ).
  814. Count(ctx)
  815. require.NoError(t, err)
  816. require.Zero(t, channelCount)
  817. decisionCount, err := client.IdentityAdoptionDecision.Query().
  818. Where(identityadoptiondecision.PendingAuthSessionIDEQ(pendingSession.ID)).
  819. Count(ctx)
  820. require.NoError(t, err)
  821. require.Zero(t, decisionCount)
  822. }
  823. func TestCompleteWeChatOAuthRegistrationBindsIdentityWithoutAdoptionFlags(t *testing.T) {
  824. handler, client := newOAuthPendingFlowTestHandler(t, false)
  825. ctx := context.Background()
  826. session, err := client.PendingAuthSession.Create().
  827. SetSessionToken("wechat-complete-no-adoption-session").
  828. SetIntent("login").
  829. SetProviderType("wechat").
  830. SetProviderKey(wechatOAuthProviderKey).
  831. SetProviderSubject("wechat-subject-no-adoption").
  832. SetResolvedEmail("wechat-subject-no-adoption@wechat-connect.invalid").
  833. SetBrowserSessionKey("wechat-browser-no-adoption").
  834. SetUpstreamIdentityClaims(map[string]any{
  835. "username": "wechat_user",
  836. "suggested_display_name": "WeChat Legacy",
  837. "suggested_avatar_url": "https://cdn.example/wechat-legacy.png",
  838. "mode": "open",
  839. "channel": "open",
  840. "channel_app_id": "wx-open-app",
  841. "channel_subject": "openid-legacy",
  842. }).
  843. SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
  844. Save(ctx)
  845. require.NoError(t, err)
  846. body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
  847. recorder := httptest.NewRecorder()
  848. completeCtx, _ := gin.CreateTestContext(recorder)
  849. completeReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/wechat/complete-registration", body)
  850. completeReq.Header.Set("Content-Type", "application/json")
  851. completeReq.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
  852. completeReq.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("wechat-browser-no-adoption")})
  853. completeCtx.Request = completeReq
  854. handler.CompleteWeChatOAuthRegistration(completeCtx)
  855. require.Equal(t, http.StatusOK, recorder.Code)
  856. responseData := decodeJSONBody(t, recorder)
  857. require.NotEmpty(t, responseData["access_token"])
  858. require.NotEmpty(t, responseData["refresh_token"])
  859. userEntity, err := client.User.Query().
  860. Where(dbuser.EmailEQ(session.ResolvedEmail)).
  861. Only(ctx)
  862. require.NoError(t, err)
  863. require.Equal(t, "wechat_user", userEntity.Username)
  864. identity, err := client.AuthIdentity.Query().
  865. Where(
  866. authidentity.ProviderTypeEQ("wechat"),
  867. authidentity.ProviderKeyEQ(wechatOAuthProviderKey),
  868. authidentity.ProviderSubjectEQ("wechat-subject-no-adoption"),
  869. ).
  870. Only(ctx)
  871. require.NoError(t, err)
  872. require.Equal(t, userEntity.ID, identity.UserID)
  873. decision, err := client.IdentityAdoptionDecision.Query().
  874. Where(identityadoptiondecision.PendingAuthSessionIDEQ(session.ID)).
  875. Only(ctx)
  876. require.NoError(t, err)
  877. require.NotNil(t, decision.IdentityID)
  878. require.Equal(t, identity.ID, *decision.IdentityID)
  879. require.False(t, decision.AdoptDisplayName)
  880. require.False(t, decision.AdoptAvatar)
  881. }
  882. func TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity(t *testing.T) {
  883. originalAccessTokenURL := wechatOAuthAccessTokenURL
  884. originalUserInfoURL := wechatOAuthUserInfoURL
  885. t.Cleanup(func() {
  886. wechatOAuthAccessTokenURL = originalAccessTokenURL
  887. wechatOAuthUserInfoURL = originalUserInfoURL
  888. })
  889. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  890. switch {
  891. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  892. w.Header().Set("Content-Type", "application/json")
  893. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  894. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  895. w.Header().Set("Content-Type", "application/json")
  896. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"Legacy WeChat","headimgurl":"https://cdn.example/legacy.png"}`))
  897. default:
  898. http.NotFound(w, r)
  899. }
  900. }))
  901. defer upstream.Close()
  902. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  903. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  904. handler, client := newWeChatOAuthTestHandler(t, false)
  905. defer client.Close()
  906. ctx := context.Background()
  907. legacyUser, err := client.User.Create().
  908. SetEmail("legacy@example.com").
  909. SetUsername("legacy-user").
  910. SetPasswordHash("hash").
  911. SetRole(service.RoleUser).
  912. SetStatus(service.StatusActive).
  913. Save(ctx)
  914. require.NoError(t, err)
  915. legacyIdentity, err := client.AuthIdentity.Create().
  916. SetUserID(legacyUser.ID).
  917. SetProviderType("wechat").
  918. SetProviderKey(wechatOAuthProviderKey).
  919. SetProviderSubject("openid-123").
  920. SetMetadata(map[string]any{"openid": "openid-123"}).
  921. Save(ctx)
  922. require.NoError(t, err)
  923. recorder := httptest.NewRecorder()
  924. c, _ := gin.CreateTestContext(recorder)
  925. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  926. req.Host = "api.example.com"
  927. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  928. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  929. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  930. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  931. c.Request = req
  932. handler.WeChatOAuthCallback(c)
  933. require.Equal(t, http.StatusFound, recorder.Code)
  934. require.Equal(t, "/auth/wechat/callback", recorder.Header().Get("Location"))
  935. sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
  936. require.NotNil(t, sessionCookie)
  937. session, err := client.PendingAuthSession.Query().
  938. Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
  939. Only(ctx)
  940. require.NoError(t, err)
  941. require.NotNil(t, session.TargetUserID)
  942. require.Equal(t, legacyUser.ID, *session.TargetUserID)
  943. require.Equal(t, legacyUser.Email, session.ResolvedEmail)
  944. repairedIdentity, err := client.AuthIdentity.Query().
  945. Where(
  946. authidentity.ProviderTypeEQ("wechat"),
  947. authidentity.ProviderKeyEQ(wechatOAuthProviderKey),
  948. authidentity.ProviderSubjectEQ("union-456"),
  949. ).
  950. Only(ctx)
  951. require.NoError(t, err)
  952. require.Equal(t, legacyIdentity.ID, repairedIdentity.ID)
  953. require.Equal(t, legacyUser.ID, repairedIdentity.UserID)
  954. openIDIdentityCount, err := client.AuthIdentity.Query().
  955. Where(
  956. authidentity.ProviderTypeEQ("wechat"),
  957. authidentity.ProviderKeyEQ(wechatOAuthProviderKey),
  958. authidentity.ProviderSubjectEQ("openid-123"),
  959. ).
  960. Count(ctx)
  961. require.NoError(t, err)
  962. require.Zero(t, openIDIdentityCount)
  963. channel, err := client.AuthIdentityChannel.Query().
  964. Where(
  965. authidentitychannel.ProviderTypeEQ("wechat"),
  966. authidentitychannel.ProviderKeyEQ(wechatOAuthProviderKey),
  967. authidentitychannel.ChannelEQ("open"),
  968. authidentitychannel.ChannelAppIDEQ("wx-open-app"),
  969. authidentitychannel.ChannelSubjectEQ("openid-123"),
  970. ).
  971. Only(ctx)
  972. require.NoError(t, err)
  973. require.Equal(t, repairedIdentity.ID, channel.IdentityID)
  974. }
  975. func TestCompleteWeChatOAuthRegistrationRejectsAdoptExistingUserSession(t *testing.T) {
  976. handler, client := newWeChatOAuthTestHandler(t, false)
  977. defer client.Close()
  978. ctx := context.Background()
  979. existingUser, err := client.User.Create().
  980. SetEmail("owner@example.com").
  981. SetUsername("owner-user").
  982. SetPasswordHash("hash").
  983. SetRole(service.RoleUser).
  984. SetStatus(service.StatusActive).
  985. Save(ctx)
  986. require.NoError(t, err)
  987. session, err := client.PendingAuthSession.Create().
  988. SetSessionToken("wechat-complete-invalid-session").
  989. SetIntent("adopt_existing_user_by_email").
  990. SetProviderType("wechat").
  991. SetProviderKey("wechat-main").
  992. SetProviderSubject("union-invalid-1").
  993. SetTargetUserID(existingUser.ID).
  994. SetResolvedEmail(existingUser.Email).
  995. SetBrowserSessionKey("wechat-invalid-browser").
  996. SetUpstreamIdentityClaims(map[string]any{
  997. "username": "wechat_user",
  998. }).
  999. SetLocalFlowState(map[string]any{
  1000. oauthCompletionResponseKey: map[string]any{
  1001. "step": "bind_login_required",
  1002. },
  1003. }).
  1004. SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
  1005. Save(ctx)
  1006. require.NoError(t, err)
  1007. body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
  1008. recorder := httptest.NewRecorder()
  1009. completeCtx, _ := gin.CreateTestContext(recorder)
  1010. req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/wechat/complete-registration", body)
  1011. req.Header.Set("Content-Type", "application/json")
  1012. req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
  1013. req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("wechat-invalid-browser")})
  1014. completeCtx.Request = req
  1015. handler.CompleteWeChatOAuthRegistration(completeCtx)
  1016. require.Equal(t, http.StatusBadRequest, recorder.Code)
  1017. storedSession, err := client.PendingAuthSession.Get(ctx, session.ID)
  1018. require.NoError(t, err)
  1019. require.Nil(t, storedSession.ConsumedAt)
  1020. }
  1021. func TestCompleteWeChatOAuthRegistrationReturnsPendingSessionWhenChoiceStillRequired(t *testing.T) {
  1022. handler, client := newWeChatOAuthTestHandler(t, false)
  1023. defer client.Close()
  1024. ctx := context.Background()
  1025. session, err := client.PendingAuthSession.Create().
  1026. SetSessionToken("wechat-complete-choice-session").
  1027. SetIntent("login").
  1028. SetProviderType("wechat").
  1029. SetProviderKey("wechat-main").
  1030. SetProviderSubject("wechat-choice-subject-1").
  1031. SetResolvedEmail("wechat-choice-subject-1@wechat-connect.invalid").
  1032. SetBrowserSessionKey("wechat-choice-browser").
  1033. SetUpstreamIdentityClaims(map[string]any{
  1034. "username": "wechat_user",
  1035. }).
  1036. SetLocalFlowState(map[string]any{
  1037. oauthCompletionResponseKey: map[string]any{
  1038. "step": oauthPendingChoiceStep,
  1039. "redirect": "/dashboard",
  1040. "email": "fresh@example.com",
  1041. "resolved_email": "fresh@example.com",
  1042. "force_email_on_signup": true,
  1043. },
  1044. }).
  1045. SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
  1046. Save(ctx)
  1047. require.NoError(t, err)
  1048. body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
  1049. recorder := httptest.NewRecorder()
  1050. completeCtx, _ := gin.CreateTestContext(recorder)
  1051. req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/wechat/complete-registration", body)
  1052. req.Header.Set("Content-Type", "application/json")
  1053. req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
  1054. req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("wechat-choice-browser")})
  1055. completeCtx.Request = req
  1056. handler.CompleteWeChatOAuthRegistration(completeCtx)
  1057. require.Equal(t, http.StatusOK, recorder.Code)
  1058. responseData := decodeJSONBody(t, recorder)
  1059. require.Equal(t, "pending_session", responseData["auth_result"])
  1060. require.Equal(t, oauthPendingChoiceStep, responseData["step"])
  1061. require.Equal(t, true, responseData["force_email_on_signup"])
  1062. require.Empty(t, responseData["access_token"])
  1063. userCount, err := client.User.Query().Count(ctx)
  1064. require.NoError(t, err)
  1065. require.Zero(t, userCount)
  1066. storedSession, err := client.PendingAuthSession.Get(ctx, session.ID)
  1067. require.NoError(t, err)
  1068. require.Nil(t, storedSession.ConsumedAt)
  1069. }
  1070. func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing.T) {
  1071. originalAccessTokenURL := wechatOAuthAccessTokenURL
  1072. originalUserInfoURL := wechatOAuthUserInfoURL
  1073. t.Cleanup(func() {
  1074. wechatOAuthAccessTokenURL = originalAccessTokenURL
  1075. wechatOAuthUserInfoURL = originalUserInfoURL
  1076. })
  1077. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1078. switch {
  1079. case strings.Contains(r.URL.Path, "/sns/oauth2/access_token"):
  1080. w.Header().Set("Content-Type", "application/json")
  1081. _, _ = w.Write([]byte(`{"access_token":"wechat-access","openid":"openid-123","unionid":"union-456","scope":"snsapi_login"}`))
  1082. case strings.Contains(r.URL.Path, "/sns/userinfo"):
  1083. w.Header().Set("Content-Type", "application/json")
  1084. _, _ = w.Write([]byte(`{"openid":"openid-123","unionid":"union-456","nickname":"Legacy Canonical","headimgurl":"https://cdn.example/legacy-canonical.png"}`))
  1085. default:
  1086. http.NotFound(w, r)
  1087. }
  1088. }))
  1089. defer upstream.Close()
  1090. wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
  1091. wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
  1092. handler, client := newWeChatOAuthTestHandler(t, false)
  1093. defer client.Close()
  1094. ctx := context.Background()
  1095. legacyUser, err := client.User.Create().
  1096. SetEmail("legacy@example.com").
  1097. SetUsername("legacy-user").
  1098. SetPasswordHash("hash").
  1099. SetRole(service.RoleUser).
  1100. SetStatus(service.StatusActive).
  1101. Save(ctx)
  1102. require.NoError(t, err)
  1103. legacyIdentity, err := client.AuthIdentity.Create().
  1104. SetUserID(legacyUser.ID).
  1105. SetProviderType("wechat").
  1106. SetProviderKey(wechatOAuthLegacyProviderKey).
  1107. SetProviderSubject("union-456").
  1108. SetMetadata(map[string]any{"unionid": "union-456"}).
  1109. Save(ctx)
  1110. require.NoError(t, err)
  1111. recorder := httptest.NewRecorder()
  1112. c, _ := gin.CreateTestContext(recorder)
  1113. req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/callback?code=wechat-code&state=state-123", nil)
  1114. req.Host = "api.example.com"
  1115. req.AddCookie(encodedCookie(wechatOAuthStateCookieName, "state-123"))
  1116. req.AddCookie(encodedCookie(wechatOAuthRedirectCookieName, "/dashboard"))
  1117. req.AddCookie(encodedCookie(wechatOAuthModeCookieName, "open"))
  1118. req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
  1119. c.Request = req
  1120. handler.WeChatOAuthCallback(c)
  1121. require.Equal(t, http.StatusFound, recorder.Code)
  1122. require.Equal(t, "/auth/wechat/callback", recorder.Header().Get("Location"))
  1123. sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
  1124. require.NotNil(t, sessionCookie)
  1125. session, err := client.PendingAuthSession.Query().
  1126. Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
  1127. Only(ctx)
  1128. require.NoError(t, err)
  1129. require.NotNil(t, session.TargetUserID)
  1130. require.Equal(t, legacyUser.ID, *session.TargetUserID)
  1131. require.Equal(t, legacyUser.Email, session.ResolvedEmail)
  1132. repairedIdentity, err := client.AuthIdentity.Query().
  1133. Where(
  1134. authidentity.ProviderTypeEQ("wechat"),
  1135. authidentity.ProviderKeyEQ(wechatOAuthProviderKey),
  1136. authidentity.ProviderSubjectEQ("union-456"),
  1137. ).
  1138. Only(ctx)
  1139. require.NoError(t, err)
  1140. require.Equal(t, legacyIdentity.ID, repairedIdentity.ID)
  1141. require.Equal(t, legacyUser.ID, repairedIdentity.UserID)
  1142. legacyIdentityCount, err := client.AuthIdentity.Query().
  1143. Where(
  1144. authidentity.ProviderTypeEQ("wechat"),
  1145. authidentity.ProviderKeyEQ(wechatOAuthLegacyProviderKey),
  1146. authidentity.ProviderSubjectEQ("union-456"),
  1147. ).
  1148. Count(ctx)
  1149. require.NoError(t, err)
  1150. require.Zero(t, legacyIdentityCount)
  1151. channel, err := client.AuthIdentityChannel.Query().
  1152. Where(
  1153. authidentitychannel.ProviderTypeEQ("wechat"),
  1154. authidentitychannel.ProviderKeyEQ(wechatOAuthProviderKey),
  1155. authidentitychannel.ChannelEQ("open"),
  1156. authidentitychannel.ChannelAppIDEQ("wx-open-app"),
  1157. authidentitychannel.ChannelSubjectEQ("openid-123"),
  1158. ).
  1159. Only(ctx)
  1160. require.NoError(t, err)
  1161. require.Equal(t, repairedIdentity.ID, channel.IdentityID)
  1162. }
  1163. func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandler, *dbent.Client) {
  1164. return newWeChatOAuthTestHandlerWithSettings(t, invitationEnabled, nil)
  1165. }
  1166. func wechatOAuthTestSettings(mode, appID, secret, frontendRedirect string) map[string]string {
  1167. return map[string]string{
  1168. service.SettingKeyWeChatConnectEnabled: "true",
  1169. service.SettingKeyWeChatConnectAppID: appID,
  1170. service.SettingKeyWeChatConnectAppSecret: secret,
  1171. service.SettingKeyWeChatConnectMode: mode,
  1172. service.SettingKeyWeChatConnectScopes: service.DefaultWeChatConnectScopesForMode(mode),
  1173. service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  1174. service.SettingKeyWeChatConnectFrontendRedirectURL: frontendRedirect,
  1175. }
  1176. }
  1177. func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool, extraSettings map[string]string) (*AuthHandler, *dbent.Client) {
  1178. t.Helper()
  1179. db, err := sql.Open("sqlite", "file:auth_wechat_oauth?mode=memory&cache=shared")
  1180. require.NoError(t, err)
  1181. t.Cleanup(func() { _ = db.Close() })
  1182. _, err = db.Exec("PRAGMA foreign_keys = ON")
  1183. require.NoError(t, err)
  1184. drv := entsql.OpenDB(dialect.SQLite, db)
  1185. client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
  1186. userRepo := &oauthPendingFlowUserRepo{client: client}
  1187. redeemRepo := repository.NewRedeemCodeRepository(client)
  1188. cfg := &config.Config{
  1189. JWT: config.JWTConfig{
  1190. Secret: "test-secret",
  1191. ExpireHour: 1,
  1192. AccessTokenExpireMinutes: 60,
  1193. RefreshTokenExpireDays: 7,
  1194. },
  1195. Default: config.DefaultConfig{
  1196. UserBalance: 0,
  1197. UserConcurrency: 1,
  1198. },
  1199. }
  1200. values := map[string]string{
  1201. service.SettingKeyRegistrationEnabled: "true",
  1202. service.SettingKeyInvitationCodeEnabled: boolSettingValue(invitationEnabled),
  1203. }
  1204. for key, value := range wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "/auth/wechat/callback") {
  1205. values[key] = value
  1206. }
  1207. for key, value := range extraSettings {
  1208. values[key] = value
  1209. }
  1210. settingSvc := service.NewSettingService(&wechatOAuthSettingRepoStub{values: values}, cfg)
  1211. authSvc := service.NewAuthService(
  1212. client,
  1213. userRepo,
  1214. redeemRepo,
  1215. &wechatOAuthRefreshTokenCacheStub{},
  1216. cfg,
  1217. settingSvc,
  1218. nil,
  1219. nil,
  1220. nil,
  1221. nil,
  1222. nil,
  1223. nil,
  1224. nil,
  1225. )
  1226. return &AuthHandler{
  1227. authService: authSvc,
  1228. settingSvc: settingSvc,
  1229. cfg: cfg,
  1230. }, client
  1231. }
  1232. type wechatOAuthSettingRepoStub struct {
  1233. values map[string]string
  1234. }
  1235. func (s *wechatOAuthSettingRepoStub) Get(context.Context, string) (*service.Setting, error) {
  1236. return nil, service.ErrSettingNotFound
  1237. }
  1238. func (s *wechatOAuthSettingRepoStub) GetValue(_ context.Context, key string) (string, error) {
  1239. value, ok := s.values[key]
  1240. if !ok {
  1241. return "", service.ErrSettingNotFound
  1242. }
  1243. return value, nil
  1244. }
  1245. func (s *wechatOAuthSettingRepoStub) Set(context.Context, string, string) error {
  1246. return nil
  1247. }
  1248. func (s *wechatOAuthSettingRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
  1249. result := make(map[string]string, len(keys))
  1250. for _, key := range keys {
  1251. if value, ok := s.values[key]; ok {
  1252. result[key] = value
  1253. }
  1254. }
  1255. return result, nil
  1256. }
  1257. func (s *wechatOAuthSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
  1258. return nil
  1259. }
  1260. func (s *wechatOAuthSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
  1261. result := make(map[string]string, len(s.values))
  1262. for key, value := range s.values {
  1263. result[key] = value
  1264. }
  1265. return result, nil
  1266. }
  1267. func (s *wechatOAuthSettingRepoStub) Delete(context.Context, string) error {
  1268. return nil
  1269. }
  1270. type wechatOAuthRefreshTokenCacheStub struct{}
  1271. func (s *wechatOAuthRefreshTokenCacheStub) StoreRefreshToken(context.Context, string, *service.RefreshTokenData, time.Duration) error {
  1272. return nil
  1273. }
  1274. func (s *wechatOAuthRefreshTokenCacheStub) GetRefreshToken(context.Context, string) (*service.RefreshTokenData, error) {
  1275. return nil, service.ErrRefreshTokenNotFound
  1276. }
  1277. func (s *wechatOAuthRefreshTokenCacheStub) DeleteRefreshToken(context.Context, string) error {
  1278. return nil
  1279. }
  1280. func (s *wechatOAuthRefreshTokenCacheStub) DeleteUserRefreshTokens(context.Context, int64) error {
  1281. return nil
  1282. }
  1283. func (s *wechatOAuthRefreshTokenCacheStub) DeleteTokenFamily(context.Context, string) error {
  1284. return nil
  1285. }
  1286. func (s *wechatOAuthRefreshTokenCacheStub) AddToUserTokenSet(context.Context, int64, string, time.Duration) error {
  1287. return nil
  1288. }
  1289. func (s *wechatOAuthRefreshTokenCacheStub) AddToFamilyTokenSet(context.Context, string, string, time.Duration) error {
  1290. return nil
  1291. }
  1292. func (s *wechatOAuthRefreshTokenCacheStub) GetUserTokenHashes(context.Context, int64) ([]string, error) {
  1293. return nil, nil
  1294. }
  1295. func (s *wechatOAuthRefreshTokenCacheStub) GetFamilyTokenHashes(context.Context, string) ([]string, error) {
  1296. return nil, nil
  1297. }
  1298. func (s *wechatOAuthRefreshTokenCacheStub) IsTokenInFamily(context.Context, string, string) (bool, error) {
  1299. return false, nil
  1300. }