From cc4135d14b43975f9c992c1a1e7800af4b9239fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Sun, 22 Dec 2024 16:54:42 +0100 Subject: [PATCH] Rework login process & implement MFA --- controllers/login.go | 75 +++++++++++----------- controllers/mfa.go | 117 +++++++++++++++++++++++++++++++++- main.go | 10 ++- middlewares/authentication.go | 14 ---- middlewares/mfa.go | 92 ++++++++++++++++++++++++++ views/totp_enroll.html | 2 +- views/totp_verify.html | 43 +++++++++++++ 7 files changed, 297 insertions(+), 56 deletions(-) create mode 100644 views/totp_verify.html diff --git a/controllers/login.go b/controllers/login.go index 75a058e..0eb9761 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -12,61 +12,62 @@ import ( ) func LoginForm(c *fiber.Ctx) error { - return c.Render("login", fiber.Map{ - "PageTitle": "Connexion", - }) -} - -func LoginProcess(c *fiber.Ctx) error { sess, err := helpers.GetSessionStore(c) if err != nil { return err } + userid := sess.Get("userid") + if userid != nil { + return fiber.NewError(fiber.StatusForbidden, "Forbidden") + } + db, err := helpers.GetDatabase() if err != nil { return err } - email := c.FormValue("email") - password := c.FormValue("password") + var loginError string + if c.Method() == "POST" { + email := c.FormValue("email") + password := c.FormValue("password") - var user models.User - result := db.First( - &user, - "LOWER(email) = LOWER(?) AND (disabled_at IS NULL OR disabled_at <= ?)", - email, - time.Now(), - ) + var user models.User + result := db.First( + &user, + "LOWER(email) = LOWER(?) AND (disabled_at IS NULL OR disabled_at <= ?)", + email, + time.Now(), + ) - allowLogin := false - if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { - return err - } else { - allowLogin = helpers.CheckPasswordHash(password, user.Password) - } + if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return err + } - if !allowLogin { - return c.Render("login", fiber.Map{ - "PageTitle": "Connexion", - "LoginError": "Email ou mot de passe incorrect", - }) - } + if helpers.CheckPasswordHash(password, user.Password) { + sess.Set("userid", user.ID) + sess.Save() - sess.Set("userid", user.ID) - sess.Save() + redirectId := c.Query("redirect") + redirectUrl := "/" - redirectId := c.Query("redirect") - redirectUrl := "/" + if len(redirectId) > 0 { + redirectKey := fmt.Sprintf("redirect-%s", redirectId) + redirectVal := sess.Get(redirectKey) - if len(redirectId) > 0 { - redirectKey := fmt.Sprintf("redirect-%s", redirectId) - redirectVal := sess.Get(redirectKey) + if redirectVal != nil { + redirectUrl = redirectVal.(string) + } + } - if redirectVal != nil { - redirectUrl = redirectVal.(string) + return c.Redirect(redirectUrl) + } else { + loginError = "Email ou mot de passe incorrect" } } - return c.Redirect(redirectUrl) + return c.Render("login", fiber.Map{ + "PageTitle": "Connexion", + "LoginError": loginError, + }) } diff --git a/controllers/mfa.go b/controllers/mfa.go index d356460..e366910 100644 --- a/controllers/mfa.go +++ b/controllers/mfa.go @@ -2,6 +2,7 @@ package controllers import ( "bytes" + "encoding/base32" "encoding/base64" "fmt" "image/png" @@ -44,6 +45,15 @@ func TotpEnrollPage(c *fiber.Ctx) error { AccountName: user.Email, } + existingSecret := sess.Get("totp-enroll-secret") + if existingSecret != nil { + var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) + options.Secret, err = b32NoPadding.DecodeString(existingSecret.(string)) + if err != nil { + return err + } + } + key, err := totp.Generate(options) if err != nil { return err @@ -65,6 +75,44 @@ func TotpEnrollPage(c *fiber.Ctx) error { base64.StdEncoding.EncodeToString(buf.Bytes()), ) + var mfaError string + if c.Method() == "POST" { + otp := c.FormValue("otp") + if totp.Validate(otp, key.Secret()) { + err = user.TotpSercet.Scan(key.Secret()) + if err != nil { + return err + } + + result = db.Save(&user) + if result.Error != nil { + return err + } + + sess.Set("totp-verified", "yes") + err = sess.Save() + if err != nil { + return err + } + + redirectId := c.Query("redirect") + redirectUrl := "/" + + if len(redirectId) > 0 { + redirectKey := fmt.Sprintf("redirect-%s", redirectId) + redirectVal := sess.Get(redirectKey) + + if redirectVal != nil { + redirectUrl = redirectVal.(string) + } + } + + return c.Redirect(redirectUrl) + } else { + mfaError = "Code temporaire invalide" + } + } + sess.Set("totp-enroll-secret", key.Secret()) err = sess.Save() if err != nil { @@ -72,8 +120,75 @@ func TotpEnrollPage(c *fiber.Ctx) error { } return c.Render("totp_enroll", fiber.Map{ - "PageTitle": "Enregistrement multifacteur", + "PageTitle": "Enregistrement multifacteur (TOTP)", "QrCode": imgBase64, "Secret": key.Secret(), + "MfaError": mfaError, + }) +} + +func TotpVerifyPage(c *fiber.Ctx) error { + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + sess, err := helpers.GetSessionStore(c) + if err != nil { + return err + } + + totpVerified := sess.Get("totp-verified") + if totpVerified != nil { + return fiber.NewError(fiber.StatusForbidden, "Forbidden") + } + + userid, err := helpers.GetSessionUserId(c) + if err != nil { + return err + } + + var user models.User + result := db.First(&user, "id = ?", userid) + + if result.Error != nil { + return err + } + + if !user.TotpSercet.Valid { + return fiber.NewError(fiber.StatusForbidden, "Forbidden") + } + + var mfaError string + if c.Method() == "POST" { + otp := c.FormValue("otp") + if totp.Validate(otp, user.TotpSercet.String) { + sess.Set("totp-verified", "yes") + err = sess.Save() + if err != nil { + return err + } + + redirectId := c.Query("redirect") + redirectUrl := "/" + + if len(redirectId) > 0 { + redirectKey := fmt.Sprintf("redirect-%s", redirectId) + redirectVal := sess.Get(redirectKey) + + if redirectVal != nil { + redirectUrl = redirectVal.(string) + } + } + + return c.Redirect(redirectUrl) + } else { + mfaError = "Code temporaire invalide" + } + } + + return c.Render("totp_verify", fiber.Map{ + "PageTitle": "Vérification multifacteur (TOTP)", + "MfaError": mfaError, }) } diff --git a/main.go b/main.go index f7de8ff..c8e1312 100644 --- a/main.go +++ b/main.go @@ -81,17 +81,21 @@ func main() { // Middlewares app.Use(middlewares.AuthMiddleware) - app.Use("/login", middlewares.DenyAuthMiddleware) app.Use(middlewares.WelcomeMiddleware) + app.Use(middlewares.MfaEnrollMiddleware) + app.Use(middlewares.MfaVerifyMiddleware) // Controllers app.Get("/", controllers.Homepage) app.Get("/login", controllers.LoginForm) - app.Post("/login", controllers.LoginProcess) + app.Post("/login", controllers.LoginForm) app.Get("/logout", controllers.LogoutProcess) app.Get("/welcome", controllers.WelcomePage) app.Post("/welcome", controllers.WelcomePage) - app.Get("/mfa/totp/enroll", controllers.TotpEnrollPage) + app.Get("/totp/enroll", controllers.TotpEnrollPage) + app.Post("/totp/enroll", controllers.TotpEnrollPage) + app.Get("/totp/verify", controllers.TotpVerifyPage) + app.Post("/totp/verify", controllers.TotpVerifyPage) listenAddr := fmt.Sprintf( "%s:%d", diff --git a/middlewares/authentication.go b/middlewares/authentication.go index e10464f..fb2c68d 100644 --- a/middlewares/authentication.go +++ b/middlewares/authentication.go @@ -55,17 +55,3 @@ func AuthMiddleware(c *fiber.Ctx) error { return c.Next() } - -func DenyAuthMiddleware(c *fiber.Ctx) error { - sess, err := helpers.GetSessionStore(c) - if err != nil { - return err - } - - userid := sess.Get("userid") - if userid != nil { - return fiber.NewError(fiber.StatusForbidden, "Forbidden") - } - - return c.Next() -} diff --git a/middlewares/mfa.go b/middlewares/mfa.go index ea553b7..b745969 100644 --- a/middlewares/mfa.go +++ b/middlewares/mfa.go @@ -1 +1,93 @@ package middlewares + +import ( + "fmt" + "strings" + + "git.readonly.ch/bouzoure/popvaud-people/helpers" + "git.readonly.ch/bouzoure/popvaud-people/models" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +func MfaEnrollMiddleware(c *fiber.Ctx) error { + if c.Path() == "/login" || c.Path() == "/welcome" || strings.HasPrefix(c.Path(), "/totp/") { + return c.Next() + } + + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + userid, err := helpers.GetSessionUserId(c) + if err != nil { + return err + } + + var user models.User + result := db.First(&user, "id = ?", userid) + + if result.Error != nil { + return err + } + + if user.TotpSercet.Valid { + return c.Next() + } + + if c.Path() == "/" { + return c.Redirect("/totp/enroll") + } + + id := uuid.NewString() + key := fmt.Sprintf("redirect-%s", id) + + sess, err := helpers.GetSessionStore(c) + if err != nil { + return err + } + + sess.Set(key, c.Path()) + sess.Save() + + redirectUrl := fmt.Sprintf( + "/totp/enroll?redirect=%s", + id, + ) + + return c.Redirect(redirectUrl) +} + +func MfaVerifyMiddleware(c *fiber.Ctx) error { + if c.Path() == "/login" || c.Path() == "/welcome" || strings.HasPrefix(c.Path(), "/totp/") { + return c.Next() + } + + sess, err := helpers.GetSessionStore(c) + if err != nil { + return err + } + + totpVerified := sess.Get("totp-verified") + if totpVerified == nil { + if c.Path() == "/" { + return c.Redirect("/totp/verify") + } + + id := uuid.NewString() + key := fmt.Sprintf("redirect-%s", id) + + sess.Set(key, c.Path()) + sess.Save() + + redirectUrl := fmt.Sprintf( + "/totp/verify?redirect=%s", + id, + ) + + return c.Redirect(redirectUrl) + } + + return c.Next() +} diff --git a/views/totp_enroll.html b/views/totp_enroll.html index 8b6e96e..c19f5cc 100644 --- a/views/totp_enroll.html +++ b/views/totp_enroll.html @@ -5,7 +5,7 @@
- Vérification multifacteur (TOTP) + Enregistrement multifacteur (TOTP)
{% if MfaError %} diff --git a/views/totp_verify.html b/views/totp_verify.html new file mode 100644 index 0000000..4fc761e --- /dev/null +++ b/views/totp_verify.html @@ -0,0 +1,43 @@ +{% extends "layouts/main.html" %} + +{% block main %} +
+
+
+
+ Vérification multifacteur (TOTP) +
+
+ {% if MfaError %} +
+ {{ MfaError }} +
+ {% endif %} + +
+
+ + +
+
+ +
+
+
+
+
+
+{% endblock %}