mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-14 03:12:28 +00:00
Merge branch 'main' into feat/ldap-groups
This commit is contained in:
1
internal/assets/migrations/000004_created_at.down.sql
Normal file
1
internal/assets/migrations/000004_created_at.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "sessions" DROP COLUMN "created_at";
|
||||
1
internal/assets/migrations/000004_created_at.up.sql
Normal file
1
internal/assets/migrations/000004_created_at.up.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "sessions" ADD COLUMN "created_at" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -42,6 +42,10 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) Setup() error {
|
||||
// validate session config
|
||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
||||
}
|
||||
// Parse users
|
||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Limit to 1 connection to sequence writes, this may need to be revisited in the future
|
||||
// if the sqlite connection starts being a bottleneck
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
migrations, err := iofs.New(assets.Migrations, "migrations")
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/middleware"
|
||||
@@ -15,7 +14,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
engine.Use(gin.Recovery())
|
||||
|
||||
if len(app.config.Server.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ","))
|
||||
err := engine.SetTrustedProxies(app.config.Server.TrustedProxies)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||
|
||||
@@ -58,15 +58,17 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
||||
services.accessControlService = accessControlsService
|
||||
|
||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||
Users: app.context.users,
|
||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||
SecureCookie: app.config.Auth.SecureCookie,
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
LoginTimeout: app.config.Auth.LoginTimeout,
|
||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||
SessionCookieName: app.context.sessionCookieName,
|
||||
}, dockerService, ldapService, queries)
|
||||
Users: app.context.users,
|
||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
||||
SecureCookie: app.config.Auth.SecureCookie,
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
LoginTimeout: app.config.Auth.LoginTimeout,
|
||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||
SessionCookieName: app.context.sessionCookieName,
|
||||
IP: app.config.Auth.IP,
|
||||
}, dockerService, services.ldapService, queries)
|
||||
|
||||
err = authService.Init()
|
||||
|
||||
|
||||
@@ -33,23 +33,30 @@ type Config struct {
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
||||
TrustedProxies string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||
}
|
||||
|
||||
type IPConfig struct {
|
||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||
}
|
||||
|
||||
type OAuthConfig struct {
|
||||
Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
||||
}
|
||||
|
||||
@@ -179,9 +179,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if userContext.IsLoggedIn {
|
||||
appAllowed := controller.auth.IsResourceAllowed(c, userContext, acls)
|
||||
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
||||
|
||||
if !appAllowed {
|
||||
if !userAllowed {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
|
||||
@@ -57,13 +57,14 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||
},
|
||||
},
|
||||
OauthWhitelist: "",
|
||||
SessionExpiry: 3600,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
OauthWhitelist: []string{},
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}, dockerService, nil, queries)
|
||||
|
||||
// Controller
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
@@ -60,23 +61,17 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
log.Debug().Str("username", req.Username).Msg("Login attempt")
|
||||
|
||||
rateIdentifier := req.Username
|
||||
|
||||
if rateIdentifier == "" {
|
||||
rateIdentifier = clientIP
|
||||
}
|
||||
|
||||
log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt")
|
||||
|
||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
||||
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
|
||||
|
||||
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{
|
||||
"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
|
||||
}
|
||||
@@ -84,8 +79,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
userSearch := controller.auth.SearchUser(req.Username)
|
||||
|
||||
if userSearch.Type == "unknown" {
|
||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found")
|
||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
||||
log.Warn().Str("username", req.Username).Msg("User not found")
|
||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -94,8 +89,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password")
|
||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
||||
log.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -103,9 +98,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
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" {
|
||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||
@@ -209,23 +204,17 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
log.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
||||
|
||||
rateIdentifier := 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)
|
||||
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
||||
|
||||
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{
|
||||
"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
|
||||
}
|
||||
@@ -235,8 +224,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
ok := totp.Validate(req.Code, user.TotpSecret)
|
||||
|
||||
if !ok {
|
||||
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code")
|
||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
||||
log.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
||||
controller.auth.RecordLoginAttempt(context.Username, false)
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -244,9 +233,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
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{
|
||||
Username: user.Username,
|
||||
|
||||
@@ -60,13 +60,14 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
|
||||
TotpSecret: totpSecret,
|
||||
},
|
||||
},
|
||||
OauthWhitelist: "",
|
||||
SessionExpiry: 3600,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
OauthWhitelist: []string{},
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}, nil, nil, queries)
|
||||
|
||||
// Controller
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
@@ -116,20 +117,34 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
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)
|
||||
|
||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||
log.Debug().Msg("User from basic auth not found")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||
log.Debug().Msg("Invalid password for basic auth user")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
m.auth.RecordLoginAttempt(basic.Username, true)
|
||||
|
||||
switch userSearch.Type {
|
||||
case "local":
|
||||
log.Debug().Msg("Basic auth user is local")
|
||||
|
||||
@@ -13,6 +13,7 @@ type Session struct {
|
||||
TotpPending bool
|
||||
OAuthGroups string
|
||||
Expiry int64
|
||||
CreatedAt int64
|
||||
OAuthName string
|
||||
OAuthSub string
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: query.sql
|
||||
// source: queries.sql
|
||||
|
||||
package repository
|
||||
|
||||
@@ -19,12 +19,13 @@ INSERT INTO sessions (
|
||||
"totp_pending",
|
||||
"oauth_groups",
|
||||
"expiry",
|
||||
"created_at",
|
||||
"oauth_name",
|
||||
"oauth_sub"
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, oauth_name, oauth_sub
|
||||
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
|
||||
`
|
||||
|
||||
type CreateSessionParams struct {
|
||||
@@ -36,6 +37,7 @@ type CreateSessionParams struct {
|
||||
TotpPending bool
|
||||
OAuthGroups string
|
||||
Expiry int64
|
||||
CreatedAt int64
|
||||
OAuthName string
|
||||
OAuthSub string
|
||||
}
|
||||
@@ -50,6 +52,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
||||
arg.TotpPending,
|
||||
arg.OAuthGroups,
|
||||
arg.Expiry,
|
||||
arg.CreatedAt,
|
||||
arg.OAuthName,
|
||||
arg.OAuthSub,
|
||||
)
|
||||
@@ -63,6 +66,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
||||
&i.TotpPending,
|
||||
&i.OAuthGroups,
|
||||
&i.Expiry,
|
||||
&i.CreatedAt,
|
||||
&i.OAuthName,
|
||||
&i.OAuthSub,
|
||||
)
|
||||
@@ -90,7 +94,7 @@ func (q *Queries) DeleteSession(ctx context.Context, uuid string) error {
|
||||
}
|
||||
|
||||
const getSession = `-- name: GetSession :one
|
||||
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, oauth_name, oauth_sub FROM "sessions"
|
||||
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub FROM "sessions"
|
||||
WHERE "uuid" = ?
|
||||
`
|
||||
|
||||
@@ -106,6 +110,7 @@ func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error)
|
||||
&i.TotpPending,
|
||||
&i.OAuthGroups,
|
||||
&i.Expiry,
|
||||
&i.CreatedAt,
|
||||
&i.OAuthName,
|
||||
&i.OAuthSub,
|
||||
)
|
||||
@@ -124,7 +129,7 @@ UPDATE "sessions" SET
|
||||
"oauth_name" = ?,
|
||||
"oauth_sub" = ?
|
||||
WHERE "uuid" = ?
|
||||
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, oauth_name, oauth_sub
|
||||
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
|
||||
`
|
||||
|
||||
type UpdateSessionParams struct {
|
||||
@@ -163,6 +168,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
|
||||
&i.TotpPending,
|
||||
&i.OAuthGroups,
|
||||
&i.Expiry,
|
||||
&i.CreatedAt,
|
||||
&i.OAuthName,
|
||||
&i.OAuthSub,
|
||||
)
|
||||
@@ -26,14 +26,16 @@ type LoginAttempt struct {
|
||||
}
|
||||
|
||||
type AuthServiceConfig struct {
|
||||
Users []config.User
|
||||
OauthWhitelist string
|
||||
SessionExpiry int
|
||||
SecureCookie bool
|
||||
CookieDomain string
|
||||
LoginTimeout int
|
||||
LoginMaxRetries int
|
||||
SessionCookieName string
|
||||
Users []config.User
|
||||
OauthWhitelist []string
|
||||
SessionExpiry int
|
||||
SessionMaxLifetime int
|
||||
SecureCookie bool
|
||||
CookieDomain string
|
||||
LoginTimeout int
|
||||
LoginMaxRetries int
|
||||
SessionCookieName string
|
||||
IP config.IPConfig
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
@@ -185,7 +187,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||
return utils.CheckFilter(auth.config.OauthWhitelist, email)
|
||||
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||
}
|
||||
|
||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
|
||||
@@ -212,6 +214,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
|
||||
TotpPending: data.TotpPending,
|
||||
OAuthGroups: data.OAuthGroups,
|
||||
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
OAuthName: data.OAuthName,
|
||||
OAuthSub: data.OAuthSub,
|
||||
}
|
||||
@@ -242,11 +245,19 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
if session.Expiry-currentTime > int64(time.Hour.Seconds()) {
|
||||
var refreshThreshold int64
|
||||
|
||||
if auth.config.SessionExpiry <= int(time.Hour.Seconds()) {
|
||||
refreshThreshold = int64(auth.config.SessionExpiry / 2)
|
||||
} else {
|
||||
refreshThreshold = int64(time.Hour.Seconds())
|
||||
}
|
||||
|
||||
if session.Expiry-currentTime > refreshThreshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
newExpiry := currentTime + int64(time.Hour.Seconds())
|
||||
newExpiry := session.Expiry + refreshThreshold
|
||||
|
||||
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
|
||||
Username: session.Username,
|
||||
@@ -265,7 +276,8 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SetCookie(auth.config.SessionCookieName, cookie, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||
log.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -306,6 +318,16 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
|
||||
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
||||
err = auth.queries.DeleteSession(c, cookie)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
||||
}
|
||||
return config.SessionCookie{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
if currentTime > session.Expiry {
|
||||
err = auth.queries.DeleteSession(c, cookie)
|
||||
if err != nil {
|
||||
@@ -331,7 +353,7 @@ func (auth *AuthService) UserAuthConfigured() bool {
|
||||
return len(auth.config.Users) > 0 || auth.ldap != nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||
if context.OAuth {
|
||||
log.Debug().Msg("Checking OAuth whitelist")
|
||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
||||
@@ -414,7 +436,11 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
||||
}
|
||||
|
||||
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||
for _, blocked := range acls.Block {
|
||||
// Merge the global and app IP filter
|
||||
blockedIps := append(auth.config.IP.Block, acls.Block...)
|
||||
allowedIPs := append(auth.config.IP.Allow, acls.Allow...)
|
||||
|
||||
for _, blocked := range blockedIps {
|
||||
res, err := utils.FilterIP(blocked, ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||
@@ -426,7 +452,7 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
for _, allowed := range acls.Allow {
|
||||
for _, allowed := range allowedIPs {
|
||||
res, err := utils.FilterIP(allowed, ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||
@@ -438,7 +464,7 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
if len(acls.Allow) > 0 {
|
||||
if len(allowedIPs) > 0 {
|
||||
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,22 +7,14 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
)
|
||||
|
||||
func ParseUsers(users string) ([]config.User, error) {
|
||||
var usersParsed []config.User
|
||||
func ParseUsers(usersStr []string) ([]config.User, error) {
|
||||
var users []config.User
|
||||
|
||||
users = strings.TrimSpace(users)
|
||||
|
||||
if users == "" {
|
||||
if len(usersStr) == 0 {
|
||||
return []config.User{}, nil
|
||||
}
|
||||
|
||||
userList := strings.Split(users, ",")
|
||||
|
||||
if len(userList) == 0 {
|
||||
return []config.User{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
for _, user := range userList {
|
||||
for _, user := range usersStr {
|
||||
if strings.TrimSpace(user) == "" {
|
||||
continue
|
||||
}
|
||||
@@ -30,64 +22,71 @@ func ParseUsers(users string) ([]config.User, error) {
|
||||
if err != nil {
|
||||
return []config.User{}, err
|
||||
}
|
||||
usersParsed = append(usersParsed, parsed)
|
||||
users = append(users, parsed)
|
||||
}
|
||||
|
||||
return usersParsed, nil
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetUsers(conf string, file string) ([]config.User, error) {
|
||||
var users string
|
||||
func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
|
||||
var usersStr []string
|
||||
|
||||
if conf == "" && file == "" {
|
||||
if len(usersCfg) == 0 && usersPath == "" {
|
||||
return []config.User{}, nil
|
||||
}
|
||||
|
||||
if conf != "" {
|
||||
users += conf
|
||||
if len(usersCfg) > 0 {
|
||||
usersStr = append(usersStr, usersCfg...)
|
||||
}
|
||||
|
||||
if file != "" {
|
||||
contents, err := ReadFile(file)
|
||||
if usersPath != "" {
|
||||
contents, err := ReadFile(usersPath)
|
||||
|
||||
if err != nil {
|
||||
return []config.User{}, err
|
||||
}
|
||||
if users != "" {
|
||||
users += ","
|
||||
|
||||
lines := strings.SplitSeq(contents, "\n")
|
||||
|
||||
for line := range lines {
|
||||
lineTrimmed := strings.TrimSpace(line)
|
||||
if lineTrimmed == "" {
|
||||
continue
|
||||
}
|
||||
usersStr = append(usersStr, lineTrimmed)
|
||||
}
|
||||
users += ParseFileToLine(contents)
|
||||
}
|
||||
|
||||
return ParseUsers(users)
|
||||
return ParseUsers(usersStr)
|
||||
}
|
||||
|
||||
func ParseUser(user string) (config.User, error) {
|
||||
if strings.Contains(user, "$$") {
|
||||
user = strings.ReplaceAll(user, "$$", "$")
|
||||
func ParseUser(userStr string) (config.User, error) {
|
||||
if strings.Contains(userStr, "$$") {
|
||||
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
||||
}
|
||||
|
||||
userSplit := strings.Split(user, ":")
|
||||
parts := strings.SplitN(userStr, ":", 4)
|
||||
|
||||
if len(userSplit) < 2 || len(userSplit) > 3 {
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
return config.User{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
for _, userPart := range userSplit {
|
||||
if strings.TrimSpace(userPart) == "" {
|
||||
for i, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return config.User{}, errors.New("invalid user format")
|
||||
}
|
||||
parts[i] = trimmed
|
||||
}
|
||||
|
||||
if len(userSplit) == 2 {
|
||||
return config.User{
|
||||
Username: strings.TrimSpace(userSplit[0]),
|
||||
Password: strings.TrimSpace(userSplit[1]),
|
||||
}, nil
|
||||
user := config.User{
|
||||
Username: parts[0],
|
||||
Password: parts[1],
|
||||
}
|
||||
|
||||
return config.User{
|
||||
Username: strings.TrimSpace(userSplit[0]),
|
||||
Password: strings.TrimSpace(userSplit[1]),
|
||||
TotpSecret: strings.TrimSpace(userSplit[2]),
|
||||
}, nil
|
||||
if len(parts) == 3 {
|
||||
user.TotpSecret = parts[2]
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestGetUsers(t *testing.T) {
|
||||
defer os.Remove("/tmp/tinyauth_users_test.txt")
|
||||
|
||||
// Test file
|
||||
users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt")
|
||||
users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestGetUsers(t *testing.T) {
|
||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
||||
|
||||
// Test config
|
||||
users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "")
|
||||
users, err = utils.GetUsers([]string{"user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestGetUsers(t *testing.T) {
|
||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
||||
|
||||
// Test both
|
||||
users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt")
|
||||
users, err = utils.GetUsers([]string{"user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "/tmp/tinyauth_users_test.txt")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -60,14 +60,14 @@ func TestGetUsers(t *testing.T) {
|
||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password)
|
||||
|
||||
// Test empty
|
||||
users, err = utils.GetUsers("", "")
|
||||
users, err = utils.GetUsers([]string{}, "")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(users))
|
||||
|
||||
// Test non-existent file
|
||||
users, err = utils.GetUsers("", "/tmp/non_existent_file.txt")
|
||||
users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt")
|
||||
|
||||
assert.ErrorContains(t, err, "no such file or directory")
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestGetUsers(t *testing.T) {
|
||||
|
||||
func TestParseUsers(t *testing.T) {
|
||||
// Valid users
|
||||
users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP
|
||||
users, err := utils.ParseUsers([]string{"user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF"}) // user2 has TOTP
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestParseUsers(t *testing.T) {
|
||||
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
|
||||
|
||||
// Valid weirdly spaced users
|
||||
users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose
|
||||
users, err = utils.ParseUsers([]string{" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ", " user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF "}) // Spacing is on purpose
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(users))
|
||||
|
||||
Reference in New Issue
Block a user