Rework login process & implement MFA
This commit is contained in:
parent
d41581aa47
commit
08c8f78328
7 changed files with 297 additions and 56 deletions
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
10
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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
43
views/totp_verify.html
Normal 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue