Skip to content

Commit ca6a7fa

Browse files
feat: add option to run tinyauth on a top-level domain (#710)
* 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. * fix: inform services and controllers if subdomain cookie domain is enabled * chore: rabbit feedback * fix: deny ip addresses for standalone domain --------- Co-authored-by: Stavros <steveiliop56@gmail.com>
1 parent 1382ab4 commit ca6a7fa

8 files changed

Lines changed: 103 additions & 5 deletions

File tree

internal/bootstrap/app_bootstrap.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ func (app *BootstrapApp) Setup() error {
104104
}
105105

106106
// Get cookie domain
107-
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
107+
cookieDomainResolver := utils.GetCookieDomain
108+
if !app.config.Auth.SubdomainsEnabled {
109+
tlog.App.Info().Msg("Subdomains disabled, automatic authentication for proxied apps will not work")
110+
cookieDomainResolver = utils.GetStandaloneCookieDomain
111+
}
112+
113+
cookieDomain, err := cookieDomainResolver(app.context.appUrl)
108114

109115
if err != nil {
110116
return err

internal/bootstrap/router_bootstrap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
8484
RedirectCookieName: app.context.redirectCookieName,
8585
CookieDomain: app.context.cookieDomain,
8686
OAuthSessionCookieName: app.context.oauthSessionCookieName,
87+
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
8788
}, apiRouter, app.services.authService)
8889

8990
oauthController.SetupRoutes()

internal/bootstrap/service_bootstrap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
100100
SessionCookieName: app.context.sessionCookieName,
101101
IP: app.config.Auth.IP,
102102
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
103+
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
103104
}, services.ldapService, queries, services.oauthBrokerService)
104105

105106
err = authService.Init()

internal/controller/oauth_controller.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type OAuthControllerConfig struct {
2626
SecureCookie bool
2727
AppURL string
2828
CookieDomain string
29+
SubdomainsEnabled bool
2930
}
3031

3132
type OAuthController struct {
@@ -105,7 +106,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
105106
return
106107
}
107108

108-
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
109+
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
109110

110111
c.JSON(200, gin.H{
111112
"status": 200,
@@ -135,7 +136,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
135136
return
136137
}
137138

138-
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
139+
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
139140

140141
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
141142

@@ -283,3 +284,10 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams)
283284
params.ClientID != "" &&
284285
params.RedirectURI != ""
285286
}
287+
288+
func (controller *OAuthController) getCookieDomain() string {
289+
if controller.config.SubdomainsEnabled {
290+
return "." + controller.config.CookieDomain
291+
}
292+
return controller.config.CookieDomain
293+
}

internal/model/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func NewDefaultConfiguration() *Config {
1818
Address: "0.0.0.0",
1919
},
2020
Auth: AuthConfig{
21+
SubdomainsEnabled: true,
2122
SessionExpiry: 86400, // 1 day
2223
SessionMaxLifetime: 0, // disabled
2324
LoginTimeout: 300, // 5 minutes
@@ -102,6 +103,7 @@ type ServerConfig struct {
102103
type AuthConfig struct {
103104
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
104105
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
106+
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
105107
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
106108
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
107109
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`

internal/service/auth_service.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type AuthServiceConfig struct {
8484
SessionCookieName string
8585
IP model.IPConfig
8686
LDAPGroupsCacheTTL int
87+
SubdomainsEnabled bool
8788
}
8889

8990
type AuthService struct {
@@ -397,6 +398,12 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
397398
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
398399
}
399400

401+
err = auth.queries.DeleteSession(ctx, uuid)
402+
403+
if err != nil {
404+
return nil, err
405+
}
406+
400407
return &http.Cookie{
401408
Name: auth.config.SessionCookieName,
402409
Value: "",
@@ -838,3 +845,10 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() {
838845
}
839846
auth.loginMutex.Unlock()
840847
}
848+
849+
func (auth *AuthService) getCookieDomain() string {
850+
if auth.config.SubdomainsEnabled {
851+
return "." + auth.config.CookieDomain
852+
}
853+
return auth.config.CookieDomain
854+
}

internal/utils/app_utils.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func GetCookieDomain(u string) (string, error) {
2222
host := parsed.Hostname()
2323

2424
if netIP := net.ParseIP(host); netIP != nil {
25-
return "", errors.New("IP addresses not allowed")
25+
return "", errors.New("ip addresses not allowed")
2626
}
2727

2828
parts := strings.Split(host, ".")
@@ -47,6 +47,27 @@ func GetCookieDomain(u string) (string, error) {
4747
return domain, nil
4848
}
4949

50+
func GetStandaloneCookieDomain(u string) (string, error) {
51+
parsed, err := url.Parse(u)
52+
if err != nil {
53+
return "", err
54+
}
55+
56+
host := parsed.Hostname()
57+
58+
if netIP := net.ParseIP(host); netIP != nil {
59+
return "", errors.New("ip addresses not allowed")
60+
}
61+
62+
parts := strings.Split(host, ".")
63+
64+
if len(parts) < 2 {
65+
return "", errors.New("invalid app url")
66+
}
67+
68+
return host, nil
69+
}
70+
5071
func ParseFileToLine(content string) string {
5172
lines := strings.Split(content, "\n")
5273
users := make([]string, 0)

internal/utils/app_utils_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestGetRootDomain(t *testing.T) {
3030
// IP address
3131
domain = "http://10.10.10.10"
3232
_, err = utils.GetCookieDomain(domain)
33-
assert.ErrorContains(t, err, "IP addresses not allowed")
33+
assert.ErrorContains(t, err, "ip addresses not allowed")
3434

3535
// Invalid URL
3636
domain = "http://[::1]:namedport"
@@ -180,3 +180,48 @@ func TestIsRedirectSafe(t *testing.T) {
180180
result = utils.IsRedirectSafe(redirectURL, domain)
181181
assert.False(t, result)
182182
}
183+
184+
func TestGetStandaloneCookieDomain(t *testing.T) {
185+
// Normal case
186+
domain := "http://tinyauth.app"
187+
expected := "tinyauth.app"
188+
result, err := utils.GetStandaloneCookieDomain(domain)
189+
assert.NoError(t, err)
190+
assert.Equal(t, expected, result)
191+
192+
// URL with subdomain (full hostname is returned, no subdomain stripping)
193+
domain = "http://sub.tinyauth.app"
194+
expected = "sub.tinyauth.app"
195+
result, err = utils.GetStandaloneCookieDomain(domain)
196+
assert.NoError(t, err)
197+
assert.Equal(t, expected, result)
198+
199+
// URL with port (port should be stripped)
200+
domain = "http://tinyauth.app:8080"
201+
expected = "tinyauth.app"
202+
result, err = utils.GetStandaloneCookieDomain(domain)
203+
assert.NoError(t, err)
204+
assert.Equal(t, expected, result)
205+
206+
// URL with path
207+
domain = "https://tinyauth.app/some/path"
208+
expected = "tinyauth.app"
209+
result, err = utils.GetStandaloneCookieDomain(domain)
210+
assert.NoError(t, err)
211+
assert.Equal(t, expected, result)
212+
213+
// IP address
214+
domain = "http://10.10.10.10"
215+
_, err = utils.GetStandaloneCookieDomain(domain)
216+
assert.ErrorContains(t, err, "ip addresses not allowed")
217+
218+
// Invalid domain (only TLD)
219+
domain = "com"
220+
_, err = utils.GetStandaloneCookieDomain(domain)
221+
assert.ErrorContains(t, err, "invalid app url")
222+
223+
// Invalid URL
224+
domain = "http://[::1]:namedport"
225+
_, err = utils.GetStandaloneCookieDomain(domain)
226+
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
227+
}

0 commit comments

Comments
 (0)