Implement saved sessions

This commit is contained in:
William Bouzourène 2025-01-08 15:21:47 +01:00
parent 025b984314
commit fa0e917d34
Signed by: bouzoure
SSH key fingerprint: SHA256:19MbXpLua4rUtk8tunMesD8KUKb91LXLHg8E/qTooww
9 changed files with 187 additions and 0 deletions

View file

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

View file

@ -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("/")
}

View file

@ -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 := "/"

View file

@ -37,6 +37,7 @@ func connectDatabase() (*gorm.DB, error) {
err = database.AutoMigrate(
&models.User{},
&models.UserSavedSession{},
&models.Section{},
&models.Role{},
&models.UserRole{},

56
helpers/saved_session.go Normal file
View file

@ -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)
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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
}

View file

@ -40,6 +40,17 @@
required
>
</div>
<div class="mb-3">
<input
type="checkbox"
class="form-check-input me-2"
id="save_session"
name="save_session"
>
<label for="save_session" class="form-label">
Se souvenir de moi
</label>
</div>
<div class="text-end">
<button class="btn btn-primary" type="submit">
<i class="me-1 bi-box-arrow-in-right"></i>