Skip to content

Commit

Permalink
Merge pull request #338 from COS301-SE-2024/feat/backend/block-login-…
Browse files Browse the repository at this point in the history
…spamming

Feat/backend/block login spamming
  • Loading branch information
waveyboym authored Aug 23, 2024
2 parents e505fa7 + 568995b commit af2ac3a
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 4 deletions.
54 changes: 52 additions & 2 deletions occupi-backend/pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"go.mongodb.org/mongo-driver/bson"
)

// TODO: Add methods to prevent users from requesting more than 5 logins per day, 5 otps per day, etc. but waiting for go-redis to be integrated first

func GetUser(appsession *models.AppSession, email string) (models.User, error) {
if appsession.Cache == nil {
return models.User{}, errors.New("cache not found")
Expand Down Expand Up @@ -317,3 +315,55 @@ func DeleteSession(appsession *models.AppSession, uuid string) {
return
}
}

func CanMakeLogin(appsession *models.AppSession, email string) (bool, error) {
if appsession.Cache == nil {
return false, errors.New("cache not found")
}

var eviction time.Duration
if configs.GetGinRunMode() == "test" {
eviction = 2 * time.Second
} else {
eviction = 24 * time.Hour
}

// check if the user can make a login request
res := appsession.Cache.Get(context.Background(), LoginKey(email))

if res.Err() != nil {
// add the user to the cache with a value of 1 and evict after one day
// set the image in the cache
res1 := appsession.Cache.Set(context.Background(), LoginKey(email), 1, eviction)

if res1.Err() != nil {
logrus.Error("failed to set user in cache", res1.Err())
return false, res1.Err()
}

return true, nil
}

// check if the user has made more than 5 login requests
loginCount, err := res.Int()

if err != nil {
return false, err
}

// Check if the value is less than 5
if loginCount < 5 {
// Increment the value
loginCount += 1

// Set the new value with the same expiration time (24 hours)
err = appsession.Cache.Set(context.Background(), LoginKey(email), loginCount, eviction).Err()
if err != nil {
return false, err
}

return true, nil
}

return false, nil
}
4 changes: 4 additions & 0 deletions occupi-backend/pkg/cache/cache_keys_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ func ImageKey(imageID string) string {
func SessionKey(email string) string {
return "Sessions:" + email
}

func LoginKey(email string) string {
return "Login:" + email
}
1 change: 1 addition & 0 deletions occupi-backend/pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
UnAuthorizedCode = "UNAUTHORIZED"
RequestEntityTooLargeCode = "REQUEST_ENTITY_TOO_LARGE"
ForbiddenCode = "FORBIDDEN"
TooManyRequestsCode = "TOO_MANY_REQUESTS"
Admin = "admin"
Basic = "basic"
AdminDBAccessOption = "authSource=admin"
Expand Down
36 changes: 34 additions & 2 deletions occupi-backend/pkg/handlers/auth_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string, cookies
return
}

if canLogin, err := CanLogin(ctx, appsession, requestUser.Email); !canLogin {
if err != nil {
captureError(ctx, err)
logrus.WithError(err).Error("Error checking if user can login")
}
return
}

// sanitize user password and email
requestUser.EmployeeID = utils.SanitizeInput(requestUser.EmployeeID)

Expand Down Expand Up @@ -105,6 +113,14 @@ func BeginLoginAdmin(ctx *gin.Context, appsession *models.AppSession) {
return
}

if canLogin, err := CanLogin(ctx, appsession, requestEmail.Email); !canLogin {
if err != nil {
captureError(ctx, err)
logrus.WithError(err).Error("Error checking if user can login")
}
return
}

// validate email exists
if valid, err := ValidateEmailExists(ctx, appsession, requestEmail.Email); !valid {
if err != nil {
Expand Down Expand Up @@ -155,7 +171,7 @@ func BeginLoginAdmin(ctx *gin.Context, appsession *models.AppSession) {
}

// Save the session data - cache will expire in x defined minutes according to the config
if err := cache.SetSession(appsession, session, uuid); err != nil && err.Error() != "cache not found" {
if err := cache.SetSession(appsession, session, uuid); err != nil {
captureError(ctx, err)
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
fmt.Printf("error saving WebAuthn session data: %v", err)
Expand Down Expand Up @@ -239,6 +255,14 @@ func BeginRegistrationAdmin(ctx *gin.Context, appsession *models.AppSession) {
return
}

if canLogin, err := CanLogin(ctx, appsession, requestEmail.Email); !canLogin {
if err != nil {
captureError(ctx, err)
logrus.WithError(err).Error("Error checking if user can login")
}
return
}

// validate email exists
if valid, err := ValidateEmailExists(ctx, appsession, requestEmail.Email); !valid {
if err != nil {
Expand Down Expand Up @@ -278,7 +302,7 @@ func BeginRegistrationAdmin(ctx *gin.Context, appsession *models.AppSession) {
}

// Save the session data - cache will expire in x defined minutes according to the config
if err := cache.SetSession(appsession, session, uuid); err != nil && err.Error() != "cache not found" {
if err := cache.SetSession(appsession, session, uuid); err != nil {
captureError(ctx, err)
logrus.WithError(err).Error("Error saving session data in cache")
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
Expand Down Expand Up @@ -363,6 +387,14 @@ func Register(ctx *gin.Context, appsession *models.AppSession) {
return
}

if canLogin, err := CanLogin(ctx, appsession, requestUser.Email); !canLogin {
if err != nil {
captureError(ctx, err)
logrus.WithError(err).Error("Error checking if user can login")
}
return
}

// sanitize user password and email
requestUser.EmployeeID = utils.SanitizeInput(requestUser.EmployeeID)

Expand Down
14 changes: 14 additions & 0 deletions occupi-backend/pkg/handlers/auth_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/mail"
Expand Down Expand Up @@ -554,3 +555,16 @@ func AttemptToSignNewEmail(ctx *gin.Context, appsession *models.AppSession, emai
}
return nil
}

func CanLogin(ctx *gin.Context, appsession *models.AppSession, email string) (bool, error) {
if canLogin, err := cache.CanMakeLogin(appsession, email); !canLogin && (err == nil || err.Error() != "cache not found") {
ctx.JSON(http.StatusTooManyRequests, utils.ErrorResponse(
http.StatusTooManyRequests,
"Too many login attempts",
constants.TooManyRequestsCode,
"Too many login attempts, please try again later",
nil))
return false, err
}
return true, nil
}
130 changes: 130 additions & 0 deletions occupi-backend/tests/cache_methods_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ func TestImageKey(t *testing.T) {
}
}

func TestLoginKey(t *testing.T) {
email := "[email protected]"
expected := "Login:[email protected]"
result := cache.LoginKey(email)
if result != expected {
t.Errorf("LoginKey(%s) = %s; want %s", email, result, expected)
}
}

func TestGetUser(t *testing.T) {
// Test case: cache is nil
t.Run("cache is nil", func(t *testing.T) {
Expand Down Expand Up @@ -1080,3 +1089,124 @@ func TestDeleteSession(t *testing.T) {
}
})
}

func TestCanMakeLogin(t *testing.T) {
email := "[email protected]"
key := cache.LoginKey(email)

t.Run("cache not found", func(t *testing.T) {
// Set up a mock Redis client
db, _ := redismock.NewClientMock()

appsession := &models.AppSession{
Cache: db,
}
appsession.Cache = nil
canLogin, err := cache.CanMakeLogin(appsession, email)
assert.False(t, canLogin)
assert.EqualError(t, err, "cache not found")
})

t.Run("new user - set value", func(t *testing.T) {
// Set up a mock Redis client
db, mock := redismock.NewClientMock()

appsession := &models.AppSession{
Cache: db,
}
// Simulate Get returning a nil value (key not found)
mock.ExpectGet(key).RedisNil()
// Simulate successful Set operation
mock.ExpectSet(key, 1, 2*time.Second).SetVal("OK")

canLogin, err := cache.CanMakeLogin(appsession, email)
assert.True(t, canLogin)
assert.NoError(t, err)

// Ensure all expectations were met
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
})

t.Run("existing user with login count less than 5", func(t *testing.T) {
// Set up a mock Redis client
db, mock := redismock.NewClientMock()

appsession := &models.AppSession{
Cache: db,
}
// Simulate Get returning a value of 3
mock.ExpectGet(key).SetVal("3")
// Simulate successful Set operation to update the value to 4
mock.ExpectSet(key, 4, 2*time.Second).SetVal("OK")

canLogin, err := cache.CanMakeLogin(appsession, email)
assert.True(t, canLogin)
assert.NoError(t, err)

// Ensure all expectations were met
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
})

t.Run("existing user with login count 5 or more", func(t *testing.T) {
// Set up a mock Redis client
db, mock := redismock.NewClientMock()

appsession := &models.AppSession{
Cache: db,
}
// Simulate Get returning a value of 5
mock.ExpectGet(key).SetVal("5")

canLogin, err := cache.CanMakeLogin(appsession, email)
assert.False(t, canLogin)
assert.NoError(t, err)

// Ensure all expectations were met
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
})

t.Run("error on Get", func(t *testing.T) {
// Set up a mock Redis client
db, mock := redismock.NewClientMock()

appsession := &models.AppSession{
Cache: db,
}
// Simulate Get operation error
mock.ExpectGet(key).SetErr(errors.New("redis get error"))
// Simulate successful Set operation
mock.ExpectSet(key, 1, 2*time.Second).SetVal("OK")

canLogin, err := cache.CanMakeLogin(appsession, email)
assert.True(t, canLogin)
assert.NoError(t, err)

// Ensure all expectations were met
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
})

t.Run("error on Set", func(t *testing.T) {
// Set up a mock Redis client
db, mock := redismock.NewClientMock()

appsession := &models.AppSession{
Cache: db,
}
// Simulate Get returning a value of 3
mock.ExpectGet(key).SetVal("3")
// Simulate Set operation error
mock.ExpectSet(key, 4, 2*time.Second).SetErr(errors.New("redis set error"))

canLogin, err := cache.CanMakeLogin(appsession, email)
assert.False(t, canLogin)
assert.EqualError(t, err, "redis set error")

// Ensure all expectations were met
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
})
}

0 comments on commit af2ac3a

Please sign in to comment.