Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
TINYAUTH_AUTH_LOGINTIMEOUT=300
# Maximum login retries.
TINYAUTH_AUTH_LOGINMAXRETRIES=3
# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically.
TINYAUTH_AUTH_LOCKDOWNENABLED=true
# Comma-separated list of trusted proxy addresses.
TINYAUTH_AUTH_TRUSTEDPROXIES=
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,3 @@ for their support for Tinyauth.
- **Freepik** for providing the police hat and badge.
- **Renee French** for the original gopher logo.
- **Syrhu** for providing the background image of the app.

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=tinyauthapp/tinyauth&type=Date)](https://www.star-history.com/#tinyauthapp/tinyauth&Date)
Binary file modified assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions internal/bootstrap/router_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ func (app *BootstrapApp) setupRouter() error {
if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err)
}

app.runtime.TrustedProxiesConfigured = true
} else {
err := engine.SetTrustedProxies(nil)

if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err)
}

app.log.App.Warn().Msg("Trusted proxies are not configured, IP access controls will NOT work")
}

middlewareProvideFor := []any{
Expand Down
7 changes: 4 additions & 3 deletions internal/controller/proxy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
clientIP := c.ClientIP()

aclsCtx := &service.ACLContext{
ACLs: acls,
IP: net.ParseIP(clientIP),
Path: proxyCtx.Path,
ACLs: acls,
IP: net.ParseIP(clientIP),
Path: proxyCtx.Path,
TrustedProxiesConfigured: controller.runtime.TrustedProxiesConfigured,
}

if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
Expand Down
41 changes: 16 additions & 25 deletions internal/controller/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,12 @@ func (controller *UserController) loginHandler(c *gin.Context) {

controller.log.App.Debug().Str("username", req.Username).Msg("Login attempt")

isLocked, remaining := controller.auth.IsAccountLocked(req.Username)

if isLocked {
controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "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) {
controller.auth.DummyPasswordCheck(req.Password)
controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt")
controller.auth.RecordLoginAttempt(req.Username, false)
controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found")
c.JSON(401, gin.H{
"status": 401,
Expand All @@ -108,14 +93,24 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return
}

isLocked, remaining := controller.auth.IsAccountLocked(req.Username)

if isLocked {
controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "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
}

if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
controller.log.App.Warn().Str("username", req.Username).Msg("Invalid password during login attempt")
controller.auth.RecordLoginAttempt(req.Username, false)
if search.Type == model.UserLocal {
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "invalid password")
} else {
controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password")
}
controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "invalid password")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
Expand Down Expand Up @@ -216,11 +211,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {

controller.log.App.Info().Str("username", req.Username).Msg("Login successful")

if search.Type == model.UserLocal {
controller.log.AuditLoginSuccess(req.Username, "local", c.ClientIP())
} else {
controller.log.AuditLoginSuccess(req.Username, "ldap", c.ClientIP())
}
controller.log.AuditLoginSuccess(req.Username, search.Type.String(), c.ClientIP())

controller.auth.RecordLoginAttempt(req.Username, true)

Expand Down
2 changes: 0 additions & 2 deletions internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
ACLs: ACLsConfig{
Policy: "allow",
},
LockdownEnabled: true,
},
UI: UIConfig{
Title: "Tinyauth",
Expand Down Expand Up @@ -151,7 +150,6 @@ type AuthConfig struct {
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"`
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"`
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
}
Expand Down
23 changes: 12 additions & 11 deletions internal/model/runtime.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package model

type RuntimeConfig struct {
AppURL string
UUID string
CookieDomain string
SessionCookieName string
CSRFCookieName string
RedirectCookieName string
OAuthSessionCookieName string
LocalUsers []LocalUser
OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string
ConfiguredProviders []Provider
AppURL string
UUID string
CookieDomain string
SessionCookieName string
CSRFCookieName string
RedirectCookieName string
OAuthSessionCookieName string
LocalUsers []LocalUser
OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string
ConfiguredProviders []Provider
TrustedProxiesConfigured bool
}

type Provider struct {
Expand Down
10 changes: 10 additions & 0 deletions internal/model/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ const (
UserLDAP
)

func (t UserSearchType) String() string {
switch t {
case UserLocal:
return "local"
case UserLDAP:
return "ldap"
}
return "unknown"
}

type LDAPUser struct {
DN string
Groups []string
Expand Down
8 changes: 8 additions & 0 deletions internal/service/access_controls_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ type IPAllowedRule struct {
}

func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
if !ctx.TrustedProxiesConfigured {
return EffectAllow // We can't block the proxy
}

// merge global and per-app block/allow lists
blockedIps := append([]string{}, rule.Config.Auth.IP.Block...)
allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...)
Expand Down Expand Up @@ -263,6 +267,10 @@ type IPBypassedRule struct {
}

func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
if !ctx.TrustedProxiesConfigured {
return EffectDeny
}

// merge global and per-app bypass lists
bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...)
if ctx.ACLs != nil {
Expand Down
Loading
Loading