mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-30 17:38:11 +00:00
363 lines
9.2 KiB
Go
363 lines
9.2 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/pquerna/otp/totp"
|
|
)
|
|
|
|
type LoginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type TotpRequest struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type UserControllerConfig struct {
|
|
CookieDomain string
|
|
SessionCookieName string
|
|
}
|
|
|
|
type UserController struct {
|
|
config UserControllerConfig
|
|
router *gin.RouterGroup
|
|
auth *service.AuthService
|
|
}
|
|
|
|
func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController {
|
|
return &UserController{
|
|
config: config,
|
|
router: router,
|
|
auth: auth,
|
|
}
|
|
}
|
|
|
|
func (controller *UserController) SetupRoutes() {
|
|
userGroup := controller.router.Group("/user")
|
|
userGroup.POST("/login", controller.loginHandler)
|
|
userGroup.POST("/logout", controller.logoutHandler)
|
|
userGroup.POST("/totp", controller.totpHandler)
|
|
}
|
|
|
|
func (controller *UserController) loginHandler(c *gin.Context) {
|
|
var req LoginRequest
|
|
|
|
err := c.ShouldBindJSON(&req)
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
|
|
c.JSON(400, gin.H{
|
|
"status": 400,
|
|
"message": "Bad Request",
|
|
})
|
|
return
|
|
}
|
|
|
|
tlog.App.Debug().Str("username", req.Username).Msg("Login attempt")
|
|
|
|
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
|
|
|
|
if isLocked {
|
|
tlog.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
|
|
tlog.AuditLoginFailure(c, req.Username, "username", "account locked")
|
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
|
c.JSON(429, gin.H{
|
|
"status": 429,
|
|
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
|
|
})
|
|
return
|
|
}
|
|
|
|
search, err := controller.auth.SearchUser(req.Username)
|
|
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrUserNotFound) {
|
|
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
|
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
|
c.JSON(401, gin.H{
|
|
"status": 401,
|
|
"message": "Unauthorized",
|
|
})
|
|
return
|
|
}
|
|
tlog.App.Error().Err(err).Str("username", req.Username).Msg("Error searching for user")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
|
|
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
|
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
|
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
|
c.JSON(401, gin.H{
|
|
"status": 401,
|
|
"message": "Unauthorized",
|
|
})
|
|
return
|
|
}
|
|
|
|
tlog.App.Info().Str("username", req.Username).Msg("Login successful")
|
|
tlog.AuditLoginSuccess(c, req.Username, "username")
|
|
|
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
|
|
|
var localUser *model.LocalUser
|
|
|
|
if search.Type == model.UserLocal {
|
|
localUser = controller.auth.GetLocalUser(req.Username)
|
|
|
|
if localUser.TOTPSecret != "" {
|
|
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
|
|
|
name := localUser.Attributes.Name
|
|
if name == "" {
|
|
name = utils.Capitalize(localUser.Username)
|
|
}
|
|
|
|
email := localUser.Attributes.Email
|
|
if email == "" {
|
|
email = utils.CompileUserEmail(localUser.Username, controller.config.CookieDomain)
|
|
}
|
|
|
|
cookie, err := controller.auth.CreateSession(c, repository.Session{
|
|
Username: localUser.Username,
|
|
Name: name,
|
|
Email: email,
|
|
Provider: "local",
|
|
TotpPending: true,
|
|
})
|
|
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
http.SetCookie(c.Writer, cookie)
|
|
|
|
c.JSON(200, gin.H{
|
|
"status": 200,
|
|
"message": "TOTP required",
|
|
"totpPending": true,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
sessionCookie := repository.Session{
|
|
Username: req.Username,
|
|
Name: utils.Capitalize(req.Username),
|
|
Email: utils.CompileUserEmail(req.Username, controller.config.CookieDomain),
|
|
Provider: "local",
|
|
}
|
|
|
|
if search.Type == model.UserLocal {
|
|
if localUser.Attributes.Name != "" {
|
|
sessionCookie.Name = localUser.Attributes.Name
|
|
}
|
|
if localUser.Attributes.Email != "" {
|
|
sessionCookie.Email = localUser.Attributes.Email
|
|
}
|
|
}
|
|
|
|
if search.Type == model.UserLDAP {
|
|
sessionCookie.Provider = "ldap"
|
|
}
|
|
|
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
|
|
|
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
|
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
http.SetCookie(c.Writer, cookie)
|
|
|
|
c.JSON(200, gin.H{
|
|
"status": 200,
|
|
"message": "Login successful",
|
|
})
|
|
}
|
|
|
|
func (controller *UserController) logoutHandler(c *gin.Context) {
|
|
tlog.App.Debug().Msg("Logout request received")
|
|
|
|
uuid, err := c.Cookie(controller.config.SessionCookieName)
|
|
|
|
if err != nil {
|
|
if errors.Is(err, http.ErrNoCookie) {
|
|
tlog.App.Warn().Msg("No session cookie found on logout request")
|
|
c.JSON(200, gin.H{
|
|
"status": 200,
|
|
"message": "Logout successful",
|
|
})
|
|
return
|
|
}
|
|
tlog.App.Error().Err(err).Msg("Error retrieving session cookie on logout")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
context, err := new(model.UserContext).NewFromGin(c)
|
|
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to get user context on logout")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
cookie, err := controller.auth.DeleteSession(c, uuid)
|
|
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Error deleting session on logout")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
|
|
|
|
http.SetCookie(c.Writer, cookie)
|
|
|
|
c.JSON(200, gin.H{
|
|
"status": 200,
|
|
"message": "Logout successful",
|
|
})
|
|
}
|
|
|
|
func (controller *UserController) totpHandler(c *gin.Context) {
|
|
var req TotpRequest
|
|
|
|
err := c.ShouldBindJSON(&req)
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
|
|
c.JSON(400, gin.H{
|
|
"status": 400,
|
|
"message": "Bad Request",
|
|
})
|
|
return
|
|
}
|
|
|
|
context, err := new(model.UserContext).NewFromGin(c)
|
|
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
if !context.TOTPPending() {
|
|
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
|
c.JSON(401, gin.H{
|
|
"status": 401,
|
|
"message": "Unauthorized",
|
|
})
|
|
return
|
|
}
|
|
|
|
tlog.App.Debug().Str("username", context.GetUsername()).Msg("TOTP verification attempt")
|
|
|
|
isLocked, remaining := controller.auth.IsAccountLocked(context.GetUsername())
|
|
|
|
if isLocked {
|
|
tlog.App.Warn().Str("username", context.GetUsername()).Msg("Account is locked due to too many failed TOTP attempts")
|
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
|
c.JSON(429, gin.H{
|
|
"status": 429,
|
|
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining),
|
|
})
|
|
return
|
|
}
|
|
|
|
user := controller.auth.GetLocalUser(context.GetUsername())
|
|
|
|
ok := totp.Validate(req.Code, user.TOTPSecret)
|
|
|
|
if !ok {
|
|
tlog.App.Warn().Str("username", context.GetUsername()).Msg("Invalid TOTP code")
|
|
controller.auth.RecordLoginAttempt(context.GetUsername(), false)
|
|
tlog.AuditLoginFailure(c, context.GetUsername(), "totp", "invalid totp code")
|
|
c.JSON(401, gin.H{
|
|
"status": 401,
|
|
"message": "Unauthorized",
|
|
})
|
|
return
|
|
}
|
|
|
|
tlog.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful")
|
|
tlog.AuditLoginSuccess(c, context.GetUsername(), "totp")
|
|
|
|
controller.auth.RecordLoginAttempt(context.GetUsername(), true)
|
|
|
|
sessionCookie := repository.Session{
|
|
Username: user.Username,
|
|
Name: utils.Capitalize(user.Username),
|
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
|
Provider: "local",
|
|
}
|
|
|
|
if user.Attributes.Name != "" {
|
|
sessionCookie.Name = user.Attributes.Name
|
|
}
|
|
if user.Attributes.Email != "" {
|
|
sessionCookie.Email = user.Attributes.Email
|
|
}
|
|
|
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
|
|
|
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
|
|
|
if err != nil {
|
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
|
c.JSON(500, gin.H{
|
|
"status": 500,
|
|
"message": "Internal Server Error",
|
|
})
|
|
return
|
|
}
|
|
|
|
http.SetCookie(c.Writer, cookie)
|
|
|
|
c.JSON(200, gin.H{
|
|
"status": 200,
|
|
"message": "Login successful",
|
|
})
|
|
}
|