Compare commits

..

1 Commits

Author SHA1 Message Date
Stavros e0a8cf5441 fix: set correct bin location in air 2025-11-16 19:42:14 +02:00
11 changed files with 28 additions and 261 deletions
+3 -2
View File
@@ -2,9 +2,10 @@ root = "/tinyauth"
tmp_dir = "tmp"
[build]
pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html", "cp /go/bin/dlv dlv"]
pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html"]
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ."
bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
bin = "tmp/tinyauth"
full_bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
include_ext = ["go"]
exclude_dir = ["internal/assets/dist"]
exclude_regex = [".*_test\\.go"]
-1
View File
@@ -71,7 +71,6 @@ func (c *rootCmd) Register() {
{"disable-analytics", false, "Disable anonymous version collection."},
{"disable-resources", false, "Disable the resources server."},
{"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 {
+2 -10
View File
@@ -48,9 +48,6 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
}
func (app *BootstrapApp) Setup() error {
// Log json
shouldLogJson := utils.ShouldLogJSON(os.Environ(), os.Args)
// Parse users
users, err := utils.GetUsers(app.config.Users, app.config.UsersFile)
@@ -145,10 +142,6 @@ func (app *BootstrapApp) Setup() error {
aclsService := service.NewAccessControlsService(dockerService)
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
accessLogService := service.NewAccessLogService(&service.AccessLogServiceConfig{
LogFile: app.config.AccessLogFile,
LogJson: shouldLogJson,
})
// Initialize services (order matters)
services := []Service{
@@ -156,7 +149,6 @@ func (app *BootstrapApp) Setup() error {
aclsService,
authService,
oauthBrokerService,
accessLogService,
}
for _, svc := range services {
@@ -252,7 +244,7 @@ func (app *BootstrapApp) Setup() error {
CSRFCookieName: csrfCookieName,
RedirectCookieName: redirectCookieName,
CookieDomain: cookieDomain,
}, apiRouter, authService, oauthBrokerService, accessLogService)
}, apiRouter, authService, oauthBrokerService)
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL,
@@ -260,7 +252,7 @@ func (app *BootstrapApp) Setup() error {
userController := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: cookieDomain,
}, apiRouter, authService, accessLogService)
}, apiRouter, authService)
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: app.config.ResourcesDir,
-1
View File
@@ -42,7 +42,6 @@ type Config struct {
DisableAnalytics bool `mapstructure:"disable-analytics"`
DisableResources bool `mapstructure:"disable-resources"`
SocketPath string `mapstructure:"socket-path"`
AccessLogFile string `mapstructure:"access-log-file"`
}
// OAuth/OIDC config
+8 -48
View File
@@ -31,16 +31,14 @@ 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, als *service.AccessLogService) *OAuthController {
func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController {
return &OAuthController{
config: config,
router: router,
auth: auth,
broker: broker,
als: als,
}
}
@@ -63,7 +61,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
svc, exists := controller.broker.GetService(req.Provider)
service, exists := controller.broker.GetService(req.Provider)
if !exists {
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
@@ -74,9 +72,9 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
svc.GenerateVerifier()
state := svc.GenerateState()
authURL := svc.GetAuthURL(state)
service.GenerateVerifier()
state := service.GenerateState()
authURL := service.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")
@@ -108,16 +106,8 @@ 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))
@@ -127,30 +117,16 @@ 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")
svc, exists := controller.broker.GetService(req.Provider)
service, 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 = svc.VerifyCode(code)
err = service.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
@@ -171,14 +147,6 @@ 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{
@@ -221,7 +189,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
Email: user.Email,
Provider: req.Provider,
OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: svc.GetName(),
OAuthName: service.GetName(),
}
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
@@ -234,14 +202,6 @@ 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) {
+7 -87
View File
@@ -29,15 +29,13 @@ type UserController struct {
config UserControllerConfig
router *gin.RouterGroup
auth *service.AuthService
als *service.AccessLogService
}
func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService, als *service.AccessLogService) *UserController {
func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController {
return &UserController{
config: config,
router: router,
auth: auth,
als: als,
}
}
@@ -74,13 +72,6 @@ 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,
@@ -92,13 +83,6 @@ 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{
@@ -109,13 +93,6 @@ 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{
@@ -125,18 +102,14 @@ 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{
@@ -185,18 +158,6 @@ 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",
@@ -206,28 +167,8 @@ 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",
@@ -247,7 +188,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
clientIP := c.ClientIP()
context, err := utils.GetContext(c)
if err != nil {
@@ -268,6 +208,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
clientIP := c.ClientIP()
rateIdentifier := context.Username
if rateIdentifier == "" {
@@ -279,13 +221,6 @@ 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,
@@ -299,13 +234,6 @@ 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{
@@ -315,14 +243,6 @@ 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)
+1 -9
View File
@@ -64,18 +64,10 @@ 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, als)
}, group, authService)
ctrl.SetupRoutes()
return router, recorder
-96
View File
@@ -1,96 +0,0 @@
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)
}
+1 -1
View File
@@ -201,7 +201,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st
return providers, nil
}
func ShouldLogJSON(environ []string, args []string) bool {
func ShoudLogJSON(environ []string, args []string) bool {
for _, e := range environ {
pair := strings.SplitN(e, "=", 2)
if len(pair) == 2 && pair[0] == "LOG_JSON" && strings.ToLower(pair[1]) == "true" {
+5 -5
View File
@@ -279,20 +279,20 @@ func TestGetOAuthProvidersConfig(t *testing.T) {
assert.DeepEqual(t, expected, result)
}
func TestShouldLogJSON(t *testing.T) {
func TestShoudLogJSON(t *testing.T) {
// Test with no env or args
result := utils.ShouldLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--foo-bar=baz"})
result := utils.ShoudLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--foo-bar=baz"})
assert.Equal(t, false, result)
// Test with env variable set
result = utils.ShouldLogJSON([]string{"LOG_JSON=true"}, []string{"tinyauth", "--foo-bar=baz"})
result = utils.ShoudLogJSON([]string{"LOG_JSON=true"}, []string{"tinyauth", "--foo-bar=baz"})
assert.Equal(t, true, result)
// Test with flag set
result = utils.ShouldLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--log-json=true"})
result = utils.ShoudLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--log-json=true"})
assert.Equal(t, true, result)
// Test with both env and flag set to false
result = utils.ShouldLogJSON([]string{"LOG_JSON=false"}, []string{"tinyauth", "--log-json=false"})
result = utils.ShoudLogJSON([]string{"LOG_JSON=false"}, []string{"tinyauth", "--log-json=false"})
assert.Equal(t, false, result)
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func main() {
log.Logger = log.Logger.With().Caller().Logger()
if !utils.ShouldLogJSON(os.Environ(), os.Args) {
if !utils.ShoudLogJSON(os.Environ(), os.Args) {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
}
cmd.Run()