mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-12-08 17:26:38 +00:00
Compare commits
4 Commits
v4.1.0-rc.
...
feat/acces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259069193f | ||
|
|
8894064e10 | ||
|
|
a2112e2ce5 | ||
|
|
64d000070f |
@@ -71,6 +71,7 @@ func (c *rootCmd) Register() {
|
|||||||
{"disable-analytics", false, "Disable anonymous version collection."},
|
{"disable-analytics", false, "Disable anonymous version collection."},
|
||||||
{"disable-resources", false, "Disable the resources server."},
|
{"disable-resources", false, "Disable the resources server."},
|
||||||
{"socket-path", "", "Path to the Unix socket to bind the server to."},
|
{"socket-path", "", "Path to the Unix socket to bind the server to."},
|
||||||
|
{"access-log-file", "", "Path to the access log file."},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range configOptions {
|
for _, opt := range configOptions {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) Setup() error {
|
func (app *BootstrapApp) Setup() error {
|
||||||
|
// Log json
|
||||||
|
shouldLogJson := utils.ShouldLogJSON(os.Environ(), os.Args)
|
||||||
|
|
||||||
// Parse users
|
// Parse users
|
||||||
users, err := utils.GetUsers(app.config.Users, app.config.UsersFile)
|
users, err := utils.GetUsers(app.config.Users, app.config.UsersFile)
|
||||||
|
|
||||||
@@ -142,6 +145,10 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
aclsService := service.NewAccessControlsService(dockerService)
|
aclsService := service.NewAccessControlsService(dockerService)
|
||||||
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
|
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
|
||||||
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
|
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
|
||||||
|
accessLogService := service.NewAccessLogService(&service.AccessLogServiceConfig{
|
||||||
|
LogFile: app.config.AccessLogFile,
|
||||||
|
LogJson: shouldLogJson,
|
||||||
|
})
|
||||||
|
|
||||||
// Initialize services (order matters)
|
// Initialize services (order matters)
|
||||||
services := []Service{
|
services := []Service{
|
||||||
@@ -149,6 +156,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
aclsService,
|
aclsService,
|
||||||
authService,
|
authService,
|
||||||
oauthBrokerService,
|
oauthBrokerService,
|
||||||
|
accessLogService,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
@@ -244,7 +252,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
CSRFCookieName: csrfCookieName,
|
CSRFCookieName: csrfCookieName,
|
||||||
RedirectCookieName: redirectCookieName,
|
RedirectCookieName: redirectCookieName,
|
||||||
CookieDomain: cookieDomain,
|
CookieDomain: cookieDomain,
|
||||||
}, apiRouter, authService, oauthBrokerService)
|
}, apiRouter, authService, oauthBrokerService, accessLogService)
|
||||||
|
|
||||||
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||||
AppURL: app.config.AppURL,
|
AppURL: app.config.AppURL,
|
||||||
@@ -252,7 +260,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
userController := controller.NewUserController(controller.UserControllerConfig{
|
userController := controller.NewUserController(controller.UserControllerConfig{
|
||||||
CookieDomain: cookieDomain,
|
CookieDomain: cookieDomain,
|
||||||
}, apiRouter, authService)
|
}, apiRouter, authService, accessLogService)
|
||||||
|
|
||||||
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
|
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
|
||||||
ResourcesDir: app.config.ResourcesDir,
|
ResourcesDir: app.config.ResourcesDir,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type Config struct {
|
|||||||
DisableAnalytics bool `mapstructure:"disable-analytics"`
|
DisableAnalytics bool `mapstructure:"disable-analytics"`
|
||||||
DisableResources bool `mapstructure:"disable-resources"`
|
DisableResources bool `mapstructure:"disable-resources"`
|
||||||
SocketPath string `mapstructure:"socket-path"`
|
SocketPath string `mapstructure:"socket-path"`
|
||||||
|
AccessLogFile string `mapstructure:"access-log-file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth/OIDC config
|
// OAuth/OIDC config
|
||||||
|
|||||||
@@ -31,14 +31,16 @@ type OAuthController struct {
|
|||||||
router *gin.RouterGroup
|
router *gin.RouterGroup
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
broker *service.OAuthBrokerService
|
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{
|
return &OAuthController{
|
||||||
config: config,
|
config: config,
|
||||||
router: router,
|
router: router,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
broker: broker,
|
broker: broker,
|
||||||
|
als: als,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service, exists := controller.broker.GetService(req.Provider)
|
svc, exists := controller.broker.GetService(req.Provider)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||||
@@ -72,9 +74,9 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service.GenerateVerifier()
|
svc.GenerateVerifier()
|
||||||
state := service.GenerateState()
|
state := svc.GenerateState()
|
||||||
authURL := service.GetAuthURL(state)
|
authURL := svc.GetAuthURL(state)
|
||||||
c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
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")
|
redirectURI := c.Query("redirect_uri")
|
||||||
@@ -106,8 +108,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
state := c.Query("state")
|
state := c.Query("state")
|
||||||
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
if err != nil || state != csrfCookie {
|
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")
|
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.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))
|
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)
|
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
service, exists := controller.broker.GetService(req.Provider)
|
svc, exists := controller.broker.GetService(req.Provider)
|
||||||
|
|
||||||
if !exists {
|
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)
|
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.VerifyCode(code)
|
err = svc.VerifyCode(code)
|
||||||
if err != nil {
|
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")
|
log.Error().Err(err).Msg("Failed to verify OAuth code")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
@@ -147,6 +171,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
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")
|
log.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||||
|
|
||||||
queries, err := query.Values(config.UnauthorizedQuery{
|
queries, err := query.Values(config.UnauthorizedQuery{
|
||||||
@@ -189,7 +221,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Provider: req.Provider,
|
Provider: req.Provider,
|
||||||
OAuthGroups: utils.CoalesceToString(user.Groups),
|
OAuthGroups: utils.CoalesceToString(user.Groups),
|
||||||
OAuthName: service.GetName(),
|
OAuthName: svc.GetName(),
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
@@ -202,6 +234,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
||||||
|
|
||||||
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ type UserController struct {
|
|||||||
config UserControllerConfig
|
config UserControllerConfig
|
||||||
router *gin.RouterGroup
|
router *gin.RouterGroup
|
||||||
auth *service.AuthService
|
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{
|
return &UserController{
|
||||||
config: config,
|
config: config,
|
||||||
router: router,
|
router: router,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
als: als,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
||||||
|
|
||||||
if isLocked {
|
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")
|
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{
|
c.JSON(429, gin.H{
|
||||||
"status": 429,
|
"status": 429,
|
||||||
@@ -83,6 +92,13 @@ 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" {
|
||||||
|
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")
|
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
@@ -93,6 +109,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
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")
|
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
@@ -102,14 +125,18 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful")
|
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
|
||||||
|
|
||||||
if userSearch.Type == "local" {
|
if userSearch.Type == "local" {
|
||||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||||
|
|
||||||
if user.TotpSecret != "" {
|
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")
|
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||||
|
|
||||||
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{
|
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{
|
||||||
@@ -158,6 +185,18 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
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{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
@@ -167,8 +206,28 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
func (controller *UserController) logoutHandler(c *gin.Context) {
|
func (controller *UserController) logoutHandler(c *gin.Context) {
|
||||||
log.Debug().Msg("Logout request received")
|
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.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{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Logout successful",
|
"message": "Logout successful",
|
||||||
@@ -188,6 +247,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientIP := c.ClientIP()
|
||||||
context, err := utils.GetContext(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -208,8 +268,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
|
||||||
|
|
||||||
rateIdentifier := context.Username
|
rateIdentifier := context.Username
|
||||||
|
|
||||||
if rateIdentifier == "" {
|
if rateIdentifier == "" {
|
||||||
@@ -221,6 +279,13 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
||||||
|
|
||||||
if isLocked {
|
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")
|
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{
|
c.JSON(429, gin.H{
|
||||||
"status": 429,
|
"status": 429,
|
||||||
@@ -234,6 +299,13 @@ 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 {
|
||||||
|
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")
|
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
@@ -243,6 +315,14 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
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")
|
log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
||||||
|
|||||||
@@ -64,10 +64,18 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
|
|||||||
SessionCookieName: "tinyauth-session",
|
SessionCookieName: "tinyauth-session",
|
||||||
}, nil, nil, database)
|
}, nil, nil, database)
|
||||||
|
|
||||||
|
// Access log service
|
||||||
|
als := service.NewAccessLogService(&service.AccessLogServiceConfig{
|
||||||
|
LogFile: "",
|
||||||
|
LogJson: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, als.Init())
|
||||||
|
|
||||||
// Controller
|
// Controller
|
||||||
ctrl := controller.NewUserController(controller.UserControllerConfig{
|
ctrl := controller.NewUserController(controller.UserControllerConfig{
|
||||||
CookieDomain: "localhost",
|
CookieDomain: "localhost",
|
||||||
}, group, authService)
|
}, group, authService, als)
|
||||||
ctrl.SetupRoutes()
|
ctrl.SetupRoutes()
|
||||||
|
|
||||||
return router, recorder
|
return router, recorder
|
||||||
|
|||||||
96
internal/service/access_log_service.go
Normal file
96
internal/service/access_log_service.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccessLog struct {
|
||||||
|
Provider string
|
||||||
|
Username string
|
||||||
|
ClientIP string
|
||||||
|
Success bool
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessLogServiceConfig struct {
|
||||||
|
LogFile string
|
||||||
|
LogJson bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessLogService struct {
|
||||||
|
config *AccessLogServiceConfig
|
||||||
|
logger zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessLogService(config *AccessLogServiceConfig) *AccessLogService {
|
||||||
|
return &AccessLogService{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (als *AccessLogService) Init() error {
|
||||||
|
writers := make([]io.Writer, 0)
|
||||||
|
|
||||||
|
if als.config.LogFile != "" {
|
||||||
|
// We are not closing the file here since we will keep writing to it until interrupted
|
||||||
|
file, err := os.OpenFile(als.config.LogFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0640)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
writter := zerolog.ConsoleWriter(zerolog.ConsoleWriter{Out: file, TimeFormat: time.RFC3339, NoColor: true, PartsOrder: []string{
|
||||||
|
"time", "level", "caller", "message",
|
||||||
|
}})
|
||||||
|
writter.FormatLevel = func(i any) string {
|
||||||
|
return strings.ToUpper(fmt.Sprintf("[ %s ]", i))
|
||||||
|
}
|
||||||
|
writter.FormatCaller = func(i any) string {
|
||||||
|
return fmt.Sprintf("%s:", i)
|
||||||
|
}
|
||||||
|
writter.FormatMessage = func(i any) string {
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
}
|
||||||
|
writter.FormatFieldName = func(i any) string {
|
||||||
|
return fmt.Sprintf("%s=", i)
|
||||||
|
}
|
||||||
|
writter.FormatFieldValue = func(i any) string {
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
}
|
||||||
|
writers = append(writers, writter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !als.config.LogJson {
|
||||||
|
writter := zerolog.ConsoleWriter(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
|
||||||
|
writers = append(writers, writter)
|
||||||
|
} else {
|
||||||
|
writers = append(writers, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
als.logger = zerolog.New(zerolog.MultiLevelWriter(writers...)).With().Caller().Logger()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (als *AccessLogService) Log(log AccessLog) {
|
||||||
|
var event *zerolog.Event
|
||||||
|
|
||||||
|
if log.Success {
|
||||||
|
event = als.logger.Info()
|
||||||
|
} else {
|
||||||
|
event = als.logger.Warn()
|
||||||
|
}
|
||||||
|
|
||||||
|
event = event.
|
||||||
|
Str("provider", log.Provider).
|
||||||
|
Str("username", log.Username).
|
||||||
|
Str("client_ip", log.ClientIP).
|
||||||
|
Int64("time", time.Now().Unix()).
|
||||||
|
Bool("success", log.Success)
|
||||||
|
|
||||||
|
event.Msg(log.Message)
|
||||||
|
}
|
||||||
@@ -201,7 +201,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st
|
|||||||
return providers, nil
|
return providers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShoudLogJSON(environ []string, args []string) bool {
|
func ShouldLogJSON(environ []string, args []string) bool {
|
||||||
for _, e := range environ {
|
for _, e := range environ {
|
||||||
pair := strings.SplitN(e, "=", 2)
|
pair := strings.SplitN(e, "=", 2)
|
||||||
if len(pair) == 2 && pair[0] == "LOG_JSON" && strings.ToLower(pair[1]) == "true" {
|
if len(pair) == 2 && pair[0] == "LOG_JSON" && strings.ToLower(pair[1]) == "true" {
|
||||||
|
|||||||
@@ -279,20 +279,20 @@ func TestGetOAuthProvidersConfig(t *testing.T) {
|
|||||||
assert.DeepEqual(t, expected, result)
|
assert.DeepEqual(t, expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShoudLogJSON(t *testing.T) {
|
func TestShouldLogJSON(t *testing.T) {
|
||||||
// Test with no env or args
|
// Test with no env or args
|
||||||
result := utils.ShoudLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--foo-bar=baz"})
|
result := utils.ShouldLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--foo-bar=baz"})
|
||||||
assert.Equal(t, false, result)
|
assert.Equal(t, false, result)
|
||||||
|
|
||||||
// Test with env variable set
|
// Test with env variable set
|
||||||
result = utils.ShoudLogJSON([]string{"LOG_JSON=true"}, []string{"tinyauth", "--foo-bar=baz"})
|
result = utils.ShouldLogJSON([]string{"LOG_JSON=true"}, []string{"tinyauth", "--foo-bar=baz"})
|
||||||
assert.Equal(t, true, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Test with flag set
|
// Test with flag set
|
||||||
result = utils.ShoudLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--log-json=true"})
|
result = utils.ShouldLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--log-json=true"})
|
||||||
assert.Equal(t, true, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Test with both env and flag set to false
|
// Test with both env and flag set to false
|
||||||
result = utils.ShoudLogJSON([]string{"LOG_JSON=false"}, []string{"tinyauth", "--log-json=false"})
|
result = utils.ShouldLogJSON([]string{"LOG_JSON=false"}, []string{"tinyauth", "--log-json=false"})
|
||||||
assert.Equal(t, false, result)
|
assert.Equal(t, false, result)
|
||||||
}
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Logger = log.Logger.With().Caller().Logger()
|
log.Logger = log.Logger.With().Caller().Logger()
|
||||||
if !utils.ShoudLogJSON(os.Environ(), os.Args) {
|
if !utils.ShouldLogJSON(os.Environ(), os.Args) {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
||||||
}
|
}
|
||||||
cmd.Run()
|
cmd.Run()
|
||||||
|
|||||||
Reference in New Issue
Block a user