Browse Source

enforce CSRF token usage by the same IP for which it was issued

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 3 years ago
parent
commit
aaf940edab
8 changed files with 367 additions and 175 deletions
  1. 17 11
      httpd/auth_utils.go
  2. 135 9
      httpd/httpd_test.go
  3. 39 15
      httpd/internal_test.go
  4. 8 2
      httpd/middleware.go
  5. 5 2
      httpd/oidc_test.go
  6. 86 77
      httpd/server.go
  7. 49 36
      httpd/webadmin.go
  8. 28 23
      httpd/webclient.go

+ 17 - 11
httpd/auth_utils.go

@@ -49,16 +49,19 @@ type jwtTokenClaims struct {
 	Username                   string
 	Permissions                []string
 	Signature                  string
-	Audience                   string
+	Audience                   []string
 	APIKeyID                   string
 	MustSetTwoFactorAuth       bool
 	RequiredTwoFactorProtocols []string
 }
 
 func (c *jwtTokenClaims) hasUserAudience() bool {
-	if c.Audience == tokenAudienceWebClient || c.Audience == tokenAudienceAPIUser {
-		return true
+	for _, audience := range c.Audience {
+		if audience == tokenAudienceWebClient || audience == tokenAudienceAPIUser {
+			return true
+		}
 	}
+
 	return false
 }
 
@@ -97,9 +100,7 @@ func (c *jwtTokenClaims) Decode(token map[string]interface{}) {
 
 	switch v := audience.(type) {
 	case []string:
-		if len(v) > 0 {
-			c.Audience = v[0]
-		}
+		c.Audience = v
 	}
 
 	if val, ok := token[claimAPIKey]; ok {
@@ -163,10 +164,10 @@ func (c *jwtTokenClaims) createToken(tokenAuth *jwtauth.JWTAuth, audience tokenA
 	claims := c.asMap()
 	now := time.Now().UTC()
 
-	claims[jwt.JwtIDKey] = fmt.Sprintf("%s%s", xid.New().String(), ip)
+	claims[jwt.JwtIDKey] = xid.New().String()
 	claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
 	claims[jwt.ExpirationKey] = now.Add(tokenDuration)
-	claims[jwt.AudienceKey] = audience
+	claims[jwt.AudienceKey] = []string{audience, ip}
 
 	return tokenAuth.Encode(claims)
 }
@@ -299,14 +300,14 @@ func getAdminFromToken(r *http.Request) *dataprovider.Admin {
 	return admin
 }
 
-func createCSRFToken() string {
+func createCSRFToken(ip string) string {
 	claims := make(map[string]interface{})
 	now := time.Now().UTC()
 
 	claims[jwt.JwtIDKey] = xid.New().String()
 	claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
 	claims[jwt.ExpirationKey] = now.Add(csrfTokenDuration)
-	claims[jwt.AudienceKey] = tokenAudienceCSRF
+	claims[jwt.AudienceKey] = []string{tokenAudienceCSRF, ip}
 
 	_, tokenString, err := csrfTokenAuth.Encode(claims)
 	if err != nil {
@@ -316,7 +317,7 @@ func createCSRFToken() string {
 	return tokenString
 }
 
-func verifyCSRFToken(tokenString string) error {
+func verifyCSRFToken(tokenString, ip string) error {
 	token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
 	if err != nil || token == nil {
 		logger.Debug(logSender, "", "error validating CSRF token %#v: %v", tokenString, err)
@@ -328,5 +329,10 @@ func verifyCSRFToken(tokenString string) error {
 		return errors.New("the form token is not valid")
 	}
 
+	if !util.IsStringInSlice(ip, token.Audience()) {
+		logger.Debug(logSender, "", "error validating CSRF token IP audience")
+		return errors.New("the form token is not valid")
+	}
+
 	return nil
 }

File diff suppressed because it is too large
+ 135 - 9
httpd/httpd_test.go


+ 39 - 15
httpd/internal_test.go

@@ -633,7 +633,7 @@ func TestUpdateWebAdminInvalidClaims(t *testing.T) {
 	assert.NoError(t, err)
 
 	form := make(url.Values)
-	form.Set(csrfFormToken, createCSRFToken())
+	form.Set(csrfFormToken, createCSRFToken(""))
 	form.Set("status", "1")
 	req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode())))
 	rctx := chi.NewRouteContext()
@@ -688,7 +688,7 @@ func TestRetentionInvalidTokenClaims(t *testing.T) {
 
 func TestCSRFToken(t *testing.T) {
 	// invalid token
-	err := verifyCSRFToken("token")
+	err := verifyCSRFToken("token", "")
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "unable to verify form token")
 	}
@@ -699,15 +699,29 @@ func TestCSRFToken(t *testing.T) {
 	claims[jwt.JwtIDKey] = xid.New().String()
 	claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
 	claims[jwt.ExpirationKey] = now.Add(tokenDuration)
-	claims[jwt.AudienceKey] = tokenAudienceAPI
+	claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
 
 	_, tokenString, err := csrfTokenAuth.Encode(claims)
 	assert.NoError(t, err)
-	err = verifyCSRFToken(tokenString)
+	err = verifyCSRFToken(tokenString, "")
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "form token is not valid")
 	}
 
+	// bad IP
+	tokenString = createCSRFToken("127.1.1.1")
+	err = verifyCSRFToken(tokenString, "127.1.1.2")
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "form token is not valid")
+	}
+
+	claims[jwt.JwtIDKey] = xid.New().String()
+	claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
+	claims[jwt.ExpirationKey] = now.Add(tokenDuration)
+	claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
+	_, tokenString, err = csrfTokenAuth.Encode(claims)
+	assert.NoError(t, err)
+
 	r := GetHTTPRouter(Binding{
 		Address:         "",
 		Port:            8080,
@@ -722,6 +736,15 @@ func TestCSRFToken(t *testing.T) {
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token")
 
+	// invalid audience
+	req.Header.Set(csrfHeaderToken, tokenString)
+	rr = httptest.NewRecorder()
+	fn.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusForbidden, rr.Code)
+	assert.Contains(t, rr.Body.String(), "the token is not valid")
+
+	// invalid IP
+	tokenString = createCSRFToken("172.16.1.2")
 	req.Header.Set(csrfHeaderToken, tokenString)
 	rr = httptest.NewRecorder()
 	fn.ServeHTTP(rr, req)
@@ -729,7 +752,7 @@ func TestCSRFToken(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), "the token is not valid")
 
 	csrfTokenAuth = jwtauth.New("PS256", util.GenerateRandomBytes(32), nil)
-	tokenString = createCSRFToken()
+	tokenString = createCSRFToken("")
 	assert.Empty(t, tokenString)
 
 	csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
@@ -765,7 +788,7 @@ func TestCreateTokenError(t *testing.T) {
 	form := make(url.Values)
 	form.Set("username", admin.Username)
 	form.Set("password", admin.Password)
-	form.Set(csrfFormToken, createCSRFToken())
+	form.Set(csrfFormToken, createCSRFToken("127.0.0.1"))
 	req, _ = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
 	req.RemoteAddr = "127.0.0.1:1234"
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -905,7 +928,7 @@ func TestCreateTokenError(t *testing.T) {
 	form = make(url.Values)
 	form.Set("username", user.Username)
 	form.Set("password", "clientpwd")
-	form.Set(csrfFormToken, createCSRFToken())
+	form.Set(csrfFormToken, createCSRFToken("127.0.0.1"))
 	req, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))
 	req.RemoteAddr = "127.0.0.1:4567"
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -1086,7 +1109,7 @@ func TestCookieExpiration(t *testing.T) {
 	claims[claimPermissionsKey] = admin.Permissions
 	claims[jwt.SubjectKey] = admin.GetSignature()
 	claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
-	claims[jwt.AudienceKey] = tokenAudienceAPI
+	claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
 	token, _, err = server.tokenAuth.Encode(claims)
 	assert.NoError(t, err)
 	req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
@@ -1121,7 +1144,7 @@ func TestCookieExpiration(t *testing.T) {
 	claims[claimPermissionsKey] = admin.Permissions
 	claims[jwt.SubjectKey] = admin.GetSignature()
 	claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
-	claims[jwt.AudienceKey] = tokenAudienceAPI
+	claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
 	token, _, err = server.tokenAuth.Encode(claims)
 	assert.NoError(t, err)
 	req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
@@ -1159,7 +1182,7 @@ func TestCookieExpiration(t *testing.T) {
 	claims[claimPermissionsKey] = user.Filters.WebClient
 	claims[jwt.SubjectKey] = user.GetSignature()
 	claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
-	claims[jwt.AudienceKey] = tokenAudienceWebClient
+	claims[jwt.AudienceKey] = []string{tokenAudienceWebClient}
 	token, _, err = server.tokenAuth.Encode(claims)
 	assert.NoError(t, err)
 
@@ -1191,7 +1214,7 @@ func TestCookieExpiration(t *testing.T) {
 	claims[claimPermissionsKey] = user.Filters.WebClient
 	claims[jwt.SubjectKey] = user.GetSignature()
 	claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
-	claims[jwt.AudienceKey] = tokenAudienceWebClient
+	claims[jwt.AudienceKey] = []string{tokenAudienceWebClient}
 	token, _, err = server.tokenAuth.Encode(claims)
 	assert.NoError(t, err)
 
@@ -1520,7 +1543,7 @@ func TestProxyHeaders(t *testing.T) {
 	form := make(url.Values)
 	form.Set("username", username)
 	form.Set("password", password)
-	form.Set(csrfFormToken, createCSRFToken())
+	form.Set(csrfFormToken, createCSRFToken(testIP))
 	req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
 	assert.NoError(t, err)
 	req.RemoteAddr = testIP
@@ -1530,6 +1553,7 @@ func TestProxyHeaders(t *testing.T) {
 	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 	assert.Contains(t, rr.Body.String(), "login from IP 10.29.1.9 not allowed")
 
+	form.Set(csrfFormToken, createCSRFToken(validForwardedFor))
 	req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
 	assert.NoError(t, err)
 	req.RemoteAddr = testIP
@@ -2069,7 +2093,7 @@ func TestInvalidClaims(t *testing.T) {
 	token, err := c.createTokenResponse(server.tokenAuth, tokenAudienceWebClient, "")
 	assert.NoError(t, err)
 	form := make(url.Values)
-	form.Set(csrfFormToken, createCSRFToken())
+	form.Set(csrfFormToken, createCSRFToken(""))
 	form.Set("public_keys", "")
 	req, _ := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -2089,7 +2113,7 @@ func TestInvalidClaims(t *testing.T) {
 	token, err = c.createTokenResponse(server.tokenAuth, tokenAudienceWebAdmin, "")
 	assert.NoError(t, err)
 	form = make(url.Values)
-	form.Set(csrfFormToken, createCSRFToken())
+	form.Set(csrfFormToken, createCSRFToken(""))
 	form.Set("allow_api_key_auth", "")
 	req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -2400,7 +2424,7 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
 	}
 
 	form := make(url.Values)
-	csrfToken := createCSRFToken()
+	csrfToken := createCSRFToken("")
 	form.Set("_form_token", csrfToken)
 	form.Set("install_code", "12345")
 	form.Set("username", defaultAdminUsername)

+ 8 - 2
httpd/middleware.go

@@ -77,7 +77,7 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
 		return errInvalidToken
 	}
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
-	if ipAddr != "" && !strings.Contains(token.JwtID(), ipAddr) {
+	if !util.IsStringInSlice(ipAddr, token.Audience()) {
 		logger.Debug(logSender, "", "the token with id %#v is not valid for the ip address %#v", token.JwtID(), ipAddr)
 		doRedirect("Your token is not valid", nil)
 		return errInvalidToken
@@ -278,7 +278,13 @@ func verifyCSRFHeader(next http.Handler) http.Handler {
 		}
 
 		if !util.IsStringInSlice(tokenAudienceCSRF, token.Audience()) {
-			logger.Debug(logSender, "", "error validating CSRF header audience")
+			logger.Debug(logSender, "", "error validating CSRF header token audience")
+			sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
+			return
+		}
+
+		if !util.IsStringInSlice(util.GetIPFromRemoteAddress(r.RemoteAddr), token.Audience()) {
+			logger.Debug(logSender, "", "error validating CSRF header IP audience")
 			sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
 			return
 		}

+ 5 - 2
httpd/oidc_test.go

@@ -81,7 +81,10 @@ func setIDTokenClaims(idToken *oidc.IDToken, claims []byte) {
 }
 
 func TestOIDCInitialization(t *testing.T) {
-	config := OIDC{
+	config := OIDC{}
+	err := config.initialize()
+	assert.NoError(t, err)
+	config = OIDC{
 		ClientID:        "sftpgo-client",
 		ClientSecret:    "jRsmE0SWnuZjP7djBqNq0mrf8QN77j2c",
 		ConfigURL:       fmt.Sprintf("http://%v/", oidcMockAddr),
@@ -89,7 +92,7 @@ func TestOIDCInitialization(t *testing.T) {
 		UsernameField:   "preferred_username",
 		RoleField:       "sftpgo_role",
 	}
-	err := config.initialize()
+	err = config.initialize()
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "oidc: unable to initialize provider")
 	}

+ 86 - 77
httpd/server.go

@@ -133,12 +133,12 @@ func (s *httpdServer) refreshCookie(next http.Handler) http.Handler {
 	})
 }
 
-func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error, ip string) {
 	data := loginPage{
 		CurrentURL: webClientLoginPath,
 		Version:    version.Get().Version,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		ExtraCSS:   s.binding.ExtraCSS,
 	}
@@ -170,7 +170,7 @@ func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *htt
 		s.renderClientChangePasswordPage(w, r, err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
 		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -189,48 +189,48 @@ func (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Reques
 		http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
 		return
 	}
-	s.renderClientLoginPage(w, getFlashMessage(w, r))
+	s.renderClientLoginPage(w, getFlashMessage(w, r), util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderClientLoginPage(w, err.Error())
+		s.renderClientLoginPage(w, err.Error(), ipAddr)
 		return
 	}
-	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	protocol := common.ProtocolHTTP
 	username := r.Form.Get("username")
 	password := r.Form.Get("password")
 	if username == "" || password == "" {
 		updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
 			dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
-		s.renderClientLoginPage(w, "Invalid credentials")
+		s.renderClientLoginPage(w, "Invalid credentials", ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
 			dataprovider.LoginMethodPassword, ipAddr, err)
-		s.renderClientLoginPage(w, err.Error())
+		s.renderClientLoginPage(w, err.Error(), ipAddr)
 		return
 	}
 
 	if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
-		s.renderClientLoginPage(w, fmt.Sprintf("access denied by post connect hook: %v", err))
+		s.renderClientLoginPage(w, fmt.Sprintf("access denied by post connect hook: %v", err), ipAddr)
 		return
 	}
 
 	user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)
 	if err != nil {
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
-		s.renderClientLoginPage(w, dataprovider.ErrInvalidCredentials.Error())
+		s.renderClientLoginPage(w, dataprovider.ErrInvalidCredentials.Error(), ipAddr)
 		return
 	}
 	connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
-		s.renderClientLoginPage(w, err.Error())
+		s.renderClientLoginPage(w, err.Error(), ipAddr)
 		return
 	}
 
@@ -239,7 +239,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
 	if err != nil {
 		logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
-		s.renderClientLoginPage(w, err.Error())
+		s.renderClientLoginPage(w, err.Error(), ipAddr)
 		return
 	}
 	s.loginUser(w, r, &user, connectionID, ipAddr, false, s.renderClientLoginPage)
@@ -247,27 +247,29 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
 
 func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderClientResetPwdPage(w, err.Error())
+		s.renderClientResetPwdPage(w, err.Error(), ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 	}
 	_, user, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), false)
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
-			s.renderClientResetPwdPage(w, e.GetErrorString())
+			s.renderClientResetPwdPage(w, e.GetErrorString(), ipAddr)
 			return
 		}
-		s.renderClientResetPwdPage(w, err.Error())
+		s.renderClientResetPwdPage(w, err.Error(), ipAddr)
 		return
 	}
 	connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
 	if err := checkHTTPClientUser(user, r, connectionID); err != nil {
-		s.renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
+		s.renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()), ipAddr)
 		return
 	}
 
@@ -275,10 +277,9 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
 	err = user.CheckFsRoot(connectionID)
 	if err != nil {
 		logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
-		s.renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
+		s.renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()), ipAddr)
 		return
 	}
-	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	s.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage)
 }
 
@@ -289,27 +290,28 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
 		s.renderNotFoundPage(w, r, nil)
 		return
 	}
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderClientTwoFactorRecoveryPage(w, err.Error())
+		s.renderClientTwoFactorRecoveryPage(w, err.Error(), ipAddr)
 		return
 	}
 	username := claims.Username
 	recoveryCode := r.Form.Get("recovery_code")
 	if username == "" || recoveryCode == "" {
-		s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		s.renderClientTwoFactorRecoveryPage(w, err.Error())
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
+		s.renderClientTwoFactorRecoveryPage(w, err.Error(), ipAddr)
 		return
 	}
 	user, err := dataprovider.UserExists(username)
 	if err != nil {
-		s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
 		return
 	}
 	if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
-		s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled")
+		s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled", ipAddr)
 		return
 	}
 	for idx, code := range user.Filters.RecoveryCodes {
@@ -319,23 +321,23 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
 		}
 		if code.Secret.GetPayload() == recoveryCode {
 			if code.Used {
-				s.renderClientTwoFactorRecoveryPage(w, "This recovery code was already used")
+				s.renderClientTwoFactorRecoveryPage(w, "This recovery code was already used", ipAddr)
 				return
 			}
 			user.Filters.RecoveryCodes[idx].Used = true
-			err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
+			err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr)
 			if err != nil {
 				logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
 				s.renderClientInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
 				return
 			}
 			connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
-			s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true,
+			s.loginUser(w, r, &user, connectionID, ipAddr, true,
 				s.renderClientTwoFactorRecoveryPage)
 			return
 		}
 	}
-	s.renderClientTwoFactorRecoveryPage(w, "Invalid recovery code")
+	s.renderClientTwoFactorRecoveryPage(w, "Invalid recovery code", ipAddr)
 }
 
 func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) {
@@ -345,27 +347,28 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
 		s.renderNotFoundPage(w, r, nil)
 		return
 	}
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderClientTwoFactorPage(w, err.Error())
+		s.renderClientTwoFactorPage(w, err.Error(), ipAddr)
 		return
 	}
 	username := claims.Username
 	passcode := r.Form.Get("passcode")
 	if username == "" || passcode == "" {
-		s.renderClientTwoFactorPage(w, "Invalid credentials")
+		s.renderClientTwoFactorPage(w, "Invalid credentials", ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		s.renderClientTwoFactorPage(w, err.Error())
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
+		s.renderClientTwoFactorPage(w, err.Error(), ipAddr)
 		return
 	}
 	user, err := dataprovider.UserExists(username)
 	if err != nil {
-		s.renderClientTwoFactorPage(w, "Invalid credentials")
+		s.renderClientTwoFactorPage(w, "Invalid credentials", ipAddr)
 		return
 	}
 	if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
-		s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled")
+		s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled", ipAddr)
 		return
 	}
 	err = user.Filters.TOTPConfig.Secret.Decrypt()
@@ -376,44 +379,45 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
 	match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,
 		user.Filters.TOTPConfig.Secret.GetPayload())
 	if !match || err != nil {
-		s.renderClientTwoFactorPage(w, "Invalid authentication code")
+		s.renderClientTwoFactorPage(w, "Invalid authentication code", ipAddr)
 		return
 	}
 	connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
-	s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true, s.renderClientTwoFactorPage)
+	s.loginUser(w, r, &user, connectionID, ipAddr, true, s.renderClientTwoFactorPage)
 }
 
 func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+
 	claims, err := getTokenClaims(r)
 	if err != nil {
 		s.renderNotFoundPage(w, r, nil)
 		return
 	}
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderTwoFactorRecoveryPage(w, err.Error())
+		s.renderTwoFactorRecoveryPage(w, err.Error(), ipAddr)
 		return
 	}
 	username := claims.Username
 	recoveryCode := r.Form.Get("recovery_code")
 	if username == "" || recoveryCode == "" {
-		s.renderTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		s.renderTwoFactorRecoveryPage(w, err.Error())
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
+		s.renderTwoFactorRecoveryPage(w, err.Error(), ipAddr)
 		return
 	}
 	admin, err := dataprovider.AdminExists(username)
 	if err != nil {
-		s.renderTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
 		return
 	}
 	if !admin.Filters.TOTPConfig.Enabled {
-		s.renderTwoFactorRecoveryPage(w, "Two factory authentication is not enabled")
+		s.renderTwoFactorRecoveryPage(w, "Two factory authentication is not enabled", ipAddr)
 		return
 	}
