diff --git a/monkit.lock b/monkit.lock index e3f46a08f..6dfcee397 100644 --- a/monkit.lock +++ b/monkit.lock @@ -62,6 +62,8 @@ storj.io/storj/satellite/console."login_mfa_passcode_success" Counter storj.io/storj/satellite/console."login_mfa_recovery_failure" Counter storj.io/storj/satellite/console."login_mfa_recovery_success" Counter storj.io/storj/satellite/console."login_success" Counter +storj.io/storj/satellite/console."login_user_captcha_error" Counter +storj.io/storj/satellite/console."login_user_captcha_unsuccessful" Counter storj.io/storj/satellite/console."login_user_failed_count" IntVal storj.io/storj/satellite/contact."failed_dial" Event storj.io/storj/satellite/contact."failed_ping_node" Event diff --git a/satellite/console/captcha_test.go b/satellite/console/captcha_test.go index 5523b79c1..91153d696 100644 --- a/satellite/console/captcha_test.go +++ b/satellite/console/captcha_test.go @@ -24,15 +24,15 @@ func (r mockRecaptcha) Verify(ctx context.Context, responseToken string, userIP return responseToken == validResponseToken, nil } -// TestRecaptcha ensures the reCAPTCHA service is working properly. -func TestRecaptcha(t *testing.T) { +// TestRegistrationRecaptcha ensures that registration reCAPTCHA service is working properly. +func TestRegistrationRecaptcha(t *testing.T) { testplanet.Run(t, testplanet.Config{ SatelliteCount: 1, Reconfigure: testplanet.Reconfigure{ Satellite: func(log *zap.Logger, index int, config *satellite.Config) { - config.Console.Recaptcha.Enabled = true - config.Console.Recaptcha.SecretKey = "mySecretKey" - config.Console.Recaptcha.SiteKey = "mySiteKey" + config.Console.Captcha.Registration.Recaptcha.Enabled = true + config.Console.Captcha.Registration.Recaptcha.SecretKey = "mySecretKey" + config.Console.Captcha.Registration.Recaptcha.SiteKey = "mySiteKey" }, }, }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { @@ -67,3 +67,61 @@ func TestRecaptcha(t *testing.T) { require.True(t, console.ErrCaptcha.Has(err)) }) } + +// TestLoginRecaptcha ensures that login reCAPTCHA service is working properly. +func TestLoginRecaptcha(t *testing.T) { + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, + Reconfigure: testplanet.Reconfigure{ + Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + config.Console.Captcha.Login.Recaptcha.Enabled = true + config.Console.Captcha.Login.Recaptcha.SecretKey = "mySecretKey" + config.Console.Captcha.Login.Recaptcha.SiteKey = "mySiteKey" + }, + }, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + service := planet.Satellites[0].API.Console.Service + require.NotNil(t, service) + service.TestSwapCaptchaHandler(mockRecaptcha{}) + + regToken, err := service.CreateRegToken(ctx, 1) + require.NoError(t, err) + + email := "user@mail.test" + password := "password" + + user, err := service.CreateUser(ctx, console.CreateUser{ + FullName: "User", + Email: email, + Password: password, + }, regToken.Secret) + + require.NotNil(t, user) + require.NoError(t, err) + + activationToken, err := service.GenerateActivationToken(ctx, user.ID, user.Email) + require.NoError(t, err) + + user, err = service.ActivateAccount(ctx, activationToken) + require.NotNil(t, user) + require.NoError(t, err) + + token, err := service.Token(ctx, console.AuthUser{ + Email: email, + Password: password, + CaptchaResponse: validResponseToken, + }) + + require.NotEmpty(t, token) + require.NoError(t, err) + + token, err = service.Token(ctx, console.AuthUser{ + Email: email, + Password: password, + CaptchaResponse: "wrong", + }) + + require.Empty(t, token) + require.True(t, console.ErrCaptcha.Has(err)) + }) +} diff --git a/satellite/console/consoleweb/server.go b/satellite/console/consoleweb/server.go index 930244c3d..c9e464095 100644 --- a/satellite/console/consoleweb/server.go +++ b/satellite/console/consoleweb/server.go @@ -435,10 +435,14 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) { StorageTBPrice string EgressTBPrice string SegmentPrice string - RecaptchaEnabled bool - RecaptchaSiteKey string - HcaptchaEnabled bool - HcaptchaSiteKey string + RegistrationRecaptchaEnabled bool + RegistrationRecaptchaSiteKey string + RegistrationHcaptchaEnabled bool + RegistrationHcaptchaSiteKey string + LoginRecaptchaEnabled bool + LoginRecaptchaSiteKey string + LoginHcaptchaEnabled bool + LoginHcaptchaSiteKey string NewProjectDashboard bool DefaultPaidStorageLimit memory.Size DefaultPaidBandwidthLimit memory.Size @@ -474,10 +478,14 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) { data.StorageTBPrice = server.pricing.StorageTBPrice data.EgressTBPrice = server.pricing.EgressTBPrice data.SegmentPrice = server.pricing.SegmentPrice - data.RecaptchaEnabled = server.config.Recaptcha.Enabled - data.RecaptchaSiteKey = server.config.Recaptcha.SiteKey - data.HcaptchaEnabled = server.config.Hcaptcha.Enabled - data.HcaptchaSiteKey = server.config.Hcaptcha.SiteKey + data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled + data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey + data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled + data.RegistrationHcaptchaSiteKey = server.config.Captcha.Registration.Hcaptcha.SiteKey + data.LoginRecaptchaEnabled = server.config.Captcha.Login.Recaptcha.Enabled + data.LoginRecaptchaSiteKey = server.config.Captcha.Login.Recaptcha.SiteKey + data.LoginHcaptchaEnabled = server.config.Captcha.Login.Hcaptcha.Enabled + data.LoginHcaptchaSiteKey = server.config.Captcha.Login.Hcaptcha.SiteKey data.NewProjectDashboard = server.config.NewProjectDashboard data.NewObjectsFlow = server.config.NewObjectsFlow data.NewAccessGrantFlow = server.config.NewAccessGrantFlow diff --git a/satellite/console/service.go b/satellite/console/service.go index 245bf5ca9..af7698ddc 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -119,18 +119,19 @@ var ( // // architecture: Service type Service struct { - log, auditLogger *zap.Logger - store DB - restKeys RESTKeys - projectAccounting accounting.ProjectAccounting - projectUsage *accounting.Service - buckets Buckets - partners *rewards.PartnersService - accounts payments.Accounts - depositWallets payments.DepositWallets - captchaHandler CaptchaHandler - analytics *analytics.Service - tokens *consoleauth.Service + log, auditLogger *zap.Logger + store DB + restKeys RESTKeys + projectAccounting accounting.ProjectAccounting + projectUsage *accounting.Service + buckets Buckets + partners *rewards.PartnersService + accounts payments.Accounts + depositWallets payments.DepositWallets + registrationCaptchaHandler CaptchaHandler + loginCaptchaHandler CaptchaHandler + analytics *analytics.Service + tokens *consoleauth.Service config Config } @@ -157,22 +158,26 @@ type Config struct { FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"` SessionDuration time.Duration `help:"duration a session is valid for" default:"168h"` UsageLimits UsageLimitsConfig - Recaptcha RecaptchaConfig - Hcaptcha HcaptchaConfig + Captcha CaptchaConfig } -// RecaptchaConfig contains configurations for the reCAPTCHA system. -type RecaptchaConfig struct { - Enabled bool `help:"whether or not reCAPTCHA is enabled for user registration" default:"false"` - SiteKey string `help:"reCAPTCHA site key"` - SecretKey string `help:"reCAPTCHA secret key"` +// CaptchaConfig contains configurations for login/registration captcha system. +type CaptchaConfig struct { + Login MultiCaptchaConfig + Registration MultiCaptchaConfig } -// HcaptchaConfig contains configurations for the hCaptcha system. -type HcaptchaConfig struct { - Enabled bool `help:"whether or not hCaptcha is enabled for user registration" default:"false"` - SiteKey string `help:"hCaptcha site key" default:""` - SecretKey string `help:"hCaptcha secret key"` +// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems. +type MultiCaptchaConfig struct { + Recaptcha SingleCaptchaConfig + Hcaptcha SingleCaptchaConfig +} + +// SingleCaptchaConfig contains configurations abstract captcha system. +type SingleCaptchaConfig struct { + Enabled bool `help:"whether or not captcha is enabled" default:"false"` + SiteKey string `help:"captcha site key"` + SecretKey string `help:"captcha secret key"` } // Payments separates all payment related functionality. @@ -192,28 +197,39 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting config.PasswordCost = bcrypt.DefaultCost } - var captchaHandler CaptchaHandler - if config.Recaptcha.Enabled { - captchaHandler = NewDefaultCaptcha(Recaptcha, config.Recaptcha.SecretKey) - } else if config.Hcaptcha.Enabled { - captchaHandler = NewDefaultCaptcha(Hcaptcha, config.Hcaptcha.SecretKey) + // We have two separate captcha handlers for login and registration. + // We want to easily swap between captchas independently. + // For example, google recaptcha for login screen and hcaptcha for registration screen. + var registrationCaptchaHandler CaptchaHandler + if config.Captcha.Registration.Recaptcha.Enabled { + registrationCaptchaHandler = NewDefaultCaptcha(Recaptcha, config.Captcha.Registration.Recaptcha.SecretKey) + } else if config.Captcha.Registration.Hcaptcha.Enabled { + registrationCaptchaHandler = NewDefaultCaptcha(Hcaptcha, config.Captcha.Registration.Hcaptcha.SecretKey) + } + + var loginCaptchaHandler CaptchaHandler + if config.Captcha.Login.Recaptcha.Enabled { + loginCaptchaHandler = NewDefaultCaptcha(Recaptcha, config.Captcha.Login.Recaptcha.SecretKey) + } else if config.Captcha.Login.Hcaptcha.Enabled { + loginCaptchaHandler = NewDefaultCaptcha(Hcaptcha, config.Captcha.Login.Hcaptcha.SecretKey) } return &Service{ - log: log, - auditLogger: log.Named("auditlog"), - store: store, - restKeys: restKeys, - projectAccounting: projectAccounting, - projectUsage: projectUsage, - buckets: buckets, - partners: partners, - accounts: accounts, - depositWallets: depositWallets, - captchaHandler: captchaHandler, - analytics: analytics, - tokens: tokens, - config: config, + log: log, + auditLogger: log.Named("auditlog"), + store: store, + restKeys: restKeys, + projectAccounting: projectAccounting, + projectUsage: projectUsage, + buckets: buckets, + partners: partners, + accounts: accounts, + depositWallets: depositWallets, + registrationCaptchaHandler: registrationCaptchaHandler, + loginCaptchaHandler: loginCaptchaHandler, + analytics: analytics, + tokens: tokens, + config: config, }, nil } @@ -631,8 +647,8 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R mon.Counter("create_user_attempt").Inc(1) //mon:locked - if s.config.Recaptcha.Enabled || s.config.Hcaptcha.Enabled { - valid, err := s.captchaHandler.Verify(ctx, user.CaptchaResponse, user.IP) + if s.config.Captcha.Registration.Recaptcha.Enabled || s.config.Captcha.Registration.Hcaptcha.Enabled { + valid, 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)) @@ -738,7 +754,8 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R // TestSwapCaptchaHandler replaces the existing handler for captchas with // the one specified for use in testing. func (s *Service) TestSwapCaptchaHandler(h CaptchaHandler) { - s.captchaHandler = h + s.registrationCaptchaHandler = h + s.loginCaptchaHandler = h } // GenerateActivationToken - is a method for generating activation token. @@ -946,6 +963,19 @@ 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) + if err != nil { + mon.Counter("login_user_captcha_error").Inc(1) //mon:locked + s.log.Error("captcha authorization failed", zap.Error(err)) + return consoleauth.Token{}, ErrCaptcha.Wrap(err) + } + if !valid { + mon.Counter("login_user_captcha_unsuccessful").Inc(1) //mon:locked + return consoleauth.Token{}, ErrCaptcha.New("captcha validation unsuccessful") + } + } + user, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, request.Email) if user == nil { if len(unverified) > 0 { diff --git a/satellite/console/users.go b/satellite/console/users.go index 52c4e1dde..08209e5d3 100644 --- a/satellite/console/users.go +++ b/satellite/console/users.go @@ -117,6 +117,7 @@ type AuthUser struct { Password string `json:"password"` MFAPasscode string `json:"mfaPasscode"` MFARecoveryCode string `json:"mfaRecoveryCode"` + CaptchaResponse string `json:"captchaResponse"` IP string `json:"-"` UserAgent string `json:"-"` } diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index c40bd8cae..af032145b 100755 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -109,6 +109,42 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0 # url link for for beta satellite support # console.beta-satellite-support-url: "" +# whether or not captcha is enabled +# console.captcha.login.hcaptcha.enabled: false + +# captcha secret key +# console.captcha.login.hcaptcha.secret-key: "" + +# captcha site key +# console.captcha.login.hcaptcha.site-key: "" + +# whether or not captcha is enabled +# console.captcha.login.recaptcha.enabled: false + +# captcha secret key +# console.captcha.login.recaptcha.secret-key: "" + +# captcha site key +# console.captcha.login.recaptcha.site-key: "" + +# whether or not captcha is enabled +# console.captcha.registration.hcaptcha.enabled: false + +# captcha secret key +# console.captcha.registration.hcaptcha.secret-key: "" + +# captcha site key +# console.captcha.registration.hcaptcha.site-key: "" + +# whether or not captcha is enabled +# console.captcha.registration.recaptcha.enabled: false + +# captcha secret key +# console.captcha.registration.recaptcha.secret-key: "" + +# captcha site key +# console.captcha.registration.recaptcha.site-key: "" + # url link to contacts page # console.contact-info-url: https://forum.storj.io @@ -148,15 +184,6 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0 # indicates if generated console api should be used # console.generated-api-enabled: false -# whether or not hCaptcha is enabled for user registration -# console.hcaptcha.enabled: false - -# hCaptcha secret key -# console.hcaptcha.secret-key: "" - -# hCaptcha site key -# console.hcaptcha.site-key: "" - # url link to storj.io homepage # console.homepage-url: https://www.storj.io @@ -226,15 +253,6 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0 # number of clients whose rate limits we store # console.rate-limit.num-limits: 1000 -# whether or not reCAPTCHA is enabled for user registration -# console.recaptcha.enabled: false - -# reCAPTCHA secret key -# console.recaptcha.secret-key: "" - -# reCAPTCHA site key -# console.recaptcha.site-key: "" - # used to display at web satellite console # console.satellite-name: Storj diff --git a/web/satellite/index.html b/web/satellite/index.html index 2cd9045e5..cc35cfc44 100644 --- a/web/satellite/index.html +++ b/web/satellite/index.html @@ -1,47 +1,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ .SatelliteName }} - - - - -
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ .SatelliteName }} + + + + +
+ diff --git a/web/satellite/src/api/auth.ts b/web/satellite/src/api/auth.ts index 83f00210f..fa986e9f4 100644 --- a/web/satellite/src/api/auth.ts +++ b/web/satellite/src/api/auth.ts @@ -42,15 +42,17 @@ export class AuthHttpApi implements UsersApi { * * @param email - email of the user * @param password - password of the user + * @param captchaResponse - captcha response token * @param mfaPasscode - MFA passcode * @param mfaRecoveryCode - MFA recovery code * @throws Error */ - public async token(email: string, password: string, mfaPasscode: string, mfaRecoveryCode: string): Promise { + public async token(email: string, password: string, captchaResponse: string, mfaPasscode: string, mfaRecoveryCode: string): Promise { const path = `${this.ROOT_PATH}/token`; const body = { email, password, + captchaResponse, mfaPasscode: mfaPasscode || null, mfaRecoveryCode: mfaRecoveryCode || null, }; diff --git a/web/satellite/src/views/LoginArea.vue b/web/satellite/src/views/LoginArea.vue index a50497772..44d8307ea 100644 --- a/web/satellite/src/views/LoginArea.vue +++ b/web/satellite/src/views/LoginArea.vue @@ -2,7 +2,7 @@ // See LICENSE for copying information. - + + + @@ -100,15 +140,19 @@