tests: fix tests

This commit is contained in:
Stavros
2026-06-21 17:15:09 +03:00
parent d87be8676c
commit a4f9c897a6
7 changed files with 185 additions and 152 deletions
+2 -2
View File
@@ -166,7 +166,7 @@ func (app *BootstrapApp) Setup() error {
app.log.App.Warn().Msg("Subdomains are disabled, cookies will be set for the current domain only") app.log.App.Warn().Msg("Subdomains are disabled, cookies will be set for the current domain only")
} }
cookieDomain, err := utils.GetCookieDomain(app.runtime.AppURL) cookieDomain, err := utils.GetCookieDomain(app.runtime.AppURL, app.config.Auth.SubdomainsEnabled)
if err != nil { if err != nil {
return fmt.Errorf("failed to get cookie domain: %w", err) return fmt.Errorf("failed to get cookie domain: %w", err)
@@ -288,7 +288,7 @@ func (app *BootstrapApp) Setup() error {
app.log.App.Info().Msg("Listening on tailscale, replacing app url with tailscale hostname") app.log.App.Info().Msg("Listening on tailscale, replacing app url with tailscale hostname")
app.runtime.AppURL = tailscaleUrl app.runtime.AppURL = tailscaleUrl
// also update cookie domain // also update cookie domain
cookieDomain, err := utils.GetCookieDomain(tailscaleUrl) cookieDomain, err := utils.GetCookieDomain(tailscaleUrl, app.config.Auth.SubdomainsEnabled)
if err != nil { if err != nil {
return fmt.Errorf("failed to get cookie domain: %w", err) return fmt.Errorf("failed to get cookie domain: %w", err)
@@ -48,9 +48,9 @@ func TestContextController(t *testing.T) {
WarningsEnabled: cfg.UI.WarningsEnabled, WarningsEnabled: cfg.UI.WarningsEnabled,
}, },
App: ACRApp{ App: ACRApp{
AppURL: runtime.AppURL, AppURL: runtime.AppURL,
CookieDomain: runtime.CookieDomain, CookieDomain: runtime.CookieDomain,
TrustedDomains: runtime.TrustedDomains, SubdomainsEnabled: cfg.Auth.SubdomainsEnabled,
}, },
} }
bytes, err := json.Marshal(expectedAppContextResponse) bytes, err := json.Marshal(expectedAppContextResponse)
+127 -101
View File
@@ -9,7 +9,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
func TestOAuthController(t *testing.T) { func TestOAuthControllerIsRedirectSafe(t *testing.T) {
log := logger.NewLogger().WithTestConfig() log := logger.NewLogger().WithTestConfig()
log.Init() log.Init()
@@ -17,145 +17,171 @@ func TestOAuthController(t *testing.T) {
type testCase struct { type testCase struct {
description string description string
run func(ctrl *OAuthController) appURL string
trustedDomains []string cookieDomain string
subdomainsEnabled bool subdomainsEnabled bool
redirectURI string
expected bool
} }
tests := []testCase{ tests := []testCase{
{ {
description: "Test exact match of redirect URI", description: "Exact host match returns true",
trustedDomains: []string{"https://tinyauth.example.com"}, appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
run: func(ctrl *OAuthController) { redirectURI: "https://tinyauth.example.com",
redirectUri := "https://tinyauth.example.com" expected: true,
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Test subdomain match of redirect URI", description: "Exact host match is case insensitive",
trustedDomains: []string{"https://tinyauth.example.com"}, appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
run: func(ctrl *OAuthController) { redirectURI: "https://TinyAuth.Example.com",
redirectUri := "https://sub.example.com" expected: true,
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Test different trusted domain", description: "Exact host match with subdomains disabled returns true",
trustedDomains: []string{"https://tinyauth.example.com", "https://tinyauth.foo.com"}, appURL: "https://tinyauth.example.com",
subdomainsEnabled: true, cookieDomain: "example.com",
run: func(ctrl *OAuthController) {
redirectUri := "https://app.foo.com"
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test invalid redirect URI",
run: func(ctrl *OAuthController) {
redirectUri := "https:/malicious"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test empty redirect URI",
run: func(ctrl *OAuthController) {
redirectUri := ""
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test redirect URI with different scheme",
trustedDomains: []string{"https://tinyauth.example.com"},
subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "http://tinyauth.example.com"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test redirect URI with different port",
trustedDomains: []string{"https://tinyauth.example.com"},
subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "https://tinyauth.example.com:8080"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
// weird case, subdomains enabled and domain without subdomain can't happen
description: "Test with trusted domain that's in PSL when split",
trustedDomains: []string{"https://example.com"}, // will become .com which we
// obviously don't want to allow
subdomainsEnabled: true,
run: func(ctrl *OAuthController) {
redirectUri := "https://sub.example.com"
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
},
{
description: "Test subdomain redirect URI when subdomains are disabled",
trustedDomains: []string{"https://tinyauth.example.com"},
subdomainsEnabled: false, subdomainsEnabled: false,
run: func(ctrl *OAuthController) { redirectURI: "https://tinyauth.example.com",
redirectUri := "https://sub.tinyauth.example.com" expected: true,
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Test domain like the .co.uk", description: "Subdomain of cookie domain returns true when subdomains enabled",
trustedDomains: []string{"https://example.co.uk"}, appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
run: func(ctrl *OAuthController) { redirectURI: "https://sub.example.com",
redirectUri := "https://sub.example.co.uk" expected: true,
assert.False(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Test domain like the .co.uk with subdomains disabled", description: "Subdomain of cookie domain is case insensitive",
trustedDomains: []string{"https://example.co.uk"}, appURL: "https://tinyauth.example.com",
cookieDomain: "Example.COM",
subdomainsEnabled: true,
redirectURI: "https://SUB.example.com",
expected: true,
},
{
description: "Subdomain not matching cookie domain returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://sub.evil.com",
expected: false,
},
{
description: "Subdomain returns false when subdomains disabled",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: false, subdomainsEnabled: false,
run: func(ctrl *OAuthController) { redirectURI: "https://sub.example.com",
redirectUri := "https://example.co.uk" expected: false,
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Test caps domain", description: "Cookie domain itself is not a subdomain match",
trustedDomains: []string{"https://TINYAUTH.ExAmpLe.com"}, appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
run: func(ctrl *OAuthController) { redirectURI: "https://example.com",
redirectUri := "https://sUb.ExAmPle.com" expected: false,
assert.True(t, ctrl.isRedirectSafe(redirectUri))
},
}, },
{ {
description: "Test edge case with @", description: "Different scheme returns false",
trustedDomains: []string{"https://tinyauth.example.com"}, appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true, subdomainsEnabled: true,
run: func(ctrl *OAuthController) { redirectURI: "http://tinyauth.example.com",
redirectUri := "https://malicious.example.com@evil.com" expected: false,
assert.False(t, ctrl.isRedirectSafe(redirectUri)) },
}, {
description: "Different port returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://tinyauth.example.com:8080",
expected: false,
},
{
description: "Empty redirect URI returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "",
expected: false,
},
{
description: "Redirect URI without host returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https:/malicious",
expected: false,
},
{
description: "Redirect URI without scheme returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "tinyauth.example.com",
expected: false,
},
{
description: "Relative redirect URI returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "/some/path",
expected: false,
},
{
description: "Userinfo trick with malicious host returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://malicious.example.com@evil.com",
expected: false,
},
{
description: "Unparseable redirect URI returns false",
appURL: "https://tinyauth.example.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://exa\x7fmple.com",
expected: false,
},
{
description: "Unparseable app URL returns false",
appURL: "https://tinyauth.\x7fexample.com",
cookieDomain: "example.com",
subdomainsEnabled: true,
redirectURI: "https://tinyauth.example.com",
expected: false,
}, },
} }
// TODO: add auth service
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
router := gin.Default() router := gin.Default()
group := router.Group("/api") group := router.Group("/api")
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
// overwrite the trusted domains and subdomain setting for each test case
runtime.TrustedDomains = tc.trustedDomains // Overwrite the app URL, cookie domain and subdomain setting for each test case
runtime.AppURL = tc.appURL
runtime.CookieDomain = tc.cookieDomain
cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled
ctrl := NewOAuthController(OAuthControllerInput{ ctrl := NewOAuthController(OAuthControllerInput{
Log: log, Log: log,
Config: &cfg, Config: &cfg,
RuntimeConfig: &runtime, RuntimeConfig: &runtime,
RouterGroup: group, RouterGroup: group,
}) })
tc.run(ctrl)
assert.Equal(t, tc.expected, ctrl.isRedirectSafe(tc.redirectURI))
}) })
} }
} }
+10 -25
View File
@@ -380,33 +380,11 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
return nil, fmt.Errorf("failed to create session entry: %w", err) return nil, fmt.Errorf("failed to create session entry: %w", err)
} }
if data.Provider == "tailscale" {
auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname")
tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname()))
if err != nil {
return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err)
}
return &http.Cookie{
Name: auth.runtime.SessionCookieName,
Value: session.UUID,
Path: "/",
Domain: fmt.Sprintf(".%s", tsCookieDomain),
Expires: expiresAt,
MaxAge: int(time.Until(expiresAt).Seconds()),
Secure: auth.config.Auth.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
return &http.Cookie{ return &http.Cookie{
Name: auth.runtime.SessionCookieName, Name: auth.runtime.SessionCookieName,
Value: session.UUID, Value: session.UUID,
Path: "/", Path: "/",
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain), Domain: auth.getCookieDomain(),
Expires: expiresAt, Expires: expiresAt,
MaxAge: int(time.Until(expiresAt).Seconds()), MaxAge: int(time.Until(expiresAt).Seconds()),
Secure: auth.config.Auth.SecureCookie, Secure: auth.config.Auth.SecureCookie,
@@ -459,7 +437,7 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http
Name: auth.runtime.SessionCookieName, Name: auth.runtime.SessionCookieName,
Value: session.UUID, Value: session.UUID,
Path: "/", Path: "/",
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain), Domain: auth.getCookieDomain(),
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second), Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
MaxAge: int(newExpiry - currentTime), MaxAge: int(newExpiry - currentTime),
Secure: auth.config.Auth.SecureCookie, Secure: auth.config.Auth.SecureCookie,
@@ -480,7 +458,7 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
Name: auth.runtime.SessionCookieName, Name: auth.runtime.SessionCookieName,
Value: "", Value: "",
Path: "/", Path: "/",
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain), Domain: auth.getCookieDomain(),
Expires: time.Now(), Expires: time.Now(),
MaxAge: -1, MaxAge: -1,
Secure: auth.config.Auth.SecureCookie, Secure: auth.config.Auth.SecureCookie,
@@ -726,3 +704,10 @@ func (auth *AuthService) calculateLockdownLimit() int {
return limit return limit
} }
func (auth *AuthService) getCookieDomain() string {
if auth.config.Auth.SubdomainsEnabled {
return "." + auth.runtime.CookieDomain
}
return auth.runtime.CookieDomain
}
+1 -4
View File
@@ -43,6 +43,7 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
ACLs: model.ACLsConfig{ ACLs: model.ACLsConfig{
Policy: "allow", Policy: "allow",
}, },
SubdomainsEnabled: true,
}, },
Database: model.DatabaseConfig{ Database: model.DatabaseConfig{
Path: filepath.Join(tempDir, "test.db"), Path: filepath.Join(tempDir, "test.db"),
@@ -165,10 +166,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
CookieDomain: "example.com", CookieDomain: "example.com",
AppURL: "https://tinyauth.example.com", AppURL: "https://tinyauth.example.com",
SessionCookieName: "tinyauth-session", SessionCookieName: "tinyauth-session",
TrustedDomains: []string{
"https://tinyauth.example.com",
"https://tinyauth.foo.com",
},
} }
return config, runtime return config, runtime
+11 -7
View File
@@ -10,7 +10,7 @@ import (
) )
// Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) // Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
func GetCookieDomain(appUrl string) (string, error) { func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
u, err := url.Parse(appUrl) u, err := url.Parse(appUrl)
if err != nil { if err != nil {
@@ -29,12 +29,16 @@ func GetCookieDomain(appUrl string) (string, error) {
return "", fmt.Errorf("invalid app url, must be in format subdomain.domain.tld or domain.tld") return "", fmt.Errorf("invalid app url, must be in format subdomain.domain.tld or domain.tld")
} }
if len(parts) == 2 { if !subdomainsEnabled || len(parts) == 2 {
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, hostname, nil)
if err != nil {
return "", fmt.Errorf("domain in public suffix list, cannot set cookies: %w", err)
}
return strings.ToLower(u.Host), nil return strings.ToLower(u.Host), nil
} }
// parts > 3
domain := strings.Join(parts[1:], ".") domain := strings.Join(parts[1:], ".")
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil) _, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil)
@@ -44,10 +48,10 @@ func GetCookieDomain(appUrl string) (string, error) {
} }
// now that we validated the domain, return with the port // now that we validated the domain, return with the port
parts = strings.Split(strings.ToLower(u.Host), ":") parts = strings.Split(strings.ToLower(u.Host), ".")
domainWithPort := strings.Join(parts[1:], ":") host := strings.Join(parts[1:], ".")
return domainWithPort, nil return host, nil
} }
func ParseFileToLine(content string) string { func ParseFileToLine(content string) string {
+31 -10
View File
@@ -11,50 +11,71 @@ func TestGetRootDomain(t *testing.T) {
// Normal case // Normal case
domain := "http://sub.tinyauth.app" domain := "http://sub.tinyauth.app"
expected := "tinyauth.app" expected := "tinyauth.app"
result, err := utils.GetCookieDomain(domain) result, err := utils.GetCookieDomain(domain, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Domain with multiple subdomains // Domain with multiple subdomains
domain = "http://b.c.tinyauth.app" domain = "http://b.c.tinyauth.app"
expected = "c.tinyauth.app" expected = "c.tinyauth.app"
result, err = utils.GetCookieDomain(domain) result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Invalid domain (only TLD) // Invalid domain (only TLD)
domain = "com" domain = "com"
_, err = utils.GetCookieDomain(domain) _, err = utils.GetCookieDomain(domain, true)
assert.ErrorContains(t, err, "invalid app url, must be at least second level domain") assert.EqualError(t, err, "invalid app url, must be in format subdomain.domain.tld or domain.tld")
// IP address // IP address
domain = "http://10.10.10.10" domain = "http://10.10.10.10"
_, err = utils.GetCookieDomain(domain) _, err = utils.GetCookieDomain(domain, true)
assert.ErrorContains(t, err, "ip addresses not allowed") assert.ErrorContains(t, err, "ip addresses not allowed")
// Invalid URL // Invalid URL
domain = "http://[::1]:namedport" domain = "http://[::1]:namedport"
_, err = utils.GetCookieDomain(domain) _, err = utils.GetCookieDomain(domain, true)
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host") assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
// URL with scheme and path // URL with scheme and path
domain = "https://sub.tinyauth.app/path" domain = "https://sub.tinyauth.app/path"
expected = "tinyauth.app" expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain) result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// URL with port // URL with port
domain = "http://sub.tinyauth.app:8080" domain = "http://sub.tinyauth.app:8080"
expected = "tinyauth.app" expected = "tinyauth.app:8080"
result, err = utils.GetCookieDomain(domain) result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Domain managed by ICANN // Domain managed by ICANN
domain = "http://example.co.uk" domain = "http://example.co.uk"
_, err = utils.GetCookieDomain(domain) _, err = utils.GetCookieDomain(domain, true)
assert.Error(t, err, "domain in public suffix list, cannot set cookies") assert.Error(t, err, "domain in public suffix list, cannot set cookies")
// Domain without subdomain
domain = "http://tinyauth.app"
expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// Case insensitivity
domain = "http://Sub.Tinyauth.App"
expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain, true)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// Subdomains disabled
domain = "http://sub.tinyauth.app"
expected = "sub.tinyauth.app"
result, err = utils.GetCookieDomain(domain, false)
assert.NoError(t, err)
assert.Equal(t, expected, result)
} }
func TestParseFileToLine(t *testing.T) { func TestParseFileToLine(t *testing.T) {