Compare commits

..

27 Commits

Author SHA1 Message Date
Stavros c6d36673eb fix: don't audit login too early 2026-05-06 16:34:56 +03:00
Stavros 71ae3e0cd2 tests: fix user controller tests 2026-05-06 16:26:12 +03:00
Stavros 0d9865793c fix: delete totp pending session cookie on totp success 2026-05-06 16:14:12 +03:00
Stavros 04b2290d73 fix: review comments batch 3 2026-05-05 18:56:35 +03:00
Stavros e04980468f fix: review comments batch 2 2026-05-05 18:54:45 +03:00
Stavros d47e4d3d79 fix: review comments batch 1 2026-05-05 18:43:22 +03:00
Stavros f3965a7470 fix: fix verion setting in cd and dockerfiles 2026-05-04 21:08:45 +03:00
Stavros 36d4e3ec52 tests: fix log wrapper tests 2026-05-04 21:01:13 +03:00
Stavros eab9f71110 tests: remove error wrapper from context tests 2026-05-04 20:57:37 +03:00
Stavros e13598bf3c tests: add tests for context middleware 2026-05-04 20:52:59 +03:00
Stavros 4d3860f860 tests: add tests for context parsing 2026-05-04 20:33:49 +03:00
Stavros 3b5da06862 fix: fix config reference generator 2026-05-04 20:25:56 +03:00
Stavros 8f337aaff8 tests: move to testify for testing in utils 2026-05-04 20:25:16 +03:00
Stavros ff3c25c09d tests: fix utils tests 2026-05-04 20:18:34 +03:00
Stavros 26daef7d4e tests: fix service tests 2026-05-04 20:11:07 +03:00
Stavros c932817757 fix: fix controller tests 2026-05-04 20:07:03 +03:00
Stavros 004df2f852 chore: rename get basic auth to encode basic auth for clarity 2026-05-04 16:14:45 +03:00
Stavros df56708b9a refactor: simplify acls checking logic by passing the entire acl struct 2026-05-04 16:13:39 +03:00
Stavros 62ffd2fd11 feat: finalize context functionality 2026-04-29 20:11:43 +03:00
Stavros a3ec07230c fix: fix oauth and oidc controller imports and context 2026-04-29 20:00:36 +03:00
Stavros b4eb7090bd fix: fix imports and context in proxy controller 2026-04-29 19:58:39 +03:00
Stavros 2f24f823eb fix: use new context in user controller 2026-04-29 19:45:23 +03:00
Stavros 9a219046ac fix: context controller 2026-04-29 19:31:44 +03:00
Stavros 97d58b376d fix: fix cli imports 2026-04-29 19:28:40 +03:00
Stavros b426a1529e fix: fix bootstrap import issues 2026-04-29 19:27:38 +03:00
Stavros c7efb71a5a fix: fix util imports 2026-04-29 19:25:23 +03:00
Stavros eec75a6f49 wip 2026-04-29 19:21:07 +03:00
16 changed files with 25 additions and 63 deletions
+1 -1
View File
@@ -38,6 +38,6 @@ jobs:
retention-days: 5 retention-days: 5
- name: Upload to code-scanning - name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4 uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+1 -7
View File
@@ -104,13 +104,7 @@ func (app *BootstrapApp) Setup() error {
} }
// Get cookie domain // Get cookie domain
cookieDomainResolver := utils.GetCookieDomain cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
if !app.config.Auth.SubdomainsEnabled {
tlog.App.Info().Msg("Subdomains disabled, automatic authentication for proxied apps will not work")
cookieDomainResolver = utils.GetStandaloneCookieDomain
}
cookieDomain, err := cookieDomainResolver(app.context.appUrl)
if err != nil { if err != nil {
return err return err
-1
View File
@@ -84,7 +84,6 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
RedirectCookieName: app.context.redirectCookieName, RedirectCookieName: app.context.redirectCookieName,
CookieDomain: app.context.cookieDomain, CookieDomain: app.context.cookieDomain,
OAuthSessionCookieName: app.context.oauthSessionCookieName, OAuthSessionCookieName: app.context.oauthSessionCookieName,
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
}, apiRouter, app.services.authService) }, apiRouter, app.services.authService)
oauthController.SetupRoutes() oauthController.SetupRoutes()
-1
View File
@@ -100,7 +100,6 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
SessionCookieName: app.context.sessionCookieName, SessionCookieName: app.context.sessionCookieName,
IP: app.config.Auth.IP, IP: app.config.Auth.IP,
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL, LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
}, services.ldapService, queries, services.oauthBrokerService) }, services.ldapService, queries, services.oauthBrokerService)
err = authService.Init() err = authService.Init()
+1 -1
View File
@@ -95,7 +95,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
Username: context.GetUsername(), Username: context.GetUsername(),
Name: context.GetName(), Name: context.GetName(),
Email: context.GetEmail(), Email: context.GetEmail(),
Provider: context.GetProviderID(), Provider: context.ProviderName(),
OAuth: context.IsOAuth(), OAuth: context.IsOAuth(),
TOTPPending: context.TOTPPending(), TOTPPending: context.TOTPPending(),
OAuthName: context.OAuthName(), OAuthName: context.OAuthName(),
+2 -10
View File
@@ -26,7 +26,6 @@ type OAuthControllerConfig struct {
SecureCookie bool SecureCookie bool
AppURL string AppURL string
CookieDomain string CookieDomain string
SubdomainsEnabled bool
} }
type OAuthController struct { type OAuthController struct {
@@ -106,7 +105,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return return
} }
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.SecureCookie, true) c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -136,7 +135,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
return return
} }
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.SecureCookie, true) c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie) oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
@@ -284,10 +283,3 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams)
params.ClientID != "" && params.ClientID != "" &&
params.RedirectURI != "" params.RedirectURI != ""
} }
func (controller *OAuthController) getCookieDomain() string {
if controller.config.SubdomainsEnabled {
return "." + controller.config.CookieDomain
}
return controller.config.CookieDomain
}
@@ -98,6 +98,7 @@ func TestProxyController(t *testing.T) {
Name: "Totpuser", Name: "Totpuser",
Email: "totpuser@example.com", Email: "totpuser@example.com",
}, },
TOTPEnabled: true,
}, },
}) })
c.Next() c.Next()
+1 -1
View File
@@ -249,7 +249,7 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
context, err := new(model.UserContext).NewFromGin(c) context, err := new(model.UserContext).NewFromGin(c)
if err == nil { if err == nil {
tlog.AuditLogout(c, context.GetUsername(), context.GetProviderID()) tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
} else { } else {
tlog.App.Warn().Err(err).Msg("Failed to get user context for logout audit, proceeding without username") tlog.App.Warn().Err(err).Msg("Failed to get user context for logout audit, proceeding without username")
tlog.AuditLogout(c, "unknown", "unknown") tlog.AuditLogout(c, "unknown", "unknown")
+5 -7
View File
@@ -78,6 +78,7 @@ func TestUserController(t *testing.T) {
Email: "totpuser@example.com", Email: "totpuser@example.com",
}, },
TOTPPending: true, TOTPPending: true,
TOTPEnabled: true,
}, },
}) })
} }
@@ -93,6 +94,7 @@ func TestUserController(t *testing.T) {
Email: "bob@example.com", Email: "bob@example.com",
}, },
TOTPPending: true, TOTPPending: true,
TOTPEnabled: true,
}, },
}) })
} }
@@ -150,9 +152,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", cookie.Name) assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly) assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain) assert.Equal(t, "example.com", cookie.Domain)
// 3 seconds should be more than enough for even slow test environments assert.Equal(t, 9, cookie.MaxAge)
assert.GreaterOrEqual(t, cookie.MaxAge, 7)
assert.LessOrEqual(t, cookie.MaxAge, 10)
}, },
}, },
{ {
@@ -241,8 +241,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", cookie.Name) assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly) assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain) assert.Equal(t, "example.com", cookie.Domain)
assert.GreaterOrEqual(t, cookie.MaxAge, 3597) assert.Equal(t, 3599, cookie.MaxAge) // 1 hour, default for totp pending sessions
assert.LessOrEqual(t, cookie.MaxAge, 3600)
}, },
}, },
{ {
@@ -336,8 +335,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", totpCookie.Name) assert.Equal(t, "tinyauth-session", totpCookie.Name)
assert.True(t, totpCookie.HttpOnly) assert.True(t, totpCookie.HttpOnly)
assert.Equal(t, "example.com", totpCookie.Domain) assert.Equal(t, "example.com", totpCookie.Domain)
assert.GreaterOrEqual(t, totpCookie.MaxAge, 7) assert.Equal(t, 9, totpCookie.MaxAge) // should use the regular session expiry time
assert.LessOrEqual(t, totpCookie.MaxAge, 10)
}, },
}, },
{ {
@@ -123,6 +123,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
if userContext.Provider == model.ProviderLocal && if userContext.Provider == model.ProviderLocal &&
userContext.Local.TOTPPending { userContext.Local.TOTPPending {
userContext.Local.TOTPEnabled = true
return userContext, nil, nil return userContext, nil, nil
} }
@@ -109,6 +109,7 @@ func TestContextMiddleware(t *testing.T) {
assert.Equal(t, "testuser", userCtx.GetUsername()) assert.Equal(t, "testuser", userCtx.GetUsername())
assert.True(t, userCtx.Authenticated) assert.True(t, userCtx.Authenticated)
require.NotNil(t, userCtx.Local) require.NotNil(t, userCtx.Local)
assert.False(t, userCtx.Local.TOTPEnabled)
}, },
}, },
{ {
@@ -133,6 +134,7 @@ func TestContextMiddleware(t *testing.T) {
assert.False(t, userCtx.Authenticated) assert.False(t, userCtx.Authenticated)
require.NotNil(t, userCtx.Local) require.NotNil(t, userCtx.Local)
assert.True(t, userCtx.Local.TOTPPending) assert.True(t, userCtx.Local.TOTPPending)
assert.True(t, userCtx.Local.TOTPEnabled)
}, },
}, },
{ {
-2
View File
@@ -18,7 +18,6 @@ func NewDefaultConfiguration() *Config {
Address: "0.0.0.0", Address: "0.0.0.0",
}, },
Auth: AuthConfig{ Auth: AuthConfig{
SubdomainsEnabled: true,
SessionExpiry: 86400, // 1 day SessionExpiry: 86400, // 1 day
SessionMaxLifetime: 0, // disabled SessionMaxLifetime: 0, // disabled
LoginTimeout: 300, // 5 minutes LoginTimeout: 300, // 5 minutes
@@ -103,7 +102,6 @@ type ServerConfig struct {
type AuthConfig struct { type AuthConfig struct {
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"` IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"` UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
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"`
+3 -2
View File
@@ -34,6 +34,7 @@ type BaseContext struct {
type LocalContext struct { type LocalContext struct {
BaseContext BaseContext
TOTPPending bool TOTPPending bool
TOTPEnabled bool
Attributes UserAttributes Attributes UserAttributes
} }
@@ -222,14 +223,14 @@ func (c *UserContext) GetName() string {
} }
} }
func (c *UserContext) GetProviderID() string { func (c *UserContext) ProviderName() string {
switch c.Provider { switch c.Provider {
case ProviderBasicAuth, ProviderLocal: case ProviderBasicAuth, ProviderLocal:
return "local" return "local"
case ProviderLDAP: case ProviderLDAP:
return "ldap" return "ldap"
case ProviderOAuth: case ProviderOAuth:
return c.OAuth.ID return c.OAuth.DisplayName // compatability
default: default:
return "unknown" return "unknown"
} }
+7 -7
View File
@@ -153,29 +153,29 @@ func TestContext(t *testing.T) {
{ {
description: "ProviderName returns 'local' for ProviderLocal", description: "ProviderName returns 'local' for ProviderLocal",
context: &model.UserContext{Provider: model.ProviderLocal}, context: &model.UserContext{Provider: model.ProviderLocal},
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() }, run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
expected: "local", expected: "local",
}, },
{ {
description: "ProviderName returns 'local' for ProviderBasicAuth", description: "ProviderName returns 'local' for ProviderBasicAuth",
context: &model.UserContext{Provider: model.ProviderBasicAuth}, context: &model.UserContext{Provider: model.ProviderBasicAuth},
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() }, run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
expected: "local", expected: "local",
}, },
{ {
description: "ProviderName returns 'ldap' for ProviderLDAP", description: "ProviderName returns 'ldap' for ProviderLDAP",
context: &model.UserContext{Provider: model.ProviderLDAP}, context: &model.UserContext{Provider: model.ProviderLDAP},
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() }, run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
expected: "ldap", expected: "ldap",
}, },
{ {
description: "ProviderName returns OAuth provider ID for ProviderOAuth", description: "ProviderName returns OAuth DisplayName for ProviderOAuth",
context: &model.UserContext{ context: &model.UserContext{
Provider: model.ProviderOAuth, Provider: model.ProviderOAuth,
OAuth: &model.OAuthContext{ID: "github"}, OAuth: &model.OAuthContext{DisplayName: "GitHub"},
}, },
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() }, run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
expected: "github", expected: "GitHub",
}, },
{ {
description: "TOTPPending returns true when local context is pending", description: "TOTPPending returns true when local context is pending",
-14
View File
@@ -84,7 +84,6 @@ type AuthServiceConfig struct {
SessionCookieName string SessionCookieName string
IP model.IPConfig IP model.IPConfig
LDAPGroupsCacheTTL int LDAPGroupsCacheTTL int
SubdomainsEnabled bool
} }
type AuthService struct { type AuthService struct {
@@ -398,12 +397,6 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway") tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
} }
err = auth.queries.DeleteSession(ctx, uuid)
if err != nil {
return nil, err
}
return &http.Cookie{ return &http.Cookie{
Name: auth.config.SessionCookieName, Name: auth.config.SessionCookieName,
Value: "", Value: "",
@@ -845,10 +838,3 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() {
} }
auth.loginMutex.Unlock() auth.loginMutex.Unlock()
} }
func (auth *AuthService) getCookieDomain() string {
if auth.config.SubdomainsEnabled {
return "." + auth.config.CookieDomain
}
return auth.config.CookieDomain
}
-9
View File
@@ -47,15 +47,6 @@ func GetCookieDomain(u string) (string, error) {
return domain, nil return domain, nil
} }
func GetStandaloneCookieDomain(u string) (string, error) {
parsed, err := url.Parse(u)
if err != nil {
return "", err
}
return parsed.Hostname(), nil
}
func ParseFileToLine(content string) string { func ParseFileToLine(content string) string {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
users := make([]string, 0) users := make([]string, 0)