mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-02 21:42:29 +00:00
Compare commits
2 Commits
fix/rate-l
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecbe7985d1 | ||
|
|
f1e2b55cd1 |
@@ -25,13 +25,13 @@
|
|||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-hook-form": "^7.69.0",
|
"react-hook-form": "^7.69.0",
|
||||||
"react-i18next": "^16.5.0",
|
"react-i18next": "^16.5.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.11.0",
|
"react-router": "^7.11.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.3.2",
|
"zod": "^4.3.4",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
@@ -797,7 +797,7 @@
|
|||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.69.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw=="],
|
"react-hook-form": ["react-hook-form@7.69.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw=="],
|
||||||
|
|
||||||
"react-i18next": ["react-i18next@16.5.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw=="],
|
"react-i18next": ["react-i18next@16.5.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw=="],
|
||||||
|
|
||||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||||
|
|
||||||
@@ -915,7 +915,7 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.2", "", {}, "sha512-b8L8yn4rIVfiXyHAmnr52/ZEpDumlT0bmxiq3Ws1ybrinhflGpt12Hvv54kYnEsGPRs6o/Ka3/ppA2OWY21IVg=="],
|
"zod": ["zod@4.3.4", "", {}, "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A=="],
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-hook-form": "^7.69.0",
|
"react-hook-form": "^7.69.0",
|
||||||
"react-i18next": "^16.5.0",
|
"react-i18next": "^16.5.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.11.0",
|
"react-router": "^7.11.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.3.2"
|
"zod": "^4.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
@@ -60,23 +61,17 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
log.Debug().Str("username", req.Username).Msg("Login attempt")
|
||||||
|
|
||||||
rateIdentifier := req.Username
|
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
|
||||||
|
|
||||||
if rateIdentifier == "" {
|
|
||||||
rateIdentifier = clientIP
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt")
|
|
||||||
|
|
||||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
|
||||||
|
|
||||||
if isLocked {
|
if isLocked {
|
||||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts")
|
log.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login 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{
|
c.JSON(429, gin.H{
|
||||||
"status": 429,
|
"status": 429,
|
||||||
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
|
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,8 +79,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
userSearch := controller.auth.SearchUser(req.Username)
|
userSearch := controller.auth.SearchUser(req.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" {
|
if userSearch.Type == "unknown" {
|
||||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found")
|
log.Warn().Str("username", req.Username).Msg("User not found")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -94,8 +89,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
||||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password")
|
log.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -103,9 +98,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful")
|
log.Info().Str("username", req.Username).Msg("Login successful")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
if userSearch.Type == "local" {
|
if userSearch.Type == "local" {
|
||||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||||
@@ -209,23 +204,17 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
log.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
||||||
|
|
||||||
rateIdentifier := context.Username
|
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
||||||
|
|
||||||
if rateIdentifier == "" {
|
|
||||||
rateIdentifier = clientIP
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt")
|
|
||||||
|
|
||||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
|
||||||
|
|
||||||
if isLocked {
|
if isLocked {
|
||||||
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts")
|
log.Warn().Str("username", context.Username).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{
|
c.JSON(429, gin.H{
|
||||||
"status": 429,
|
"status": 429,
|
||||||
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remainingTime),
|
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -235,8 +224,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
ok := totp.Validate(req.Code, user.TotpSecret)
|
ok := totp.Validate(req.Code, user.TotpSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code")
|
log.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(context.Username, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -244,9 +233,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful")
|
log.Info().Str("username", context.Username).Msg("TOTP verification successful")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := config.SessionCookie{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
@@ -116,20 +117,34 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
log.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
||||||
|
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.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userSearch := m.auth.SearchUser(basic.Username)
|
userSearch := m.auth.SearchUser(basic.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
log.Debug().Msg("User from basic auth not found")
|
log.Debug().Msg("User from basic auth not found")
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
log.Debug().Msg("Invalid password for basic auth user")
|
log.Debug().Msg("Invalid password for basic auth user")
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, true)
|
||||||
|
|
||||||
switch userSearch.Type {
|
switch userSearch.Type {
|
||||||
case "local":
|
case "local":
|
||||||
log.Debug().Msg("Basic auth user is local")
|
log.Debug().Msg("Basic auth user is local")
|
||||||
|
|||||||
Reference in New Issue
Block a user