Skip to content

feat: add TINYAUTH_AUTH_SINGLECOOKIEDOMAIN option (and fix release workflow)#710

Open
jacekkow wants to merge 1 commit intotinyauthapp:mainfrom
jacekkow:single-cookie-domain
Open

feat: add TINYAUTH_AUTH_SINGLECOOKIEDOMAIN option (and fix release workflow)#710
jacekkow wants to merge 1 commit intotinyauthapp:mainfrom
jacekkow:single-cookie-domain

Conversation

@jacekkow
Copy link
Copy Markdown
Contributor

@jacekkow jacekkow commented Mar 12, 2026

It allows to set-up Tinyauth on top-level domain or to have it as a standalone OIDC server, without transparent authentication for apps in subdomains.

Additionally fix issue in release workflow where metadata (VERSION, COMMIT_HASH, BUILD_TIMESTAMP) is fetched from nightly tag rather than from the relevant v... tag.

Summary by CodeRabbit

  • New Features
    • Added an authentication setting to choose subdomain-based or standalone cookie domain handling.
    • Cookie domain resolution now adapts at runtime based on that setting, affecting how session cookies are scoped by default (subdomains enabled by default).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

Runtime cookie-domain resolution now selects between two resolver functions based on a new Auth.SubdomainsEnabled config flag; when disabled, the bootstrap switches to a standalone resolver and logs an info message before using it to set app.context.cookieDomain.

Changes

Cohort / File(s) Summary
Configuration
internal/config/config.go
Added SubdomainsEnabled bool to AuthConfig (yaml:"subdomainsEnabled") and defaulted it to true in the default configuration.
Utility Functions
internal/utils/app_utils.go
Added GetStandaloneCookieDomain(u string) (string, error) which parses the URL and returns parsed.Hostname() without public-suffix or IP rejection checks.
Bootstrap Logic
internal/bootstrap/app_bootstrap.go
In BootstrapApp.Setup, introduced a runtime-selected cookieDomainResolver (default GetCookieDomain); if Auth.SubdomainsEnabled is false, switch resolver to GetStandaloneCookieDomain and log an info message; resolved domain assigned to app.context.cookieDomain.

Sequence Diagram(s)

sequenceDiagram
    participant Bootstrap as BootstrapApp
    participant Config as Config (Auth.SubdomainsEnabled)
    participant Utils as utils package

    Bootstrap->>Config: read Auth.SubdomainsEnabled
    alt SubdomainsEnabled == true
        Bootstrap->>Utils: use GetCookieDomain(app.context.appUrl)
    else SubdomainsEnabled == false
        Bootstrap->>Utils: use GetStandaloneCookieDomain(app.context.appUrl)
    end
    Utils-->>Bootstrap: return cookieDomain / error
    Bootstrap-->>Bootstrap: set app.context.cookieDomain or return error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • steveiliop56

Poem

🐰 I nibble configs under moonlight sheen,
A flag, a resolver, cookie fields seen,
Hop! I choose hostname when subdomains rest,
Quietly setting domains — I did my best! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title mentions 'TINYAUTH_AUTH_SINGLECOOKIEDOMAIN option' but the actual implementation uses 'SubdomainsEnabled' (and appears to correspond to a 'subdomainsEnabled' configuration). The parenthetical '(and fix release workflow)' acknowledges a secondary change that was extracted to a separate PR. Clarify whether the title should reflect the implemented config field name (SubdomainsEnabled/subdomainsEnabled) or confirm the environment variable name mapping. Consider whether the release-workflow fix should be mentioned given it was moved to a separate PR.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/utils/app_utils.go (1)

46-53: Consider validating empty hostname edge case.

Unlike GetCookieDomain, this function doesn't validate for IP addresses or empty hostnames. While the relaxed validation may be intentional for single-domain mode, url.Parse can return an empty hostname for malformed URLs (e.g., "not-a-url"), which would silently return an empty string instead of an error.

