diff --git a/controllers/login.go b/controllers/login.go index 9fa18a6..9bb5928 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -51,6 +51,10 @@ func LoginForm(c *fiber.Ctx) error { } } + if c.FormValue("save_session") == "on" { + sess.Set("create-saved-session", "yes") + } + sess.Set("userid", user.ID) sess.Save() diff --git a/controllers/logout.go b/controllers/logout.go index 8b64e3c..8e9b5a5 100644 --- a/controllers/logout.go +++ b/controllers/logout.go @@ -16,5 +16,13 @@ func LogoutProcess(c *fiber.Ctx) error { return err } + sessionUUID := c.Cookies("saved-session-uuid") + if len(sessionUUID) > 0 { + helpers.RemoveSavedSession(sessionUUID) + + c.ClearCookie("saved-session-uuid") + c.ClearCookie("saved-session-secret") + } + return c.Redirect("/") } diff --git a/controllers/mfa.go b/controllers/mfa.go index 1b88ed0..362356a 100644 --- a/controllers/mfa.go +++ b/controllers/mfa.go @@ -91,6 +91,26 @@ func TotpEnrollPage(c *fiber.Ctx) error { sess.Set("totp-verified", "yes") + if sess.Get("create-saved-session") == "yes" { + savedSession, secret, err := helpers.CreateSavedSession(user.ID) + if err == nil { + cookieUUID := fiber.Cookie{ + Name: "saved-session-uuid", + Value: savedSession.UUID, + Expires: savedSession.Expiration, + } + + cookieSecret := fiber.Cookie{ + Name: "saved-session-secret", + Value: secret, + Expires: savedSession.Expiration, + } + + c.Cookie(&cookieUUID) + c.Cookie(&cookieSecret) + } + } + redirectId := c.Query("redirect") redirectUrl := "/" @@ -164,6 +184,26 @@ func TotpVerifyPage(c *fiber.Ctx) error { if c.Method() == "POST" { otp := c.FormValue("otp") if totp.Validate(otp, user.TotpSecret.String) { + if sess.Get("create-saved-session") == "yes" { + savedSession, secret, err := helpers.CreateSavedSession(user.ID) + if err == nil { + cookieUUID := fiber.Cookie{ + Name: "saved-session-uuid", + Value: savedSession.UUID, + Expires: savedSession.Expiration, + } + + cookieSecret := fiber.Cookie{ + Name: "saved-session-secret", + Value: secret, + Expires: savedSession.Expiration, + } + + c.Cookie(&cookieUUID) + c.Cookie(&cookieSecret) + } + } + redirectId := c.Query("redirect") redirectUrl := "/" diff --git a/helpers/database.go b/helpers/database.go index 4783ce5..652f2eb 100644 --- a/helpers/database.go +++ b/helpers/database.go @@ -37,6 +37,7 @@ func connectDatabase() (*gorm.DB, error) { err = database.AutoMigrate( &models.User{}, + &models.UserSavedSession{}, &models.Section{}, &models.Role{}, &models.UserRole{}, diff --git a/helpers/saved_session.go b/helpers/saved_session.go new file mode 100644 index 0000000..26cc6a5 --- /dev/null +++ b/helpers/saved_session.go @@ -0,0 +1,56 @@ +package helpers + +import ( + "crypto/rand" + "encoding/hex" + "time" + + "git.readonly.ch/bouzoure/pop-camarades/models" + "github.com/google/uuid" +) + +func CreateSavedSession(userid uint) (models.UserSavedSession, string, error) { + var savedSession models.UserSavedSession + + db, err := GetDatabase() + if err != nil { + return savedSession, "", err + } + + secret := GenerateSecureToken(30) + hashedSecret, err := HashPassword(secret) + if err != nil { + return savedSession, "", err + } + + now := time.Now() + expiration := now.AddDate(0, 0, 30) + + savedSession.UserID = userid + savedSession.UUID = uuid.NewString() + savedSession.Secret = hashedSecret + savedSession.Expiration = expiration + + db.Create(&savedSession) + + return savedSession, secret, nil +} + +func RemoveSavedSession(uuid string) error { + db, err := GetDatabase() + if err != nil { + return err + } + + db.Delete(&models.UserSavedSession{}, "uuid = ?", uuid) + + return nil +} + +func GenerateSecureToken(length int) string { + b := make([]byte, length) + if _, err := rand.Read(b); err != nil { + return "" + } + return hex.EncodeToString(b) +} diff --git a/main.go b/main.go index 904d861..5072c6e 100644 --- a/main.go +++ b/main.go @@ -89,6 +89,7 @@ func main() { app.Use(helmet.New()) // Security middlewares + app.Use(middlewares.SavedSessionMiddleware) app.Use(middlewares.AuthMiddleware) app.Use(middlewares.WelcomeMiddleware) app.Use(middlewares.MfaEnrollMiddleware) diff --git a/middlewares/saved_session.go b/middlewares/saved_session.go new file mode 100644 index 0000000..bcbd046 --- /dev/null +++ b/middlewares/saved_session.go @@ -0,0 +1,55 @@ +package middlewares + +import ( + "errors" + "time" + + "git.readonly.ch/bouzoure/pop-camarades/helpers" + "git.readonly.ch/bouzoure/pop-camarades/models" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func SavedSessionMiddleware(c *fiber.Ctx) error { + sessionUUID := c.Cookies("saved-session-uuid") + sessionSecret := c.Cookies("saved-session-secret") + + if len(sessionUUID) > 0 && len(sessionSecret) > 0 { + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + var savedSession models.UserSavedSession + result := db.Find( + &savedSession, + "uuid = ? AND expiration >= ?", + sessionUUID, + time.Now(), + ) + + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.ClearCookie("saved-session-uuid") + c.ClearCookie("saved-session-secret") + + return c.Next() + } + + if result.Error != nil { + return result.Error + } + + if helpers.CheckPasswordHash(sessionSecret, savedSession.Secret) { + sess, err := helpers.GetSessionStore(c) + if err != nil { + return err + } + + sess.Set("userid", savedSession.UserID) + sess.Set("totp-verified", "yes") + sess.Save() + } + } + + return c.Next() +} diff --git a/models/users.go b/models/users.go index b71743c..367c020 100644 --- a/models/users.go +++ b/models/users.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "time" "gorm.io/gorm" ) @@ -25,3 +26,13 @@ type UserRole struct { SectionID uint Section Section } + +// TODO: Autoclean expired sessions +type UserSavedSession struct { + gorm.Model + UserID uint + User User + UUID string + Secret string + Expiration time.Time +} diff --git a/views/login.html b/views/login.html index 097ba26..0794759 100644 --- a/views/login.html +++ b/views/login.html @@ -40,6 +40,17 @@ required > +
+ + +