Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/backend/block login spamming #338

Merged
merged 2 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@
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 warning on line 328 in occupi-backend/pkg/cache/cache.go

View check run for this annotation

Codecov / codecov/patch

occupi-backend/pkg/cache/cache.go#L328

Added line #L328 was not covered by tests
}

// 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()

Check warning on line 341 in occupi-backend/pkg/cache/cache.go

View check run for this annotation

Codecov / codecov/patch

occupi-backend/pkg/cache/cache.go#L340-L341

Added lines #L340 - L341 were not covered by tests
}

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 warning on line 351 in occupi-backend/pkg/cache/cache.go

View check run for this annotation

Codecov / codecov/patch

occupi-backend/pkg/cache/cache.go#L351

Added line #L351 was not covered by tests
}

// 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)
})
}
Loading