Skip to content

feat: add option to run tinyauth on a top-level domain#710

Open
jacekkow wants to merge 4 commits intotinyauthapp:mainfrom
jacekkow:single-cookie-domain
Open

feat: add option to run tinyauth on a top-level domain#710
jacekkow wants to merge 4 commits 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 toggle subdomain vs standalone cookie scoping (defaults to enabled).
    • Session and OAuth cookie creation, refresh, and deletion now respect the configured mode so cookie scope behaves correctly for subdomains or single-host deployments.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Runtime cookie-domain selection now respects a new Auth.SubdomainsEnabled flag: the bootstrap chooses between GetCookieDomain and GetStandaloneCookieDomain and assigns the resolved value to app.context.cookieDomain (with existing error handling preserved).

Changes

Cohort / File(s) Summary
Configuration
internal/config/config.go
Added SubdomainsEnabled bool to AuthConfig (yaml:"subdomainsEnabled") and defaulted it to true in NewDefaultConfiguration().
Bootstrap logic
internal/bootstrap/app_bootstrap.go, internal/bootstrap/service_bootstrap.go, internal/bootstrap/router_bootstrap.go
BootstrapApp.Setup selects a cookieDomainResolver based on Auth.SubdomainsEnabled (switches to GetStandaloneCookieDomain when disabled and logs an info message); service_bootstrap.go and router wiring pass SubdomainsEnabled into auth service and controller configs.
Utilities
internal/utils/app_utils.go
Added GetStandaloneCookieDomain(u string) (string, error) which parses the URL and returns parsed.Hostname() (standalone hostname resolver).
Controllers
internal/controller/oauth_controller.go
OAuthControllerConfig gained SubdomainsEnabled; cookie creation/deletion now call a getCookieDomain() helper that returns either CookieDomain or "."+CookieDomain depending on the flag.
Services
internal/service/auth_service.go
AuthServiceConfig extended with SubdomainsEnabled; session-cookie helpers (CreateSessionCookie, RefreshSessionCookie, DeleteSessionCookie) delegate domain selection to getCookieDomain() which respects the flag.

Sequence Diagram(s)

sequenceDiagram
    participant Bootstrap as BootstrapApp
    participant Config as Auth Config
    participant Utils as utils

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

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • refactor: tests #731 — touches cookie-domain determination and related tests; closely related to the added standalone resolver and caller changes.

Suggested reviewers

  • steveiliop56

Poem

🐰 I nibble configs under moonlight sheen,
A flag decides if dots are seen,
I hop to parse hostname neat and round,
Choosing resolver without a sound,
Cookies settle home — my little bound.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main feature: adding an option to deploy Tinyauth on a top-level domain, which aligns with the primary purpose of the PR as described in objectives.

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

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

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.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Apr 29, 2026
@jacekkow
Copy link
Copy Markdown
Contributor Author

jacekkow commented Apr 29, 2026

@steveiliop56 - thanks for taking that on - I'd have time only during next weekend.

BTW. Should maybe CookieDomain field in AuthServiceConfig be renamed to MainDomain, too?

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/controller/oauth_controller.go`:
- Line 30: The OAuthControllerConfig gains a SubdomainsEnabled field but the
controller creation (the OAuthControllerConfig struct literal in the router
bootstrap) never sets it, so getCookieDomain() will never apply subdomain
prefixes; update the struct literal that constructs OAuthControllerConfig to
include SubdomainsEnabled: cfg.SubdomainsEnabled (or the appropriate app config
boolean) so the controller receives the flag and getCookieDomain() can behave
correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e59be7f4-cc6e-4bf5-bef5-d6e95ab95353

📥 Commits

Reviewing files that changed from the base of the PR and between d90e3d6 and 4c0181c.

📒 Files selected for processing (6)
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/config/config.go
  • internal/controller/oauth_controller.go
  • internal/service/auth_service.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

Comment thread internal/controller/oauth_controller.go
@jacekkow
Copy link
Copy Markdown
Contributor Author

@steveiliop56 - thanks for taking that on.

BTW. Should CookieDomain in AuthServiceConfig be changed to BaseDomain/MainDomain, too? It has less to do with cookies now.

@steveiliop56
Copy link
Copy Markdown
Member

@jacekkow don't worry I will take it over from here, meaning we merge because nothing is left.

BTW. Should maybe CookieDomain field in AuthServiceConfig be renamed to MainDomain, too?

Most probably yes. But let's leave it as-is for now since I would like to rework the cookie and context logic later.

@steveiliop56 steveiliop56 changed the title feat: add TINYAUTH_AUTH_SINGLECOOKIEDOMAIN option (and fix release workflow) feat: add option to run tinyauth on a top-level domain Apr 29, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Apr 29, 2026
)

// Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
func GetCookieDomain(u string) (string, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Am I tripping or do we now have three implementations of getCookieDomain? This will cause a bug at some point in the future, I'm sure of it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, this one has test coverage, the other two do not.

return domain, nil
}

func GetStandaloneCookieDomain(u string) (string, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This probably needs tests to cover the scenarios it may be used in.

@dosubot dosubot Bot removed the lgtm This PR has been approved by a maintainer label Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants