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/limit login to one mobile device #367

Merged
merged 9 commits into from
Sep 22, 2024
21 changes: 21 additions & 0 deletions occupi-backend/configs/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ func CreateCache() *redis.Client {
return client
}

// Create mobile link cache
func CreateMobileCache() *redis.Client {
redisUsername := GetRedisUsername()
redisPassword := GetRedisPassword()
redisHost := GetRedisHost()
redisPort := GetRedisPort()

url := fmt.Sprintf("redis://%s:%s@%s:%s/0?protocol=3", redisUsername, redisPassword, redisHost, redisPort)
opts, err := redis.ParseURL(url)
if err != nil {
logrus.Fatal(err)
}

client := redis.NewClient(opts)

fmt.Println("Cache created!")
logrus.Info("Cache created!")

return client
}

// Create cache for sessions
func CreateSessionCache() *bigcache.BigCache {
config := bigcache.DefaultConfig(time.Duration(GetCacheEviction()) * time.Second) // Set the eviction time to x seconds
Expand Down
62 changes: 62 additions & 0 deletions occupi-backend/pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,65 @@

return false, nil
}

func GetMobileUser(appsession *models.AppSession, email string) (models.MobileUser, error) {
if appsession.MobileCache == nil {
return models.MobileUser{}, errors.New("cache not found")
}

// unmarshal the user from the cache
var user models.MobileUser
res := appsession.MobileCache.Get(context.Background(), MobileUserKey(email))

if res.Err() != nil {
logrus.Error("key does not exist: ", res.Err())
return models.MobileUser{}, res.Err()
}

userData, err := res.Bytes()

if err != nil {
logrus.Error("failed to get bytes", err)
return models.MobileUser{}, err

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

View check run for this annotation

Codecov / codecov/patch

occupi-backend/pkg/cache/cache.go#L324-L325

Added lines #L324 - L325 were not covered by tests
}

if err := bson.Unmarshal(userData, &user); err != nil {
logrus.Error("failed to unmarshall", err)
return models.MobileUser{}, err
}

return user, nil
}

func SetMobileUser(appsession *models.AppSession, user models.MobileUser) {
if appsession.MobileCache == nil {
return
}

// marshal the user
userData, err := bson.Marshal(user)
if err != nil {
logrus.Error("failed to marshall", err)
return

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

View check run for this annotation

Codecov / codecov/patch

occupi-backend/pkg/cache/cache.go#L344-L345

Added lines #L344 - L345 were not covered by tests
}

// set the user in the cache
res := appsession.MobileCache.Set(context.Background(), MobileUserKey(user.Email), userData, 0)

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

func DeleteMobileUser(appsession *models.AppSession, email string) {
if appsession.MobileCache == nil {
return
}

// delete the user from the cache
res := appsession.MobileCache.Del(context.Background(), MobileUserKey(email))

if res.Err() != nil {
logrus.Error("failed to delete user from cache", res.Err())
}
}
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 @@ -23,3 +23,7 @@ func SessionKey(email string) string {
func LoginKey(email string) string {
return "Login:" + email
}

func MobileUserKey(email string) string {
return "MobileUsers:" + email
}
12 changes: 12 additions & 0 deletions occupi-backend/pkg/database/database_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ func IsLocationInRange(locations []models.Location, unrecognizedLogger *ipinfo.C
return true
}

// check if each loc.Location is == "" and return true if all are
allEmpty := true
for _, loc := range locations {
if loc.Location != "" {
allEmpty = false
break
}
}
if allEmpty {
return true
}

for _, loc := range locations {
// Skip if loc.Location is empty
if loc.Location == "" {
Expand Down
25 changes: 24 additions & 1 deletion occupi-backend/pkg/handlers/auth_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string, cookies
return
}

AddMobileUser(ctx, appsession, requestUser.Email, token)

// Use AllocateAuthTokens to handle the response
AllocateAuthTokens(ctx, token, expirationTime, cookies)
}
Expand Down Expand Up @@ -573,6 +575,8 @@ func VerifyOTP(ctx *gin.Context, appsession *models.AppSession, login bool, role
return
}

AddMobileUser(ctx, appsession, userotp.Email, token)

// Use AllocateAuthTokens to handle the response
AllocateAuthTokens(ctx, token, expirationTime, cookies)
}
Expand Down Expand Up @@ -757,12 +761,31 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession, role string,
return
}

AddMobileUser(ctx, appsession, resetRequest.Email, token)

// Use AllocateAuthTokens to handle the response
AllocateAuthTokens(ctx, token, exp, cookies)
}

