console/satellite: track signup captcha scores

This change tracks signup captcha scores in the signup_captcha column in the users table.
It slightly modifies the captcha verify method to return both the score and success.

see: https://github.com/storj/storj/issues/5067

Change-Id: I7b3993e44958cfcf179806c7df19d6887fe3eda9
This commit is contained in:
Wilfred Asomani 2022-08-17 10:30:07 +00:00
parent cd89e1e557
commit a4192acabb
5 changed files with 27 additions and 14 deletions

View File

@ -21,7 +21,7 @@ const hcaptchaAPIURL = "https://hcaptcha.com/siteverify"
// and returning whether the user response characterized by the given
// response token and IP is valid.
type CaptchaHandler interface {
Verify(ctx context.Context, responseToken string, userIP string) (bool, error)
Verify(ctx context.Context, responseToken string, userIP string) (bool, *float64, error)
}
// CaptchaType is a type of captcha.
@ -55,12 +55,12 @@ func NewDefaultCaptcha(kind CaptchaType, secretKey string) CaptchaHandler {
// Verify contacts the captcha API and returns whether the given response token is valid.
// The documentation can be found here for recaptcha: https://developers.google.com/recaptcha/docs/verify
// And here for hcaptcha: https://docs.hcaptcha.com/
func (r captchaHandler) Verify(ctx context.Context, responseToken string, userIP string) (valid bool, err error) {
func (r captchaHandler) Verify(ctx context.Context, responseToken string, userIP string) (valid bool, score *float64, err error) {
if responseToken == "" {
return false, errs.New("the response token is empty")
return false, nil, errs.New("the response token is empty")
}
if userIP == "" {
return false, errs.New("the user's IP address is empty")
return false, nil, errs.New("the user's IP address is empty")
}
reqBody := url.Values{
@ -71,13 +71,13 @@ func (r captchaHandler) Verify(ctx context.Context, responseToken string, userIP
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.Endpoint, strings.NewReader(reqBody))
if err != nil {
return false, err
return false, nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, err
return false, nil, err
}
defer func() {
@ -85,16 +85,17 @@ func (r captchaHandler) Verify(ctx context.Context, responseToken string, userIP
}()
if resp.StatusCode != http.StatusOK {
return false, errors.New(resp.Status)
return false, nil, errors.New(resp.Status)
}
var data struct {
Success bool `json:"success"`
Success bool `json:"success"`
Score float64 `json:"score"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return false, err
return false, nil, err
}
return data.Success, nil
return data.Success, &data.Score, nil
}

View File

@ -20,8 +20,9 @@ const validResponseToken = "myResponseToken"
type mockRecaptcha struct{}
func (r mockRecaptcha) Verify(ctx context.Context, responseToken string, userIP string) (bool, error) {
return responseToken == validResponseToken, nil
func (r mockRecaptcha) Verify(ctx context.Context, responseToken string, userIP string) (bool, *float64, error) {
score := 1.0
return responseToken == validResponseToken, &score, nil
}
// TestRegistrationRecaptcha ensures that registration reCAPTCHA service is working properly.
@ -52,6 +53,8 @@ func TestRegistrationRecaptcha(t *testing.T) {
require.NotNil(t, user)
require.NoError(t, err)
require.NotNil(t, user.SignupCaptcha)
require.Equal(t, 1.0, *user.SignupCaptcha)
regToken2, err := service.CreateRegToken(ctx, 1)
require.NoError(t, err)

View File

@ -653,10 +653,12 @@ func (s *Service) checkRegistrationSecret(ctx context.Context, tokenSecret Regis
func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret RegistrationSecret) (u *User, err error) {
defer mon.Task()(&ctx)(&err)
var captchaScore *float64
mon.Counter("create_user_attempt").Inc(1) //mon:locked
if s.config.Captcha.Registration.Recaptcha.Enabled || s.config.Captcha.Registration.Hcaptcha.Enabled {
valid, err := s.registrationCaptchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
valid, score, err := s.registrationCaptchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
if err != nil {
mon.Counter("create_user_captcha_error").Inc(1) //mon:locked
s.log.Error("captcha authorization failed", zap.Error(err))
@ -666,6 +668,7 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
mon.Counter("create_user_captcha_unsuccessful").Inc(1) //mon:locked
return nil, ErrCaptcha.New("captcha validation unsuccessful")
}
captchaScore = score
}
if err := user.IsValid(); err != nil {
@ -715,6 +718,7 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
EmployeeCount: user.EmployeeCount,
HaveSalesContact: user.HaveSalesContact,
SignupPromoCode: user.SignupPromoCode,
SignupCaptcha: captchaScore,
}
if user.UserAgent != nil {
@ -979,7 +983,7 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut
mon.Counter("login_attempt").Inc(1) //mon:locked
if s.config.Captcha.Login.Recaptcha.Enabled || s.config.Captcha.Login.Hcaptcha.Enabled {
valid, err := s.loginCaptchaHandler.Verify(ctx, request.CaptchaResponse, request.IP)
valid, _, err := s.loginCaptchaHandler.Verify(ctx, request.CaptchaResponse, request.IP)
if err != nil {
mon.Counter("login_user_captcha_error").Inc(1) //mon:locked
return consoleauth.Token{}, ErrCaptcha.Wrap(err)

View File

@ -176,6 +176,7 @@ type User struct {
FailedLoginCount int `json:"failedLoginCount"`
LoginLockoutExpiration time.Time `json:"loginLockoutExpiration"`
SignupCaptcha *float64 `json:"-"`
}
// ResponseUser is an entity which describes db User and can be sent in response.

View File

@ -155,6 +155,9 @@ func (users *users) Insert(ctx context.Context, user *console.User) (_ *console.
optional.EmployeeCount = dbx.User_EmployeeCount(user.EmployeeCount)
optional.HaveSalesContact = dbx.User_HaveSalesContact(user.HaveSalesContact)
}
if user.SignupCaptcha != nil {
optional.SignupCaptcha = dbx.User_SignupCaptcha(*user.SignupCaptcha)
}
createdUser, err := users.db.Create_User(ctx,
dbx.User_Id(user.ID[:]),
@ -368,6 +371,7 @@ func userFromDBX(ctx context.Context, user *dbx.User) (_ *console.User, err erro
HaveSalesContact: user.HaveSalesContact,
MFAEnabled: user.MfaEnabled,
VerificationReminders: user.VerificationReminders,
SignupCaptcha: user.SignupCaptcha,
}
if user.PartnerId != nil {