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
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
sarif_file: results.sarif
+1 -7
View File
@@ -104,13 +104,7 @@ func (app *BootstrapApp) Setup() error {
}
// Get cookie domain
cookieDomainResolver := utils.GetCookieDomain
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)
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
if err != nil {
return err
-1
View File
@@ -84,7 +84,6 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
RedirectCookieName: app.context.redirectCookieName,
CookieDomain: app.context.cookieDomain,
OAuthSessionCookieName: app.context.oauthSessionCookieName,
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
}, apiRouter, app.services.authService)
oauthController.SetupRoutes()
-1
View File
@@ -100,7 +100,6 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
SessionCookieName: app.context.sessionCookieName,
IP: app.config.Auth.IP,
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
}, services.ldapService, queries, services.oauthBrokerService)
err = authService.Init()
+1 -1
View File
@@ -95,7 +95,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
Username: context.GetUsername(),
Name: context.GetName(),
Email: context.GetEmail(),
Provider: context.GetProviderID(),
Provider: context.ProviderName(),
OAuth: context.IsOAuth(),
TOTPPending: context.TOTPPending(),
OAuthName: context.OAuthName(),
+2 -10
View File
@@ -26,7 +26,6 @@ type OAuthControllerConfig struct {
SecureCookie bool
AppURL string
CookieDomain string
SubdomainsEnabled bool
}
type OAuthController struct {
@@ -106,7 +105,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
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{
"status": 200,
@@ -136,7 +135,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
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)
@@ -284,10 +283,3 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams)
params.ClientID != "" &&
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",
Email: "totpuser@example.com",
},
TOTPEnabled: true,
},
})
c.Next()
+1 -1
View File
@@ -249,7 +249,7 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
context, err := new(model.UserContext).NewFromGin(c)
if err == nil {
tlog.AuditLogout(c, context.GetUsername(), context.GetProviderID())
tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
} else {
tlog.App.Warn().Err(err).Msg("Failed to get user context for logout audit, proceeding without username")
tlog.AuditLogout(c, "unknown", "unknown")
+5 -7
View File
@@ -78,6 +78,7 @@ func TestUserController(t *testing.T) {
Email: "totpuser@example.com",
},
TOTPPending: true,
TOTPEnabled: true,
},
})
}
@@ -93,6 +94,7 @@ func TestUserController(t *testing.T) {
Email: "bob@example.com",
},
TOTPPending: true,
TOTPEnabled: true,
},
})
}
@@ -150,9 +152,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain)
// 3 seconds should be more than enough for even slow test environments
assert.GreaterOrEqual(t, cookie.MaxAge, 7)
assert.LessOrEqual(t, cookie.MaxAge, 10)
assert.Equal(t, 9, cookie.MaxAge)
},
},
{
@@ -241,8 +241,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain)
assert.GreaterOrEqual(t, cookie.MaxAge, 3597)
assert.LessOrEqual(t, cookie.MaxAge, 3600)
assert.Equal(t, 3599, cookie.MaxAge) // 1 hour, default for totp pending sessions
},
},
{
@@ -336,8 +335,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", totpCookie.Name)
assert.True(t, totpCookie.HttpOnly)
assert.Equal(t, "example.com", totpCookie.Domain)
assert.GreaterOrEqual(t, totpCookie.MaxAge, 7)
assert.LessOrEqual(t, totpCookie.MaxAge, 10)
assert.Equal(t, 9, totpCookie.MaxAge) // should use the regular session expiry time
},
},
{
@@ -123,6 +123,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
if userContext.Provider == model.ProviderLocal &&
userContext.Local.TOTPPending {
userContext.Local.TOTPEnabled = true
return userContext, nil, nil
}
@@ -109,6 +109,7 @@ func TestContextMiddleware(t *testing.T) {
assert.Equal(t, "testuser", userCtx.GetUsername())
assert.True(t, userCtx.Authenticated)
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)
require.NotNil(t, userCtx.Local)
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",
},
Auth: AuthConfig{
SubdomainsEnabled: true,
SessionExpiry: 86400, // 1 day
SessionMaxLifetime: 0, // disabled
LoginTimeout: 300, // 5 minutes
@@ -103,7 +102,6 @@ type ServerConfig struct {
type AuthConfig struct {
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
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"`
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
+3 -2
View File
@@ -34,6 +34,7 @@ type BaseContext struct {
type LocalContext struct {
BaseContext
TOTPPending bool
TOTPEnabled bool
Attributes UserAttributes
}
@@ -222,14 +223,14 @@ func (c *UserContext) GetName() string {
}
}
func (c *UserContext) GetProviderID() string {
func (c *UserContext) ProviderName() string {
switch c.Provider {
case ProviderBasicAuth, ProviderLocal:
return "local"
case ProviderLDAP:
return "ldap"
case ProviderOAuth:
return c.OAuth.ID
return c.OAuth.DisplayName // compatability
default:
return "unknown"
}
+7 -7
View File
@@ -153,29 +153,29 @@ func TestContext(t *testing.T) {
{
description: "ProviderName returns 'local' for 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",
},
{
description: "ProviderName returns 'local' for 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",
},
{
description: "ProviderName returns 'ldap' for 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",
},
{
description: "ProviderName returns OAuth provider ID for ProviderOAuth",
description: "ProviderName returns OAuth DisplayName for ProviderOAuth",
context: &model.UserContext{
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() },
expected: "github",
run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
expected: "GitHub",
},
{
description: "TOTPPending returns true when local context is pending",
-14
View File
@@ -84,7 +84,6 @@ type AuthServiceConfig struct {
SessionCookieName string
IP model.IPConfig
LDAPGroupsCacheTTL int
SubdomainsEnabled bool
}
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")
}
err = auth.queries.DeleteSession(ctx, uuid)
if err != nil {
return nil, err
}
return &http.Cookie{
Name: auth.config.SessionCookieName,
Value: "",
@@ -845,10 +838,3 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() {
}
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
}
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 {
lines := strings.Split(content, "\n")
users := make([]string, 0)