// handler for logging out a request
func Logout(ctx *gin.Context) {
func Logout(ctx *gin.Context, appsession *models.AppSession) {
claims, err := utils.GetClaimsFromCTX(ctx)

if err != nil {
ctx.JSON(http.StatusUnauthorized,
utils.ErrorResponse(
http.StatusUnauthorized,
"Bad Request",
constants.InvalidAuthCode,
"User not authorized or Invalid auth token",
nil))
ctx.Abort()
return
}

// clear token from cache
cache.DeleteMobileUser(appsession, claims.Email)

_ = utils.ClearSession(ctx)

// Clear the Authorization header
Expand Down
19 changes: 17 additions & 2 deletions occupi-backend/pkg/handlers/auth_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,14 +357,14 @@ func PreLoginAccountChecks(ctx *gin.Context, appsession *models.AppSession, emai
return false, nil
}

isAllowedNewIP, err := database.CheckIfUserIsAllowedNewIP(ctx, appsession, email)
blockAnonymousIPAddress, err := database.CheckIfUserIsAllowedNewIP(ctx, appsession, email)

if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
return false, err
}

if !isAllowedNewIP {
if blockAnonymousIPAddress {
ctx.JSON(http.StatusForbidden, utils.ErrorResponse(
http.StatusForbidden,
"Forbidden from access",
Expand Down Expand Up @@ -599,3 +599,18 @@ func CanLogin(ctx *gin.Context, appsession *models.AppSession, email string) (bo
}
return true, nil
}

func AddMobileUser(ctx *gin.Context, appsession *models.AppSession, email string, jwt string) {
// check if ctx req header is a mobile device(either iOS or Android)
if !utils.IsMobileDevice(ctx) {
return
}

mobileUser := models.MobileUser{
Email: email,
JWT: jwt,
}

// add the user to the mobile user cache(or overwrite the user if they already exist)
cache.SetMobileUser(appsession, mobileUser)
}
42 changes: 42 additions & 0 deletions occupi-backend/pkg/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"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/models"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils"
Expand Down Expand Up @@ -61,6 +62,47 @@ func ProtectedRoute(ctx *gin.Context) {
ctx.Next()
}

func VerifyMobileUser(ctx *gin.Context, appsession *models.AppSession) {
claims, err := utils.GetClaimsFromCTX(ctx)

if err != nil {
ctx.JSON(http.StatusUnauthorized,
utils.ErrorResponse(
http.StatusUnauthorized,
"Bad Request",
constants.InvalidAuthCode,
"User not authorized or Invalid auth token or You may have forgotten to include the Authorization header",
nil))
ctx.Abort()
return
}

if utils.IsMobileDevice(ctx) {
user, errv := cache.GetMobileUser(appsession, claims.Email)
if errv != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error(errv)
ctx.Abort()
return
}

headertokenStr := ctx.GetHeader("Authorization")

// check if the jwt tokens match
if user.JWT != headertokenStr {
ctx.JSON(http.StatusUnauthorized,
utils.ErrorResponse(
http.StatusUnauthorized,
"Bad Request",
constants.InvalidAuthCode,
"This token is no longer valid as another device has logged into this account",
nil))
ctx.Abort()
return
}
}
}

// ProtectedRoute is a middleware that checks if
// the user has not been authenticated previously.
func UnProtectedRoute(ctx *gin.Context) {
Expand Down
2 changes: 2 additions & 0 deletions occupi-backend/pkg/models/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type AppSession struct {
Counter *Counter
MailConn *gomail.Dialer
AzureClient *azblob.Client
MobileCache *redis.Client
}

// constructor for app session
Expand All @@ -57,6 +58,7 @@ func New(db *mongo.Client, cache *redis.Client) *AppSession {
MailConn: configs.CreateMailServerConnection(),
AzureClient: configs.CreateAzureBlobClient(),
Counter: CreateCounter(centrifugo),
MobileCache: configs.CreateMobileCache(),
}
}

Expand Down
5 changes: 5 additions & 0 deletions occupi-backend/pkg/models/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,8 @@ type Attendance struct {
NumberAttended int `json:"Number_Attended" bson:"Number_Attended"`
AttendeesEmail []string `json:"Attendees_Email" bson:"Attendees_Email"`
}

type MobileUser struct {
Email string `json:"email" bson:"email"`
JWT string `json:"jwt" bson:"jwt"`
}
2 changes: 1 addition & 1 deletion occupi-backend/pkg/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) {
auth.POST("/verify-otp-admin-login", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.VerifyOTP(ctx, appsession, true, constants.Admin, true) })
auth.POST("/verify-otp-mobile-login", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.VerifyOTP(ctx, appsession, true, constants.Basic, false) })
auth.POST("/verify-otp-mobile-admin-login", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.VerifyOTP(ctx, appsession, true, constants.Admin, false) })
auth.POST("/logout", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.Logout(ctx) })
auth.POST("/logout", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.Logout(ctx, appsession) })
// it's typically used by users who can't log in because they've forgotten their password.

auth.POST("/reset-password-login", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.ResetPassword(ctx, appsession, constants.Basic, true) })
Expand Down
27 changes: 27 additions & 0 deletions occupi-backend/pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,30 @@ func ValidateIP(ip string) bool {
var ipRegex = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`)
return ipRegex.MatchString(ip)
}

func IsMobileDevice(ctx *gin.Context) bool {
userAgent := ctx.GetHeader("User-Agent")
deviceType := DetectDeviceType(userAgent)
if deviceType == "iOS" || deviceType == "Android" {
return true
}
return false
}

func DetectDeviceType(userAgent string) string {
switch {
case strings.Contains(userAgent, "CFNetwork"),
strings.Contains(userAgent, "iPhone"),
strings.Contains(userAgent, "iPad"):
return "iOS"

case strings.Contains(userAgent, "Macintosh"):
return "macOS"

case strings.Contains(userAgent, "Android"):
return "Android"

default:
return "Unknown"
}
}
Loading
Loading