mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-13 19:02:29 +00:00
feat: add session max lifetime and fix refresh logic (#559)
* feat: allow any HTTP method for /api/auth/envoy and restrict methods for non-envoy proxies * feat: add Allow header for invalid methods in proxyHandler * feat: add session max lifetime and fix refresh logic * fix: set default value for created_at column and improve session expiration logic --------- Co-authored-by: Stavros <steveiliop56@gmail.com>
This commit is contained in:
@@ -38,6 +38,8 @@ TINYAUTH_AUTH_USERSFILE=""
|
|||||||
TINYAUTH_AUTH_SECURECOOKIE="true"
|
TINYAUTH_AUTH_SECURECOOKIE="true"
|
||||||
# Session expiry in seconds (7200 = 2 hours)
|
# Session expiry in seconds (7200 = 2 hours)
|
||||||
TINYAUTH_AUTH_SESSIONEXPIRY="7200"
|
TINYAUTH_AUTH_SESSIONEXPIRY="7200"
|
||||||
|
# Session maximum lifetime in seconds (0 = unlimited)
|
||||||
|
TINYAUTH_AUTH_SESSIONMAXLIFETIME="0"
|
||||||
# Login timeout in seconds (300 = 5 minutes)
|
# Login timeout in seconds (300 = 5 minutes)
|
||||||
TINYAUTH_AUTH_LOGINTIMEOUT="300"
|
TINYAUTH_AUTH_LOGINTIMEOUT="300"
|
||||||
# Maximum login retries before lockout
|
# Maximum login retries before lockout
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
},
|
},
|
||||||
Auth: config.AuthConfig{
|
Auth: config.AuthConfig{
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
|
SessionMaxLifetime: 0,
|
||||||
LoginTimeout: 300,
|
LoginTimeout: 300,
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ auth:
|
|||||||
secureCookie: false
|
secureCookie: false
|
||||||
# Session expiry in seconds (3600 = 1 hour)
|
# Session expiry in seconds (3600 = 1 hour)
|
||||||
sessionExpiry: 3600
|
sessionExpiry: 3600
|
||||||
|
# Session maximum lifetime in seconds (0 = unlimited)
|
||||||
|
sessionMaxLifetime: 0
|
||||||
# Login timeout in seconds (300 = 5 minutes)
|
# Login timeout in seconds (300 = 5 minutes)
|
||||||
loginTimeout: 300
|
loginTimeout: 300
|
||||||
# Maximum login retries before lockout
|
# Maximum login retries before lockout
|
||||||
|
|||||||
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 {
|
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
|
// Parse users
|
||||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
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)
|
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")
|
migrations, err := iofs.New(assets.Migrations, "migrations")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
Users: app.context.users,
|
Users: app.context.users,
|
||||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
|
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
||||||
SecureCookie: app.config.Auth.SecureCookie,
|
SecureCookie: app.config.Auth.SecureCookie,
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
LoginTimeout: app.config.Auth.LoginTimeout,
|
LoginTimeout: app.config.Auth.LoginTimeout,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type AuthConfig struct {
|
|||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
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"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
|||||||
},
|
},
|
||||||
OauthWhitelist: "",
|
OauthWhitelist: "",
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
|
SessionMaxLifetime: 0,
|
||||||
SecureCookie: false,
|
SecureCookie: false,
|
||||||
CookieDomain: "localhost",
|
CookieDomain: "localhost",
|
||||||
LoginTimeout: 300,
|
LoginTimeout: 300,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
|
|||||||
},
|
},
|
||||||
OauthWhitelist: "",
|
OauthWhitelist: "",
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
|
SessionMaxLifetime: 0,
|
||||||
SecureCookie: false,
|
SecureCookie: false,
|
||||||
CookieDomain: "localhost",
|
CookieDomain: "localhost",
|
||||||
LoginTimeout: 300,
|
LoginTimeout: 300,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Session struct {
|
|||||||
TotpPending bool
|
TotpPending bool
|
||||||
OAuthGroups string
|
OAuthGroups string
|
||||||
Expiry int64
|
Expiry int64
|
||||||
|
CreatedAt int64
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ INSERT INTO sessions (
|
|||||||
"totp_pending",
|
"totp_pending",
|
||||||
"oauth_groups",
|
"oauth_groups",
|
||||||
"expiry",
|
"expiry",
|
||||||
|
"created_at",
|
||||||
"oauth_name",
|
"oauth_name",
|
||||||
"oauth_sub"
|
"oauth_sub"
|
||||||
) VALUES (
|
) 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 {
|
type CreateSessionParams struct {
|
||||||
@@ -36,6 +37,7 @@ type CreateSessionParams struct {
|
|||||||
TotpPending bool
|
TotpPending bool
|
||||||
OAuthGroups string
|
OAuthGroups string
|
||||||
Expiry int64
|
Expiry int64
|
||||||
|
CreatedAt int64
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
}
|
}
|
||||||
@@ -50,6 +52,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
|||||||
arg.TotpPending,
|
arg.TotpPending,
|
||||||
arg.OAuthGroups,
|
arg.OAuthGroups,
|
||||||
arg.Expiry,
|
arg.Expiry,
|
||||||
|
arg.CreatedAt,
|
||||||
arg.OAuthName,
|
arg.OAuthName,
|
||||||
arg.OAuthSub,
|
arg.OAuthSub,
|
||||||
)
|
)
|
||||||
@@ -63,6 +66,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
|||||||
&i.TotpPending,
|
&i.TotpPending,
|
||||||
&i.OAuthGroups,
|
&i.OAuthGroups,
|
||||||
&i.Expiry,
|
&i.Expiry,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.OAuthName,
|
&i.OAuthName,
|
||||||
&i.OAuthSub,
|
&i.OAuthSub,
|
||||||
)
|
)
|
||||||
@@ -90,7 +94,7 @@ func (q *Queries) DeleteSession(ctx context.Context, uuid string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSession = `-- name: GetSession :one
|
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" = ?
|
WHERE "uuid" = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -106,6 +110,7 @@ func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error)
|
|||||||
&i.TotpPending,
|
&i.TotpPending,
|
||||||
&i.OAuthGroups,
|
&i.OAuthGroups,
|
||||||
&i.Expiry,
|
&i.Expiry,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.OAuthName,
|
&i.OAuthName,
|
||||||
&i.OAuthSub,
|
&i.OAuthSub,
|
||||||
)
|
)
|
||||||
@@ -124,7 +129,7 @@ UPDATE "sessions" SET
|
|||||||
"oauth_name" = ?,
|
"oauth_name" = ?,
|
||||||
"oauth_sub" = ?
|
"oauth_sub" = ?
|
||||||
WHERE "uuid" = ?
|
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 {
|
type UpdateSessionParams struct {
|
||||||
@@ -163,6 +168,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
|
|||||||
&i.TotpPending,
|
&i.TotpPending,
|
||||||
&i.OAuthGroups,
|
&i.OAuthGroups,
|
||||||
&i.Expiry,
|
&i.Expiry,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.OAuthName,
|
&i.OAuthName,
|
||||||
&i.OAuthSub,
|
&i.OAuthSub,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type AuthServiceConfig struct {
|
|||||||
Users []config.User
|
Users []config.User
|
||||||
OauthWhitelist string
|
OauthWhitelist string
|
||||||
SessionExpiry int
|
SessionExpiry int
|
||||||
|
SessionMaxLifetime int
|
||||||
SecureCookie bool
|
SecureCookie bool
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
LoginTimeout int
|
LoginTimeout int
|
||||||
@@ -212,6 +213,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
|
|||||||
TotpPending: data.TotpPending,
|
TotpPending: data.TotpPending,
|
||||||
OAuthGroups: data.OAuthGroups,
|
OAuthGroups: data.OAuthGroups,
|
||||||
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
OAuthName: data.OAuthName,
|
OAuthName: data.OAuthName,
|
||||||
OAuthSub: data.OAuthSub,
|
OAuthSub: data.OAuthSub,
|
||||||
}
|
}
|
||||||
@@ -242,11 +244,19 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
|
|
||||||
currentTime := time.Now().Unix()
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newExpiry := currentTime + int64(time.Hour.Seconds())
|
newExpiry := session.Expiry + refreshThreshold
|
||||||
|
|
||||||
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
|
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
|
||||||
Username: session.Username,
|
Username: session.Username,
|
||||||
@@ -265,7 +275,8 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -306,6 +317,16 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
|||||||
|
|
||||||
currentTime := time.Now().Unix()
|
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 {
|
if currentTime > session.Expiry {
|
||||||
err = auth.queries.DeleteSession(c, cookie)
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ INSERT INTO sessions (
|
|||||||
"totp_pending",
|
"totp_pending",
|
||||||
"oauth_groups",
|
"oauth_groups",
|
||||||
"expiry",
|
"expiry",
|
||||||
|
"created_at",
|
||||||
"oauth_name",
|
"oauth_name",
|
||||||
"oauth_sub"
|
"oauth_sub"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS "sessions" (
|
|||||||
"totp_pending" BOOLEAN NOT NULL,
|
"totp_pending" BOOLEAN NOT NULL,
|
||||||
"oauth_groups" TEXT NULL,
|
"oauth_groups" TEXT NULL,
|
||||||
"expiry" INTEGER NOT NULL,
|
"expiry" INTEGER NOT NULL,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
"oauth_name" TEXT NULL,
|
"oauth_name" TEXT NULL,
|
||||||
"oauth_sub" TEXT NULL
|
"oauth_sub" TEXT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user