-	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	for idx, code := range admin.Filters.RecoveryCodes {
 		if err := code.Secret.Decrypt(); err != nil {
 			s.renderInternalServerErrorPage(w, r, fmt.Errorf("unable to decrypt recovery code: %w", err))
@@ -421,7 +425,7 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
 		}
 		if code.Secret.GetPayload() == recoveryCode {
 			if code.Used {
-				s.renderTwoFactorRecoveryPage(w, "This recovery code was already used")
+				s.renderTwoFactorRecoveryPage(w, "This recovery code was already used", ipAddr)
 				return
 			}
 			admin.Filters.RecoveryCodes[idx].Used = true
@@ -435,7 +439,7 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
 			return
 		}
 	}
-	s.renderTwoFactorRecoveryPage(w, "Invalid recovery code")
+	s.renderTwoFactorRecoveryPage(w, "Invalid recovery code", ipAddr)
 }
 
 func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http.Request) {
@@ -445,27 +449,28 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
 		s.renderNotFoundPage(w, r, nil)
 		return
 	}
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderTwoFactorPage(w, err.Error())
+		s.renderTwoFactorPage(w, err.Error(), ipAddr)
 		return
 	}
 	username := claims.Username
 	passcode := r.Form.Get("passcode")
 	if username == "" || passcode == "" {
-		s.renderTwoFactorPage(w, "Invalid credentials")
+		s.renderTwoFactorPage(w, "Invalid credentials", ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		s.renderTwoFactorPage(w, err.Error())
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
+		s.renderTwoFactorPage(w, err.Error(), ipAddr)
 		return
 	}
 	admin, err := dataprovider.AdminExists(username)
 	if err != nil {
-		s.renderTwoFactorPage(w, "Invalid credentials")
+		s.renderTwoFactorPage(w, "Invalid credentials", ipAddr)
 		return
 	}
 	if !admin.Filters.TOTPConfig.Enabled {
-		s.renderTwoFactorPage(w, "Two factory authentication is not enabled")
+		s.renderTwoFactorPage(w, "Two factory authentication is not enabled", ipAddr)
 		return
 	}
 	err = admin.Filters.TOTPConfig.Secret.Decrypt()
@@ -476,43 +481,44 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
 	match, err := mfa.ValidateTOTPPasscode(admin.Filters.TOTPConfig.ConfigName, passcode,
 		admin.Filters.TOTPConfig.Secret.GetPayload())
 	if !match || err != nil {
-		s.renderTwoFactorPage(w, "Invalid authentication code")
+		s.renderTwoFactorPage(w, "Invalid authentication code", ipAddr)
 		return
 	}
-	s.loginAdmin(w, r, &admin, true, s.renderTwoFactorPage, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	s.loginAdmin(w, r, &admin, true, s.renderTwoFactorPage, ipAddr)
 }
 
 func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderAdminLoginPage(w, err.Error())
+		s.renderAdminLoginPage(w, err.Error(), ipAddr)
 		return
 	}
 	username := r.Form.Get("username")
 	password := r.Form.Get("password")
 	if username == "" || password == "" {
-		s.renderAdminLoginPage(w, "Invalid credentials")
+		s.renderAdminLoginPage(w, "Invalid credentials", ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		s.renderAdminLoginPage(w, err.Error())
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
+		s.renderAdminLoginPage(w, err.Error(), ipAddr)
 		return
 	}
-	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	admin, err := dataprovider.CheckAdminAndPass(username, password, ipAddr)
 	if err != nil {
-		s.renderAdminLoginPage(w, err.Error())
+		s.renderAdminLoginPage(w, err.Error(), ipAddr)
 		return
 	}
 	s.loginAdmin(w, r, &admin, false, s.renderAdminLoginPage, ipAddr)
 }
 
-func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, error, ip string) {
 	data := loginPage{
 		CurrentURL: webAdminLoginPath,
 		Version:    version.Get().Version,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		ExtraCSS:   s.binding.ExtraCSS,
 	}
@@ -534,7 +540,7 @@ func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request
 		http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
 		return
 	}
-	s.renderAdminLoginPage(w, getFlashMessage(w, r))
+	s.renderAdminLoginPage(w, getFlashMessage(w, r), util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminLogout(w http.ResponseWriter, r *http.Request) {
@@ -553,7 +559,7 @@ func (s *httpdServer) handleWebAdminChangePwdPost(w http.ResponseWriter, r *http
 		s.renderChangePasswordPage(w, r, err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -568,26 +574,28 @@ func (s *httpdServer) handleWebAdminChangePwdPost(w http.ResponseWriter, r *http
 
 func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderResetPwdPage(w, err.Error())
+		s.renderResetPwdPage(w, err.Error(), ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
 	admin, _, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), true)
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
-			s.renderResetPwdPage(w, e.GetErrorString())
+			s.renderResetPwdPage(w, e.GetErrorString(), ipAddr)
 			return
 		}
-		s.renderResetPwdPage(w, err.Error())
+		s.renderResetPwdPage(w, err.Error(), ipAddr)
 		return
 	}
 
-	s.loginAdmin(w, r, admin, false, s.renderResetPwdPage, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	s.loginAdmin(w, r, admin, false, s.renderResetPwdPage, ipAddr)
 }
 
 func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Request) {
@@ -601,7 +609,8 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 		s.renderAdminSetupPage(w, r, "", err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -631,7 +640,6 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 		Status:      1,
 		Permissions: []string{dataprovider.PermAdminAny},
 	}
-	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err = dataprovider.AddAdmin(&admin, username, ipAddr)
 	if err != nil {
 		s.renderAdminSetupPage(w, r, username, err.Error())
@@ -642,7 +650,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 
 func (s *httpdServer) loginUser(
 	w http.ResponseWriter, r *http.Request, user *dataprovider.User, connectionID, ipAddr string,
-	isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, error string),
+	isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, error, ip string),
 ) {
 	c := jwtTokenClaims{
 		Username:                   user.Username,
@@ -662,7 +670,7 @@ func (s *httpdServer) loginUser(
 	if err != nil {
 		logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err)
 		updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
-		errorFunc(w, err.Error())
+		errorFunc(w, err.Error(), ipAddr)
 		return
 	}
 	if isSecondFactorAuth {
@@ -679,7 +687,8 @@ func (s *httpdServer) loginUser(
 
 func (s *httpdServer) loginAdmin(
 	w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
-	isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, error string), ip string,
+	isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, error, ip string),
+	ipAddr string,
 ) {
 	c := jwtTokenClaims{
 		Username:    admin.Username,
@@ -692,14 +701,14 @@ func (s *httpdServer) loginAdmin(
 		audience = tokenAudienceWebAdminPartial
 	}
 
-	err := c.createAndSetCookie(w, r, s.tokenAuth, audience, ip)
+	err := c.createAndSetCookie(w, r, s.tokenAuth, audience, ipAddr)
 	if err != nil {
 		logger.Warn(logSender, "", "unable to set admin login cookie %v", err)
 		if errorFunc == nil {
 			s.renderAdminSetupPage(w, r, admin.Username, err.Error())
 			return
 		}
-		errorFunc(w, err.Error())
+		errorFunc(w, err.Error(), ipAddr)
 		return
 	}
 	if isSecondFactorAuth {

+ 49 - 36
httpd/webadmin.go

@@ -376,7 +376,7 @@ func loadAdminTemplates(templatesPath string) {
 func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
 	var csrfToken string
 	if currentURL != "" {
-		csrfToken = createCSRFToken()
+		csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
 	}
 	return basePage{
 		Title:              title,
@@ -458,11 +458,11 @@ func (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request,
 	s.renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
 }
 
-func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, error, ip string) {
 	data := forgotPwdPage{
 		CurrentURL: webAdminForgotPwdPath,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		Title:      pageForgotPwdTitle,
 		ExtraCSS:   s.binding.ExtraCSS,
@@ -470,11 +470,11 @@ func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, error string) {
 	renderAdminTemplate(w, templateForgotPassword, data)
 }
 
-func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, error, ip string) {
 	data := resetPwdPage{
 		CurrentURL: webAdminResetPwdPath,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		Title:      pageResetPwdTitle,
 		ExtraCSS:   s.binding.ExtraCSS,
@@ -482,12 +482,12 @@ func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, error string) {
 	renderAdminTemplate(w, templateResetPassword, data)
 }
 
-func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, error, ip string) {
 	data := twoFactorPage{
 		CurrentURL:  webAdminTwoFactorPath,
 		Version:     version.Get().Version,
 		Error:       error,
-		CSRFToken:   createCSRFToken(),
+		CSRFToken:   createCSRFToken(ip),
 		StaticURL:   webStaticFilesPath,
 		RecoveryURL: webAdminTwoFactorRecoveryPath,
 		ExtraCSS:    s.binding.ExtraCSS,
@@ -495,12 +495,12 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, error string) {
 	renderAdminTemplate(w, templateTwoFactor, data)
 }
 
-func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, error, ip string) {
 	data := twoFactorPage{
 		CurrentURL: webAdminTwoFactorRecoveryPath,
 		Version:    version.Get().Version,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		ExtraCSS:   s.binding.ExtraCSS,
 	}
@@ -1376,27 +1376,29 @@ func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Req
 		s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 	}
-	s.renderForgotPwdPage(w, "")
+	s.renderForgotPwdPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderForgotPwdPage(w, err.Error())
+		s.renderForgotPwdPage(w, err.Error(), ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
 	err = handleForgotPassword(r, r.Form.Get("username"), true)
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
-			s.renderForgotPwdPage(w, e.GetErrorString())
+			s.renderForgotPwdPage(w, e.GetErrorString(), ipAddr)
 			return
 		}
-		s.renderForgotPwdPage(w, err.Error())
+		s.renderForgotPwdPage(w, err.Error(), ipAddr)
 		return
 	}
 	http.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)
@@ -1408,17 +1410,17 @@ func (s *httpdServer) handleWebAdminPasswordReset(w http.ResponseWriter, r *http
 		s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 	}
-	s.renderResetPwdPage(w, "")
+	s.renderResetPwdPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	s.renderTwoFactorPage(w, "")
+	s.renderTwoFactorPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	s.renderTwoFactorRecoveryPage(w, "")
+	s.renderTwoFactorRecoveryPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {
@@ -1443,7 +1445,8 @@ func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.R
 		s.renderProfilePage(w, r, err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1460,7 +1463,7 @@ func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.R
 	admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
 	admin.Email = r.Form.Get("email")
 	admin.Description = r.Form.Get("description")
-	err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr)
 	if err != nil {
 		s.renderProfilePage(w, r, err.Error())
 		return
@@ -1487,7 +1490,9 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	defer r.MultipartForm.RemoveAll() //nolint:errcheck
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1517,7 +1522,7 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := restoreBackup(backupContent, "", scanQuota, restoreMode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
+	if err := restoreBackup(backupContent, "", scanQuota, restoreMode, claims.Username, ipAddr); err != nil {
 		s.renderMaintenancePage(w, r, err.Error())
 		return
 	}
@@ -1594,11 +1599,12 @@ func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Reque
 		s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
-	err = dataprovider.AddAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.Method))
+	err = dataprovider.AddAdmin(&admin, claims.Username, ipAddr)
 	if err != nil {
 		s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
 		return
@@ -1624,7 +1630,8 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
 		s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1650,7 +1657,7 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
 			return
 		}
 	}
-	err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr)
 	if err != nil {
 		s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
 		return
@@ -1733,7 +1740,8 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http
 	}
 	defer r.MultipartForm.RemoveAll() //nolint:errcheck
 
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1772,7 +1780,7 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http
 		render.JSON(w, r, dump)
 		return
 	}
-	if err = RestoreFolders(dump.Folders, "", 1, 0, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
+	if err = RestoreFolders(dump.Folders, "", 1, 0, claims.Username, ipAddr); err != nil {
 		s.renderMessagePage(w, r, "Unable to save folders", "Cannot save the defined folders:",
 			getRespStatus(err), err, "")
 		return
@@ -1819,7 +1827,8 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R
 		s.renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "")
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1854,7 +1863,7 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R
 		render.JSON(w, r, dump)
 		return
 	}
-	if err = RestoreUsers(dump.Users, "", 1, 0, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
+	if err = RestoreUsers(dump.Users, "", 1, 0, claims.Username, ipAddr); err != nil {
 		s.renderMessagePage(w, r, "Unable to save users", "Cannot save the defined users:",
 			getRespStatus(err), err, "")
 		return
@@ -1898,11 +1907,12 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
 		s.renderUserPage(w, r, &user, userPageModeAdd, err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
-	err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.AddUser(&user, claims.Username, ipAddr)
 	if err == nil {
 		http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
 	} else {
@@ -1931,7 +1941,8 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
 		s.renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1947,7 +1958,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
 		user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
 		user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey)
 
-	err = dataprovider.UpdateUser(&updatedUser, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr)
 	if err == nil {
 		if len(r.Form.Get("disconnect")) > 0 {
 			disconnectUser(user.Username)
@@ -1992,7 +2003,8 @@ func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Requ
 	}
 	defer r.MultipartForm.RemoveAll() //nolint:errcheck
 
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -2051,7 +2063,8 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
 	}
 	defer r.MultipartForm.RemoveAll() //nolint:errcheck
 
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -2072,7 +2085,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
 		folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
 		folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey)
 
-	err = dataprovider.UpdateFolder(updatedFolder, folder.Users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.UpdateFolder(updatedFolder, folder.Users, claims.Username, ipAddr)
 	if err != nil {
 		s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
 		return

+ 28 - 23
httpd/webclient.go

@@ -314,7 +314,7 @@ func loadClientTemplates(templatesPath string) {
 func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
 	var csrfToken string
 	if currentURL != "" {
-		csrfToken = createCSRFToken()
+		csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
 	}
 	v := version.Get()
 
@@ -341,11 +341,11 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
 	}
 }
 
-func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error, ip string) {
 	data := forgotPwdPage{
 		CurrentURL: webClientForgotPwdPath,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		Title:      pageClientForgotPwdTitle,
 		ExtraCSS:   s.binding.ExtraCSS,
@@ -353,11 +353,11 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error str
 	renderClientTemplate(w, templateForgotPassword, data)
 }
 
-func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error, ip string) {
 	data := resetPwdPage{
 		CurrentURL: webClientResetPwdPath,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		Title:      pageClientResetPwdTitle,
 		ExtraCSS:   s.binding.ExtraCSS,
@@ -405,12 +405,12 @@ func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Re
 	s.renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
 }
 
-func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error, ip string) {
 	data := twoFactorPage{
 		CurrentURL:  webClientTwoFactorPath,
 		Version:     version.Get().Version,
 		Error:       error,
-		CSRFToken:   createCSRFToken(),
+		CSRFToken:   createCSRFToken(ip),
 		StaticURL:   webStaticFilesPath,
 		RecoveryURL: webClientTwoFactorRecoveryPath,
 		ExtraCSS:    s.binding.ExtraCSS,
@@ -418,12 +418,12 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error str
 	renderClientTemplate(w, templateTwoFactor, data)
 }
 
-func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error, ip string) {
 	data := twoFactorPage{
 		CurrentURL: webClientTwoFactorRecoveryPath,
 		Version:    version.Get().Version,
 		Error:      error,
-		CSRFToken:  createCSRFToken(),
+		CSRFToken:  createCSRFToken(ip),
 		StaticURL:  webStaticFilesPath,
 		ExtraCSS:   s.binding.ExtraCSS,
 	}
@@ -972,7 +972,8 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
 		s.renderAddUpdateSharePage(w, r, share, err.Error(), true)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -986,7 +987,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
 			return
 		}
 	}
-	err = dataprovider.AddShare(share, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.AddShare(share, claims.Username, ipAddr)
 	if err == nil {
 		http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
 	} else {
@@ -1015,7 +1016,8 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
 		s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1030,7 +1032,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
 			return
 		}
 	}
-	err = dataprovider.UpdateShare(updatedShare, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr)
 	if err == nil {
 		http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
 	} else {
@@ -1090,7 +1092,8 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
 		s.renderClientProfilePage(w, r, err.Error())
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1118,7 +1121,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
 		user.Email = r.Form.Get("email")
 		user.Description = r.Form.Get("description")
 	}
-	err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr)
 	if err != nil {
 		s.renderClientProfilePage(w, r, err.Error())
 		return
@@ -1134,12 +1137,12 @@ func (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request)
 
 func (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	s.renderClientTwoFactorPage(w, "")
+	s.renderClientTwoFactorPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	s.renderClientTwoFactorRecoveryPage(w, "")
+	s.renderClientTwoFactorRecoveryPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
@@ -1181,17 +1184,19 @@ func (s *httpdServer) handleWebClientForgotPwd(w http.ResponseWriter, r *http.Re
 		s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 	}
-	s.renderClientForgotPwdPage(w, "")
+	s.renderClientForgotPwdPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderClientForgotPwdPage(w, err.Error())
+		s.renderClientForgotPwdPage(w, err.Error(), ipAddr)
 		return
 	}
-	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 	}
@@ -1199,10 +1204,10 @@ func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *htt
 	err = handleForgotPassword(r, username, false)
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
-			s.renderClientForgotPwdPage(w, e.GetErrorString())
+			s.renderClientForgotPwdPage(w, e.GetErrorString(), ipAddr)
 			return
 		}
-		s.renderClientForgotPwdPage(w, err.Error())
+		s.renderClientForgotPwdPage(w, err.Error(), ipAddr)
 		return
 	}
 	http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
@@ -1214,7 +1219,7 @@ func (s *httpdServer) handleWebClientPasswordReset(w http.ResponseWriter, r *htt
 		s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 	}
-	s.renderClientResetPwdPage(w, "")
+	s.renderClientResetPwdPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) {

Some files were not shown because too many files changed in this diff