This commit is contained in:
Stavros
2025-11-15 11:38:57 +02:00
parent d4d4cb3634
commit 64d000070f
11 changed files with 265 additions and 29 deletions

View File

@@ -31,14 +31,16 @@ type OAuthController struct {
router *gin.RouterGroup
auth *service.AuthService
broker *service.OAuthBrokerService
als *service.AccessLogService
}
func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController {
func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService, als *service.AccessLogService) *OAuthController {
return &OAuthController{
config: config,
router: router,
auth: auth,
broker: broker,
als: als,
}
}
@@ -61,7 +63,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
service, exists := controller.broker.GetService(req.Provider)
svc, exists := controller.broker.GetService(req.Provider)
if !exists {
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
@@ -72,9 +74,9 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
service.GenerateVerifier()
state := service.GenerateState()
authURL := service.GetAuthURL(state)
svc.GenerateVerifier()
state := svc.GenerateState()
authURL := svc.GetAuthURL(state)
c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
redirectURI := c.Query("redirect_uri")
@@ -106,8 +108,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
state := c.Query("state")
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
clientIP := c.ClientIP()
if err != nil || state != csrfCookie {
controller.als.Log(service.AccessLog{
Provider: req.Provider,
Username: "",
ClientIP: clientIP,
Success: false,
Message: "CSRF token mismatch or cookie missing",
})
log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
@@ -117,16 +127,30 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
code := c.Query("code")
service, exists := controller.broker.GetService(req.Provider)
svc, exists := controller.broker.GetService(req.Provider)
if !exists {
controller.als.Log(service.AccessLog{
Provider: req.Provider,
Username: "",
ClientIP: clientIP,
Success: false,
Message: "OAuth provider not found",
})
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
err = service.VerifyCode(code)
err = svc.VerifyCode(code)
if err != nil {
controller.als.Log(service.AccessLog{
Provider: req.Provider,
Username: "",
ClientIP: clientIP,
Success: false,
Message: "Failed to verify OAuth code",
})
log.Error().Err(err).Msg("Failed to verify OAuth code")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
@@ -147,6 +171,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
}
if !controller.auth.IsEmailWhitelisted(user.Email) {
controller.als.Log(service.AccessLog{
Provider: req.Provider,
Username: user.Email,
ClientIP: clientIP,
Success: false,
Message: "Email not whitelisted",
})
log.Warn().Str("email", user.Email).Msg("Email not whitelisted")
queries, err := query.Values(config.UnauthorizedQuery{
@@ -189,7 +221,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
Email: user.Email,
Provider: req.Provider,
OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: service.GetName(),
OAuthName: svc.GetName(),
}
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
@@ -202,6 +234,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
return
}
controller.als.Log(service.AccessLog{
Provider: req.Provider,
Username: user.Email,
ClientIP: clientIP,
Success: true,
Message: "OAuth login successful",
})
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {

View File

@@ -29,13 +29,15 @@ type UserController struct {
config UserControllerConfig
router *gin.RouterGroup
auth *service.AuthService
als *service.AccessLogService
}
func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController {
func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService, als *service.AccessLogService) *UserController {
return &UserController{
config: config,
router: router,
auth: auth,
als: als,
}
}
@@ -72,6 +74,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
if isLocked {
controller.als.Log(service.AccessLog{
Provider: "username",
Username: req.Username,
ClientIP: clientIP,
Success: false,
Message: "Account is locked due to too many failed login attempts",
})
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts")
c.JSON(429, gin.H{
"status": 429,
@@ -83,6 +92,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
userSearch := controller.auth.SearchUser(req.Username)
if userSearch.Type == "unknown" {
controller.als.Log(service.AccessLog{
Provider: "username",
Username: req.Username,
ClientIP: clientIP,
Success: false,
Message: "User not found",
})
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found")
controller.auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{
@@ -93,6 +109,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
}
if !controller.auth.VerifyUser(userSearch, req.Password) {
controller.als.Log(service.AccessLog{
Provider: "username",
Username: req.Username,
ClientIP: clientIP,
Success: false,
Message: "Invalid password",
})
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password")
controller.auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{
@@ -102,14 +125,18 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return
}
log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful")
controller.auth.RecordLoginAttempt(rateIdentifier, true)
if userSearch.Type == "local" {
user := controller.auth.GetLocalUser(userSearch.Username)
if user.TotpSecret != "" {
controller.als.Log(service.AccessLog{
Provider: "username",
Username: req.Username,
ClientIP: clientIP,
Success: true,
Message: "User has TOTP enabled, requiring TOTP verification",
})
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{
@@ -158,6 +185,18 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return
}
controller.als.Log(service.AccessLog{
Provider: "username",
Username: req.Username,
ClientIP: clientIP,
Success: true,
Message: "Login successful",
})
log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful")
controller.auth.RecordLoginAttempt(rateIdentifier, true)
c.JSON(200, gin.H{
"status": 200,
"message": "Login successful",
@@ -167,8 +206,28 @@ func (controller *UserController) loginHandler(c *gin.Context) {
func (controller *UserController) logoutHandler(c *gin.Context) {
log.Debug().Msg("Logout request received")
context, err := utils.GetContext(c)
if err != nil {
log.Debug().Msg("Not logged in, nothing to do")
c.JSON(200, gin.H{
"status": 200,
"message": "Not logged in",
})
return
}
clientIP := c.ClientIP()
controller.auth.DeleteSessionCookie(c)
controller.als.Log(service.AccessLog{
Provider: "username",
Username: context.Username,
ClientIP: clientIP,
Success: true,
Message: "Logout successful",
})
c.JSON(200, gin.H{
"status": 200,
"message": "Logout successful",
@@ -188,6 +247,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
clientIP := c.ClientIP()
context, err := utils.GetContext(c)
if err != nil {
@@ -208,8 +268,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
clientIP := c.ClientIP()
rateIdentifier := context.Username
if rateIdentifier == "" {
@@ -221,6 +279,13 @@ func (controller *UserController) totpHandler(c *gin.Context) {
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
if isLocked {
controller.als.Log(service.AccessLog{
Provider: "username",
Username: context.Username,
ClientIP: clientIP,
Success: false,
Message: "Account is locked due to too many failed TOTP attempts",
})
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts")
c.JSON(429, gin.H{
"status": 429,
@@ -234,6 +299,13 @@ func (controller *UserController) totpHandler(c *gin.Context) {
ok := totp.Validate(req.Code, user.TotpSecret)
if !ok {
controller.als.Log(service.AccessLog{
Provider: "username",
Username: context.Username,
ClientIP: clientIP,
Success: false,
Message: "Invalid TOTP code",
})
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code")
controller.auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{
@@ -243,6 +315,14 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
controller.als.Log(service.AccessLog{
Provider: "username",
Username: context.Username,
ClientIP: clientIP,
Success: true,
Message: "TOTP verification successful",
})
log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful")
controller.auth.RecordLoginAttempt(rateIdentifier, true)

View File

@@ -64,10 +64,18 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
SessionCookieName: "tinyauth-session",
}, nil, nil, database)
// Access log service
als := service.NewAccessLogService(&service.AccessLogServiceConfig{
LogFile: "",
LogJson: true,
})
assert.NilError(t, als.Init())
// Controller
ctrl := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: "localhost",
}, group, authService)
}, group, authService, als)
ctrl.SetupRoutes()
return router, recorder