Rework login process & implement MFA

This commit is contained in:
William Bouzourène 2024-12-22 16:54:42 +01:00
parent ad2467b72d
commit cc4135d14b
7 changed files with 297 additions and 56 deletions

View file

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

View file

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

10
main.go
View file

@ -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",

View file

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

View file

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

View file

@ -5,7 +5,7 @@
<div id="login-card" class="my-5">
<div class="card">
<div class="card-header">
Vérification multifacteur (TOTP)
Enregistrement multifacteur (TOTP)
</div>
<div class="card-body">
{% if MfaError %}

43
views/totp_verify.html Normal file
View file

@ -0,0 +1,43 @@
{% extends "layouts/main.html" %}
{% block main %}
<div class="container">
<div id="login-card" class="my-5">
<div class="card">
<div class="card-header">
Vérification multifacteur (TOTP)
</div>
<div class="card-body">
{% if MfaError %}
<div class="alert alert-danger">
{{ MfaError }}
</div>
{% endif %}
<form id="login" method="post">
<div class="mb-3">
<label for="otp" class="form-label">
Code temporaire
</label>
<input
id="otp"
class="form-control"
type="text"
name="otp"
required
placeholder="000000"
pattern="[0-9]{6}"
>
</div>
<div class="text-end">
<button class="btn btn-primary" type="submit">
<i class="me-1" data-feather="check-circle"></i>
Vérifier
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}