💡 Optional: Add minimal validation
 func GetCookieSingleDomain(u string) (string, error) {
 	parsed, err := url.Parse(u)
 	if err != nil {
 		return "", err
 	}
 
+	hostname := parsed.Hostname()
+	if hostname == "" {
+		return "", errors.New("invalid URL: empty hostname")
+	}
+
-	return parsed.Hostname(), nil
+	return hostname, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/utils/app_utils.go` around lines 46 - 53, GetCookieSingleDomain
currently returns parsed.Hostname() even when url.Parse yields an empty hostname
(e.g., for malformed inputs); update GetCookieSingleDomain to validate the
parsed.Hostname() result after calling url.Parse and return a descriptive error
if the hostname is empty (or otherwise invalid), so callers receive an error
instead of an empty string; reference the GetCookieSingleDomain function, the
url.Parse call and parsed.Hostname() usage when implementing this check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/utils/app_utils.go`:
- Around line 46-53: GetCookieSingleDomain currently returns parsed.Hostname()
even when url.Parse yields an empty hostname (e.g., for malformed inputs);
update GetCookieSingleDomain to validate the parsed.Hostname() result after
calling url.Parse and return a descriptive error if the hostname is empty (or
otherwise invalid), so callers receive an error instead of an empty string;
reference the GetCookieSingleDomain function, the url.Parse call and
parsed.Hostname() usage when implementing this check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c33fc343-de8e-419c-a237-26836edd6bc4

📥 Commits

Reviewing files that changed from the base of the PR and between 03f13ef and 6465057.

📒 Files selected for processing (4)
  • .github/workflows/release.yml
  • internal/bootstrap/app_bootstrap.go
  • internal/config/config.go
  • internal/utils/app_utils.go
💤 Files with no reviewable changes (1)
  • .github/workflows/release.yml

@steveiliop56
Copy link
Copy Markdown
Member

#724 related

@jacekkow I believe the correct approach would be to automatically detect if we are running on a non-subdomain app URL. On start-up check what strings.Split returns. If it's 2 then we are running on the root, so we show a warning that cookies will be set for .<root> and continue as normal. If it's more than 2 we are running on a subdomain so we do the regular checks for the PSL and stuff.

@jacekkow
Copy link
Copy Markdown
Contributor Author

@steveiliop56 - that is not my use case. I want to use this app as a standalone component (auth server). I do not want it to set cookies for any other domain (esp. wildcard ones). Secured applications use OIDC and are at completely different URLs.

@steveiliop56
Copy link
Copy Markdown
Member

@jacekkow that's fine, it will not set cookies if you don't use the authentication feature. The idea is to just automatically handle root domains instead of needing another config option. Your pull request basically does this. Are you interested in implementing said feature or should I do it?

@jacekkow
Copy link
Copy Markdown
Contributor Author

it will not set cookies if you don't use the authentication feature

OIDC login does set cookies.

The idea is to just automatically handle root domains instead of needing another config option.

My instance of Tinyauth is not hosted on root domain!

So unless you've missed something my use case is different and requires a different solution than an algorithm from your first comment (#710 (comment)).

@steveiliop56
Copy link
Copy Markdown
Member

OIDC login does set cookies.

My brain was not thinking when I wrote this.

..for the rest..

Yes you are correct again, I thought your instance was on the root. Anyway in that case I guess we could have such option. But maybe under a different name? SINGLEDOMAIN is a bit confusing imo. Maybe something like TINYAUTH_AUTH_COOKIEOIDCONLY?

@jacekkow
Copy link
Copy Markdown
Contributor Author

@steveiliop56 Sure - naming can be changed.

What I also considered is having an option to just set arbitrary "cookie domain" (TINYAUTH_AUTH_COOKIEDOMAIN maybe) - and if it is not set default to the algorithm without any modifications. If wrong value is set, it's on the user :)

What would you prefer? Renaming "my option" to TINYAUTH_AUTH_COOKIEOIDCONLY (or maybe TINYAUTH_AUTH_STANDALONE?) or an option to set cookie domain?

@steveiliop56
Copy link
Copy Markdown
Member

I think the standalone idea is nice, let's change it to that. As for a cookie domain option, I think it's better to avoid adding it because it can and probably will cause confusion.

Also maybe we should add a small info log saying that authentication will not work for proxied apps with this option enabled (again to avoid future issues).

Small note: This pull request is "queued" for v5.1.0 which will come later. I am focusing on fixing some issues in a patch release and then we can start adding new features.

@jacekkow
Copy link
Copy Markdown
Contributor Author

jacekkow commented Mar 19, 2026

@steveiliop56 Sure, no problem, I can work with a fork for now. BTW - would you like to have commit 6465057 submitted as a separate PR for the fix release?

@steveiliop56
Copy link
Copy Markdown
Member

Was planning to fix this with #715, I noticed it there too. But yeah feel free to create a pull request - please check if there is any nightly leftover anywhere else -.

@jacekkow
Copy link
Copy Markdown
Contributor Author

Haven't found any more of those. PR: #725

@jacekkow jacekkow force-pushed the single-cookie-domain branch 2 times, most recently from f66ddf9 to 1848a7a Compare March 20, 2026 15:21
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 0% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.00%. Comparing base (479f165) to head (d90e3d6).

Files with missing lines Patch % Lines
internal/bootstrap/app_bootstrap.go 0.00% 5 Missing ⚠️
internal/utils/app_utils.go 0.00% 5 Missing ⚠️
internal/config/config.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #710      +/-   ##
==========================================
- Coverage   20.05%   20.00%   -0.06%     
==========================================
  Files          50       50              
  Lines        3995     4005      +10     
==========================================
  Hits          801      801              
- Misses       3121     3131      +10     
  Partials       73       73              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Rycochet
Copy link
Copy Markdown
Collaborator

Rycochet commented Apr 7, 2026

I don't like the naming of this option - it doesn't explain exactly what it does and has connotations that don't match, instead how about using the same pattern as analytics with a default-true value that explains it directly and call it Subdomains instead?

TINYAUTH_AUTH_SUBDOMAINS - and make it clear in the description (or docs) that this is "deeper subdomains" so it will work from whatever level you specify?

(The option itself is awesome though!)

@jacekkow
Copy link
Copy Markdown
Contributor Author

jacekkow commented Apr 7, 2026

@steveiliop56 - should I do one more rename to TINYAUTH_AUTH_SUBDOMAINS?

@steveiliop56
Copy link
Copy Markdown
Member

@jacekkow I believe @Rycochet is correct standalone implies it runs without any additional software. How about TINYAUTH_AUTH_SUBDOMAINSENABLED which defaults to true? I think it makes the most sense?

naming things is so unbelievably hard

Setting it to false allows to use Tinyauth on top-level domain only,
but forbids automatic cross-app authentication using Traefik/Nginx.
@jacekkow jacekkow force-pushed the single-cookie-domain branch from 1848a7a to d90e3d6 Compare April 19, 2026 20:18
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Apr 19, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/bootstrap/app_bootstrap.go (1)

109-121: ⚠️ Potential issue | 🟠 Major

When subdomains are disabled, cookies must be host-only to prevent automatic authentication on subdomains.

Line 115 calls GetStandaloneCookieDomain, which returns the hostname for https://example.com as example.com. This is then formatted with a leading dot in all cookie setters (fmt.Sprintf(".%s", config.CookieDomain)), producing Domain=.example.com. Per RFC 6265, this allows browsers to send the cookie to all subdomains, defeating the intent of disabling subdomain authentication. Set cookieDomain to an empty string in standalone mode, which will result in an invalid/empty domain attribute that browsers treat as host-only cookies.

Proposed fix
-	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
-	}
-
-	app.context.cookieDomain = cookieDomain
+	if app.config.Auth.SubdomainsEnabled {
+		cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
+		if err != nil {
+			return err
+		}
+		app.context.cookieDomain = cookieDomain
+	} else {
+		tlog.App.Info().Msg("Subdomains disabled, cookies will be host-only and automatic authentication for proxied apps will not work")
+		app.context.cookieDomain = ""
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/bootstrap/app_bootstrap.go` around lines 109 - 121, The code
currently calls GetStandaloneCookieDomain via cookieDomainResolver when
app.config.Auth.SubdomainsEnabled is false, which produces a host name that
later gets formatted as ".%s" and enables subdomain cookies; change the logic so
that when SubdomainsEnabled is false you do not call GetStandaloneCookieDomain
but instead set cookieDomain to the empty string (""), then assign
app.context.cookieDomain = "" so cookie setters will omit a domain attribute and
the browser will treat cookies as host-only; update the conditional around
cookieDomainResolver/app.context.cookieDomain to reflect this behavior and keep
GetCookieDomain used only when SubdomainsEnabled is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/bootstrap/app_bootstrap.go`:
- Around line 109-121: The code currently calls GetStandaloneCookieDomain via
cookieDomainResolver when app.config.Auth.SubdomainsEnabled is false, which
produces a host name that later gets formatted as ".%s" and enables subdomain
cookies; change the logic so that when SubdomainsEnabled is false you do not
call GetStandaloneCookieDomain but instead set cookieDomain to the empty string
(""), then assign app.context.cookieDomain = "" so cookie setters will omit a
domain attribute and the browser will treat cookies as host-only; update the
conditional around cookieDomainResolver/app.context.cookieDomain to reflect this
behavior and keep GetCookieDomain used only when SubdomainsEnabled is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f640cb00-ea72-4e72-ab1b-df5cfff1b5a7

📥 Commits

Reviewing files that changed from the base of the PR and between 1848a7a and d90e3d6.

📒 Files selected for processing (3)
  • internal/bootstrap/app_bootstrap.go
  • internal/config/config.go
  • internal/utils/app_utils.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/utils/app_utils.go
  • internal/config/config.go

@jacekkow
Copy link
Copy Markdown
Contributor Author

I have changed the naming.

Coderabbit is actually correct that cookieDomain is always prefixed with . when setting the cookies. It is not an issue for me, but would you also like to have separate appDomain (used with utils.CompileUserEmail) and actual cookieDomain (set to .{domain} if subdomains are enables and otherwise just {domain})?

@steveiliop56
Copy link
Copy Markdown
Member

@jacekkow if you can also fix that, it would be amazing. You should be able to do it by either passing the cookie domain already formatted so as the controllers don't have to append anything or by adding a separate option in the controller configuration. I would honestly prefer the first one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants