Rework login process & implement MFA
This commit is contained in:
parent
ad2467b72d
commit
cc4135d14b
7 changed files with 297 additions and 56 deletions
|
|
@ -12,61 +12,62 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoginForm(c *fiber.Ctx) error {
|
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)
|
sess, err := helpers.GetSessionStore(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userid := sess.Get("userid")
|
||||||
|
if userid != nil {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Forbidden")
|
||||||
|
}
|
||||||
|
|
||||||
db, err := helpers.GetDatabase()
|
db, err := helpers.GetDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
email := c.FormValue("email")
|
var loginError string
|
||||||
password := c.FormValue("password")
|
if c.Method() == "POST" {
|
||||||
|
email := c.FormValue("email")
|
||||||
|
password := c.FormValue("password")
|
||||||
|
|
||||||
var user models.User
|
var user models.User
|
||||||
result := db.First(
|
result := db.First(
|
||||||
&user,
|
&user,
|
||||||
"LOWER(email) = LOWER(?) AND (disabled_at IS NULL OR disabled_at <= ?)",
|
"LOWER(email) = LOWER(?) AND (disabled_at IS NULL OR disabled_at <= ?)",
|
||||||
email,
|
email,
|
||||||
time.Now(),
|
time.Now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
allowLogin := false
|
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
return err
|
||||||
return err
|
}
|
||||||
} else {
|
|
||||||
allowLogin = helpers.CheckPasswordHash(password, user.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !allowLogin {
|
if helpers.CheckPasswordHash(password, user.Password) {
|
||||||
return c.Render("login", fiber.Map{
|
sess.Set("userid", user.ID)
|
||||||
"PageTitle": "Connexion",
|
sess.Save()
|
||||||
"LoginError": "Email ou mot de passe incorrect",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sess.Set("userid", user.ID)
|
redirectId := c.Query("redirect")
|
||||||
sess.Save()
|
redirectUrl := "/"
|
||||||
|
|
||||||
redirectId := c.Query("redirect")
|
if len(redirectId) > 0 {
|
||||||
redirectUrl := "/"
|
redirectKey := fmt.Sprintf("redirect-%s", redirectId)
|
||||||
|
redirectVal := sess.Get(redirectKey)
|
||||||
|
|
||||||
if len(redirectId) > 0 {
|
if redirectVal != nil {
|
||||||
redirectKey := fmt.Sprintf("redirect-%s", redirectId)
|
redirectUrl = redirectVal.(string)
|
||||||
redirectVal := sess.Get(redirectKey)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if redirectVal != nil {
|
return c.Redirect(redirectUrl)
|
||||||
redirectUrl = redirectVal.(string)
|
} 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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
|
@ -44,6 +45,15 @@ func TotpEnrollPage(c *fiber.Ctx) error {
|
||||||
AccountName: user.Email,
|
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)
|
key, err := totp.Generate(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -65,6 +75,44 @@ func TotpEnrollPage(c *fiber.Ctx) error {
|
||||||
base64.StdEncoding.EncodeToString(buf.Bytes()),
|
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())
|
sess.Set("totp-enroll-secret", key.Secret())
|
||||||
err = sess.Save()
|
err = sess.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -72,8 +120,75 @@ func TotpEnrollPage(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Render("totp_enroll", fiber.Map{
|
return c.Render("totp_enroll", fiber.Map{
|
||||||
"PageTitle": "Enregistrement multifacteur",
|
"PageTitle": "Enregistrement multifacteur (TOTP)",
|
||||||
"QrCode": imgBase64,
|
"QrCode": imgBase64,
|
||||||
"Secret": key.Secret(),
|
"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
|
// Middlewares
|
||||||
app.Use(middlewares.AuthMiddleware)
|
app.Use(middlewares.AuthMiddleware)
|
||||||
app.Use("/login", middlewares.DenyAuthMiddleware)
|
|
||||||
app.Use(middlewares.WelcomeMiddleware)
|
app.Use(middlewares.WelcomeMiddleware)
|
||||||
|
app.Use(middlewares.MfaEnrollMiddleware)
|
||||||
|
app.Use(middlewares.MfaVerifyMiddleware)
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
app.Get("/", controllers.Homepage)
|
app.Get("/", controllers.Homepage)
|
||||||
app.Get("/login", controllers.LoginForm)
|
app.Get("/login", controllers.LoginForm)
|
||||||
app.Post("/login", controllers.LoginProcess)
|
app.Post("/login", controllers.LoginForm)
|
||||||
app.Get("/logout", controllers.LogoutProcess)
|
app.Get("/logout", controllers.LogoutProcess)
|
||||||
app.Get("/welcome", controllers.WelcomePage)
|
app.Get("/welcome", controllers.WelcomePage)
|
||||||
app.Post("/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(
|
listenAddr := fmt.Sprintf(
|
||||||
"%s:%d",
|
"%s:%d",
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,3 @@ func AuthMiddleware(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return c.Next()
|
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
|
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 id="login-card" class="my-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Vérification multifacteur (TOTP)
|
Enregistrement multifacteur (TOTP)
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if MfaError %}
|
{% 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