From d90e3d652d410788f67c906967743e28a649a59a Mon Sep 17 00:00:00 2001 From: Jacek Kowalski Date: Thu, 12 Mar 2026 18:34:49 +0100 Subject: [PATCH 1/4] Add TINYAUTH_AUTH_SUBDOMAINSENABLED option Setting it to false allows to use Tinyauth on top-level domain only, but forbids automatic cross-app authentication using Traefik/Nginx. --- internal/bootstrap/app_bootstrap.go | 8 +++++++- internal/config/config.go | 2 ++ internal/utils/app_utils.go | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 18d9068b..55c0f477 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -106,7 +106,13 @@ func (app *BootstrapApp) Setup() error { } // Get cookie domain - cookieDomain, err := utils.GetCookieDomain(app.context.appUrl) + 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) if err != nil { return err diff --git a/internal/config/config.go b/internal/config/config.go index b8db08a9..f9280f8e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ func NewDefaultConfiguration() *Config { Address: "0.0.0.0", }, Auth: AuthConfig{ + SubdomainsEnabled: true, SessionExpiry: 86400, // 1 day SessionMaxLifetime: 0, // disabled LoginTimeout: 300, // 5 minutes @@ -116,6 +117,7 @@ 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"` UsersFile string `description:"Path to the users file." yaml:"usersFile"` + SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"` 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"` diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index c5055e36..0cbc16eb 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -49,6 +49,15 @@ 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) From 44a7cbf41baf25524035821fec5834cb1c86e7e0 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 29 Apr 2026 15:49:45 +0300 Subject: [PATCH 2/4] fix: inform services and controllers if subdomain cookie domain is enabled --- internal/bootstrap/service_bootstrap.go | 1 + internal/controller/oauth_controller.go | 12 ++++++++++-- internal/service/auth_service.go | 14 +++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index 7bd4a620..a6d518e6 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -80,6 +80,7 @@ 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, }, dockerService, services.ldapService, queries, services.oauthBrokerService) err = authService.Init() diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index aa116134..f36e269d 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -27,6 +27,7 @@ type OAuthControllerConfig struct { SecureCookie bool AppURL string CookieDomain string + SubdomainsEnabled bool } type OAuthController struct { @@ -106,7 +107,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { return } - c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.SecureCookie, true) c.JSON(200, gin.H{ "status": 200, @@ -136,7 +137,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.SecureCookie, true) oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie) @@ -282,3 +283,10 @@ 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 +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 807d39c5..46758572 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -78,6 +78,7 @@ type AuthServiceConfig struct { SessionCookieName string IP config.IPConfig LDAPGroupsCacheTTL int + SubdomainsEnabled bool } type AuthService struct { @@ -327,7 +328,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Se return err } - c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", auth.getCookieDomain(), auth.config.SecureCookie, true) return nil } @@ -378,7 +379,7 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error { return err } - c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", auth.getCookieDomain(), auth.config.SecureCookie, true) tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed") return nil @@ -397,7 +398,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { return err } - c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, "", -1, "/", auth.getCookieDomain(), auth.config.SecureCookie, true) return nil } @@ -834,3 +835,10 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() { } auth.loginMutex.Unlock() } + +func (auth *AuthService) getCookieDomain() string { + if auth.config.SubdomainsEnabled { + return "." + auth.config.CookieDomain + } + return auth.config.CookieDomain +} From 4077bacfdfb45b603de20fd5581f4a9ca0097a6d Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 29 Apr 2026 16:00:59 +0300 Subject: [PATCH 3/4] chore: rabbit feedback --- internal/bootstrap/router_bootstrap.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 91d36ac2..d1e213fb 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -83,6 +83,7 @@ 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() From 49e38ebeb003c932938d18b54ee00e9db561901a Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 7 May 2026 16:03:15 +0300 Subject: [PATCH 4/4] fix: deny ip addresses for standalone domain --- internal/utils/app_utils.go | 16 +++++++++-- internal/utils/app_utils_test.go | 47 +++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index c7568570..d021c083 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -22,7 +22,7 @@ func GetCookieDomain(u string) (string, error) { host := parsed.Hostname() if netIP := net.ParseIP(host); netIP != nil { - return "", errors.New("IP addresses not allowed") + return "", errors.New("ip addresses not allowed") } parts := strings.Split(host, ".") @@ -53,7 +53,19 @@ func GetStandaloneCookieDomain(u string) (string, error) { return "", err } - return parsed.Hostname(), nil + host := parsed.Hostname() + + if netIP := net.ParseIP(host); netIP != nil { + return "", errors.New("ip addresses not allowed") + } + + parts := strings.Split(host, ".") + + if len(parts) < 2 { + return "", errors.New("invalid app url") + } + + return host, nil } func ParseFileToLine(content string) string { diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index 46dacafc..6554fad8 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -30,7 +30,7 @@ func TestGetRootDomain(t *testing.T) { // IP address domain = "http://10.10.10.10" _, err = utils.GetCookieDomain(domain) - assert.ErrorContains(t, err, "IP addresses not allowed") + assert.ErrorContains(t, err, "ip addresses not allowed") // Invalid URL domain = "http://[::1]:namedport" @@ -180,3 +180,48 @@ func TestIsRedirectSafe(t *testing.T) { result = utils.IsRedirectSafe(redirectURL, domain) assert.False(t, result) } + +func TestGetStandaloneCookieDomain(t *testing.T) { + // Normal case + domain := "http://tinyauth.app" + expected := "tinyauth.app" + result, err := utils.GetStandaloneCookieDomain(domain) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // URL with subdomain (full hostname is returned, no subdomain stripping) + domain = "http://sub.tinyauth.app" + expected = "sub.tinyauth.app" + result, err = utils.GetStandaloneCookieDomain(domain) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // URL with port (port should be stripped) + domain = "http://tinyauth.app:8080" + expected = "tinyauth.app" + result, err = utils.GetStandaloneCookieDomain(domain) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // URL with path + domain = "https://tinyauth.app/some/path" + expected = "tinyauth.app" + result, err = utils.GetStandaloneCookieDomain(domain) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // IP address + domain = "http://10.10.10.10" + _, err = utils.GetStandaloneCookieDomain(domain) + assert.ErrorContains(t, err, "ip addresses not allowed") + + // Invalid domain (only TLD) + domain = "com" + _, err = utils.GetStandaloneCookieDomain(domain) + assert.ErrorContains(t, err, "invalid app url") + + // Invalid URL + domain = "http://[::1]:namedport" + _, err = utils.GetStandaloneCookieDomain(domain) + assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host") +}