auth_oauth_captcha_start_test.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. //go:build unit
  2. package handler
  3. import (
  4. "context"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. )
  14. type oauthCaptchaSettingRepo struct {
  15. values map[string]string
  16. }
  17. func (r *oauthCaptchaSettingRepo) Get(context.Context, string) (*service.Setting, error) {
  18. return nil, service.ErrSettingNotFound
  19. }
  20. func (r *oauthCaptchaSettingRepo) GetValue(_ context.Context, key string) (string, error) {
  21. value, ok := r.values[key]
  22. if !ok {
  23. return "", service.ErrSettingNotFound
  24. }
  25. return value, nil
  26. }
  27. func (r *oauthCaptchaSettingRepo) Set(context.Context, string, string) error { return nil }
  28. func (r *oauthCaptchaSettingRepo) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
  29. values := make(map[string]string, len(keys))
  30. for _, key := range keys {
  31. if value, ok := r.values[key]; ok {
  32. values[key] = value
  33. }
  34. }
  35. return values, nil
  36. }
  37. func (r *oauthCaptchaSettingRepo) SetMultiple(context.Context, map[string]string) error {
  38. return nil
  39. }
  40. func (r *oauthCaptchaSettingRepo) GetAll(context.Context) (map[string]string, error) {
  41. return r.values, nil
  42. }
  43. func (r *oauthCaptchaSettingRepo) Delete(context.Context, string) error { return nil }
  44. type oauthCaptchaVerifier struct {
  45. calls int
  46. proof service.TencentCaptchaProof
  47. }
  48. func (v *oauthCaptchaVerifier) VerifyTicket(_ context.Context, _ service.TencentCaptchaCredentials, proof service.TencentCaptchaProof, _ string) (*service.TencentCaptchaVerifyResponse, error) {
  49. v.calls++
  50. v.proof = proof
  51. return &service.TencentCaptchaVerifyResponse{CaptchaCode: 1}, nil
  52. }
  53. func newOAuthCaptchaTestHandler(enabled bool) (*AuthHandler, *oauthCaptchaVerifier) {
  54. values := map[string]string{}
  55. if enabled {
  56. values = map[string]string{
  57. service.SettingKeyTencentCaptchaEnabled: "true",
  58. service.SettingKeyTencentCaptchaAppID: "123456789",
  59. service.SettingKeyTencentCaptchaAppSecretKey: "app-secret",
  60. service.SettingKeyTencentCaptchaCloudSecretID: "cloud-secret-id",
  61. service.SettingKeyTencentCaptchaCloudSecretKey: "cloud-secret-key",
  62. }
  63. }
  64. cfg := &config.Config{}
  65. settings := service.NewSettingService(&oauthCaptchaSettingRepo{values: values}, cfg)
  66. verifier := &oauthCaptchaVerifier{}
  67. authService := service.NewAuthService(nil, nil, nil, nil, cfg, settings, nil, nil, nil, nil, nil, nil, nil)
  68. authService.SetTencentCaptchaService(service.NewTencentCaptchaService(settings, verifier))
  69. return &AuthHandler{authService: authService, settingSvc: settings, cfg: cfg}, verifier
  70. }
  71. func oauthStartHandlers() map[string]func(*AuthHandler, *gin.Context) {
  72. return map[string]func(*AuthHandler, *gin.Context){
  73. "github": func(h *AuthHandler, c *gin.Context) { h.GitHubOAuthStart(c) },
  74. "google": func(h *AuthHandler, c *gin.Context) { h.GoogleOAuthStart(c) },
  75. "linuxdo": func(h *AuthHandler, c *gin.Context) { h.LinuxDoOAuthStart(c) },
  76. "dingtalk": func(h *AuthHandler, c *gin.Context) { h.DingTalkOAuthStart(c) },
  77. "wechat": func(h *AuthHandler, c *gin.Context) { h.WeChatOAuthStart(c) },
  78. "oidc": func(h *AuthHandler, c *gin.Context) { h.OIDCOAuthStart(c) },
  79. }
  80. }
  81. func TestOAuthStartGetRejectsAnonymousLoginWhenTencentEnabledWithoutSideEffects(t *testing.T) {
  82. gin.SetMode(gin.TestMode)
  83. for provider, start := range oauthStartHandlers() {
  84. t.Run(provider, func(t *testing.T) {
  85. handler, verifier := newOAuthCaptchaTestHandler(true)
  86. recorder := httptest.NewRecorder()
  87. c, _ := gin.CreateTestContext(recorder)
  88. c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/"+provider+"/start?intent=bind_current_user", nil)
  89. start(handler, c)
  90. require.Equal(t, http.StatusBadRequest, recorder.Code)
  91. require.Contains(t, recorder.Body.String(), "TENCENT_CAPTCHA_VERIFICATION_FAILED")
  92. require.Empty(t, recorder.Header().Get("Location"))
  93. require.Empty(t, recorder.Header().Values("Set-Cookie"))
  94. require.Zero(t, verifier.calls)
  95. })
  96. }
  97. }
  98. func TestOAuthStartPostReturnsAuthorizeURLAfterTencentVerification(t *testing.T) {
  99. gin.SetMode(gin.TestMode)
  100. for provider := range oauthStartHandlers() {
  101. t.Run(provider, func(t *testing.T) {
  102. handler, verifier := newOAuthCaptchaTestHandler(true)
  103. recorder := httptest.NewRecorder()
  104. c, _ := gin.CreateTestContext(recorder)
  105. c.Request = httptest.NewRequest(
  106. http.MethodPost,
  107. "/api/v1/auth/oauth/"+provider+"/start",
  108. strings.NewReader(`{"tencent_captcha_ticket":"ticket-value","tencent_captcha_randstr":"@rand-value"}`),
  109. )
  110. c.Request.Header.Set("Content-Type", "application/json")
  111. require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
  112. respondOAuthStart(c, "https://provider.example/authorize")
  113. require.Equal(t, http.StatusOK, recorder.Code)
  114. require.Contains(t, recorder.Body.String(), `"authorize_url":"https://provider.example/authorize"`)
  115. require.Equal(t, 1, verifier.calls)
  116. require.Equal(t, service.TencentCaptchaProof{Ticket: "ticket-value", Randstr: "@rand-value"}, verifier.proof)
  117. })
  118. }
  119. }
  120. func TestOAuthStartPostRequiresTencentProofWhenEnabled(t *testing.T) {
  121. gin.SetMode(gin.TestMode)
  122. for provider := range oauthStartHandlers() {
  123. t.Run(provider, func(t *testing.T) {
  124. handler, verifier := newOAuthCaptchaTestHandler(true)
  125. recorder := httptest.NewRecorder()
  126. c, _ := gin.CreateTestContext(recorder)
  127. c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/"+provider+"/start", strings.NewReader(`{}`))
  128. c.Request.Header.Set("Content-Type", "application/json")
  129. require.False(t, handler.requireActionCaptchaForOAuthLoginStart(c))
  130. require.Equal(t, http.StatusBadRequest, recorder.Code)
  131. require.Contains(t, recorder.Body.String(), "TENCENT_CAPTCHA_VERIFICATION_FAILED")
  132. require.Zero(t, verifier.calls)
  133. })
  134. }
  135. }
  136. func TestOAuthBindingPathRemainsOutsideTencentGate(t *testing.T) {
  137. gin.SetMode(gin.TestMode)
  138. handler := &AuthHandler{}
  139. recorder := httptest.NewRecorder()
  140. c, _ := gin.CreateTestContext(recorder)
  141. c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/oidc/bind/start", nil)
  142. require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
  143. require.Equal(t, http.StatusOK, recorder.Code)
  144. }
  145. func TestOAuthStartGetRemainsCompatibleWhenTencentDisabled(t *testing.T) {
  146. gin.SetMode(gin.TestMode)
  147. handler, verifier := newOAuthCaptchaTestHandler(false)
  148. recorder := httptest.NewRecorder()
  149. c, _ := gin.CreateTestContext(recorder)
  150. c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/github/start", nil)
  151. require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
  152. respondOAuthStart(c, "https://provider.example/authorize")
  153. require.Equal(t, http.StatusFound, recorder.Code)
  154. require.Equal(t, "https://provider.example/authorize", recorder.Header().Get("Location"))
  155. require.Zero(t, verifier.calls)
  156. }