Skip to content

refactor: remove concurrent listeners and rework cookie logic#950

Open
steveiliop56 wants to merge 8 commits into
mainfrom
chore/remove-concurrent-listeners
Open

refactor: remove concurrent listeners and rework cookie logic#950
steveiliop56 wants to merge 8 commits into
mainfrom
chore/remove-concurrent-listeners

Conversation

@steveiliop56

@steveiliop56 steveiliop56 commented Jun 21, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added Tailscale funnel mode support (with a warning when funnel is enabled but listening is off).
  • Improvements

    • Updated the redirect trust model to use the subdomain setting instead of an explicit trusted-domains list, affecting redirect validation and untrusted redirect messaging.
    • Simplified and hardened redirect-URI safety checks, with stricter scheme/host/port validation and conditional subdomain acceptance.
    • Improved session cookie domain derivation to consistently follow the configured subdomain behavior.
    • Adjusted server startup to select a single listener type based on runtime configuration (Tailscale, Unix socket, or HTTP).

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 21, 2026
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces the explicit trustedDomains list with a subdomainsEnabled boolean throughout the entire stack: backend model, redirect-safety validation, cookie-domain utility, auth service session cookies, context controller API response, and frontend schema/hooks/UI warnings. Also adds Tailscale funnel support, simplifies listener selection to a single getListenerFunc, and normalizes AppURL to lowercase.

Changes

Subdomain Trust Model Refactor

Layer / File(s) Summary
Model/config/schema contracts
internal/model/runtime.go, internal/model/config.go, frontend/src/schemas/app-context-schema.ts
Removes TrustedDomains from RuntimeConfig, removes ConcurrentListenersEnabled from ServerConfig, adds Funnel/Listen to TailscaleConfig, and replaces trustedDomains: z.array(z.string()) with subdomainsEnabled: z.boolean() in the frontend schema.
GetCookieDomain utility refactor
internal/utils/app_utils.go, internal/utils/app_utils_test.go
Rewrites GetCookieDomain to accept (appUrl, subdomainsEnabled), removes GetStandaloneCookieDomain, adds IP rejection, lowercase normalization, and fmt.Errorf-wrapped errors; updates tests for the new calling convention.
Backend redirect safety and context API
internal/controller/oauth_controller.go, internal/controller/context_controller.go, internal/controller/context_controller_test.go, internal/controller/oauth_controller_test.go
Updates imports to remove publicsuffix-go and add dig; replaces ACRApp.TrustedDomains with ACRApp.SubdomainsEnabled; rewrites isRedirectSafe to use scheme+exact-host matching with optional cookie-domain suffix check when SubdomainsEnabled; replaces the prior public-suffix-based allowlist; expands test coverage with a table-driven test suite.
Auth service cookie domain helper
internal/service/auth_service.go
Adds getCookieDomain() helper that conditionally returns CookieDomain when SubdomainsEnabled; uses it in CreateSession, RefreshSession, and DeleteSession; removes Tailscale-specific cookie-domain extraction from CreateSession.
Bootstrap: AppURL normalization, Tailscale, listener selection
internal/bootstrap/app_bootstrap.go, internal/bootstrap/router_bootstrap.go, internal/service/tailscale_service.go
Lowercases AppURL at setup; calls GetCookieDomain unconditionally with SubdomainsEnabled; overwrites AppURL with Tailscale URL instead of appending to TrustedDomains; removes Listener type and runListeners, adds getListenerFunc for single-handler selection; adds Tailscale funnel listener path.
Frontend redirect trust hook and warning UI
frontend/src/lib/hooks/redirect-uri.ts, frontend/src/pages/continue-page.tsx, frontend/src/components/layout/layout.tsx
Extends useRedirectUri with subdomainsEnabled parameter and gates subdomain trust behind it; passes app.subdomainsEnabled from ContinuePage; conditionally dot-prefixes cookieDomain in the untrusted redirect subtitle; changes DomainWarning to compare currentUrl !== app.appUrl.
Test fixture updates
internal/test/test.go
Adds SubdomainsEnabled: true to test auth ACL config and removes the TrustedDomains list from the test runtime.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OAuthController
  participant BootstrapApp
  participant GetCookieDomain

  BootstrapApp->>GetCookieDomain: GetCookieDomain(AppURL, subdomainsEnabled)
  GetCookieDomain-->>BootstrapApp: cookieDomain string

  Client->>OAuthController: OAuth callback with redirect_uri
  OAuthController->>OAuthController: isRedirectSafe(redirectURI)
  Note over OAuthController: parse redirect URI and AppURL<br/>check scheme equality<br/>allow exact host match OR<br/>subdomain suffix if SubdomainsEnabled
  OAuthController-->>Client: allow / reject redirect
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyauthapp/tinyauth#630: Modifies the same useRedirectUri hook and ContinuePage redirect warning flow that this PR extends with subdomainsEnabled-aware trust logic.
  • tinyauthapp/tinyauth#641: Refactors the same ContinuePage auto-redirect and warning-branch rendering based on the trusted flag from useRedirectUri.
  • tinyauthapp/tinyauth#847: Changes the same Tailscale integration wiring, bootstrap/routing, and auth/cookie-domain logic in the Go backend.

Suggested reviewers

  • Rycochet

🐇 No more domain lists to maintain and keep,
just one little flag, set true or asleep.
Subdomains blossom when enabled with care,
the tailscale now owns the AppURL we share.
One getListenerFunc picks the path just right,
and cookies dot-prefix only when subdomains ignite! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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.
Title check ✅ Passed The title accurately summarizes the two main changes in the pull request: removing concurrent listener infrastructure and refactoring cookie domain logic to use subdomainsEnabled instead of trustedDomains.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/remove-concurrent-listeners

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.

@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/layout/layout.tsx`:
- Line 43: The domain warning condition on line 43 only checks for an exact
match against app.appUrl, but it should also respect the subdomainsEnabled flag
to allow valid subdomain origins without triggering the warning. Update the
condition in the if statement that checks ignoreDomainWarning,
ui.warningsEnabled, and currentUrl to also validate that currentUrl matches
either the canonical app.appUrl or is a valid subdomain when subdomainsEnabled
is true, using the same domain suffix matching predicate used in the redirect
and cookie trust model.

In `@internal/bootstrap/app_bootstrap.go`:
- Around line 165-169: The code logs that cookies will be "set for the current
domain only" when SubdomainsEnabled is false, but then calls
utils.GetCookieDomain() which still returns a domain value that gets set in the
cookie Domain attribute. When subdomains are disabled, the Domain attribute
should be completely omitted from the cookie (not set to a non-dotted domain
like "example.com") to achieve true host-only behavior. Modify the logic around
the utils.GetCookieDomain() call so that when app.config.Auth.SubdomainsEnabled
is false, the cookieDomain is set to an empty string instead of retrieving a
domain value, ensuring the Domain attribute is not included in the cookie.
- Around line 284-298: After updating app.runtime.AppURL and
app.runtime.CookieDomain in the Tailscale listening block, you must also refresh
the OAuth provider RedirectURL values that were initially assigned earlier based
on the original AppURL. Locate where the default provider RedirectURLs are set
(around lines 145-147 mentioned in the comment) and add code in the Tailscale
block to re-assign these provider redirect URLs using the new tailscaleUrl
value, ensuring OAuth callbacks correctly target the Tailscale hostname instead
of the previously configured host.

In `@internal/bootstrap/router_bootstrap.go`:
- Around line 132-141: The getListenerFunc method currently falls through to
Unix or HTTP listeners when tailscale.listen is true but tailscaleService is
nil, which violates the fail-closed principle. Modify the getListenerFunc method
to return an error when Tailscale listening is configured but the
tailscaleService is unavailable, preventing the silent fallback to standard
listeners. Update the function signature to return both a listener function and
an error, then handle the returned error in the Setup method before starting the
listener goroutine to fail early with an appropriate error message.

In `@internal/controller/oauth_controller.go`:
- Line 317: The oauth_controller.go file is logging full redirectURI values
which can expose sensitive query parameters from upstream applications. In the
error logging statements at lines involving redirect URI validation (around the
"Failed to parse redirect URI" log and similar rejection paths at lines 322 and
334), replace the logging of the complete redirectURI variable with safer
alternatives such as logging only the URI scheme and host components or the
length of the URI. This prevents sensitive data in query parameters from being
exposed in logs while still providing useful debugging information.
- Around line 326-331: The url.Parse() call for controller.runtime.AppURL can
succeed on malformed URLs without proper scheme and host validation. After the
error check for the url.Parse() call on au, add explicit validation to ensure
both the scheme and host are not empty, matching the validation pattern used for
redirectURI validation (which checks these fields around lines 297-305). This
validation should occur before the parsed URL is used downstream, and should
return false if either the scheme or host is missing or invalid. Additionally,
add a test case covering the malformed URL scenario where appURL is
"https:/example.com" (single slash) and redirectURI is
"https://sub.example.com", which should evaluate to false.

In `@internal/service/auth_service.go`:
- Around line 708-713: The getCookieDomain methods in both AuthService and
OAuthController do not correctly enforce cookie domain isolation. When
SubdomainsEnabled is false, the methods should return an empty string (not the
domain value) to create a host-only cookie that restricts to the current host
only, which matches the documented behavior. When SubdomainsEnabled is true,
remove the leading dot before the domain since RFC 6265 specifies that leading
dots are ignored. Apply these changes to both AuthService.getCookieDomain() and
OAuthController.getCookieDomain() methods.

In `@internal/utils/app_utils.go`:
- Around line 32-54: The function currently returns `u.Host` which includes port
numbers, violating RFC 6265 for Set-Cookie Domain attributes and causing
frontend redirect validation failures. Additionally, the manual `parts[1:]`
slicing incorrectly handles multi-label public suffixes by stripping too many
parts (e.g., `example.co.uk` becomes `co.uk`). Replace all references to
`u.Host` with `u.Hostname()` to eliminate ports, and use the domain value
returned directly from `publicsuffix.DomainFromListWithOptions` instead of
manually reconstructing it from sliced parts. Update the test cases to expect
port-free results and to correctly validate multi-label public suffix domains as
acceptable cookie domains.
🪄 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: 31468088-60a0-4afc-92e4-10b80af1211d

📥 Commits

Reviewing files that changed from the base of the PR and between 72d39a2 and a4f9c89.

📒 Files selected for processing (17)
  • frontend/src/components/layout/layout.tsx
  • frontend/src/lib/hooks/redirect-uri.ts
  • frontend/src/pages/continue-page.tsx
  • frontend/src/schemas/app-context-schema.ts
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/controller/context_controller.go
  • internal/controller/context_controller_test.go
  • internal/controller/oauth_controller.go
  • internal/controller/oauth_controller_test.go
  • internal/model/config.go
  • internal/model/runtime.go
  • internal/service/auth_service.go
  • internal/service/tailscale_service.go
  • internal/test/test.go
  • internal/utils/app_utils.go
  • internal/utils/app_utils_test.go
💤 Files with no reviewable changes (1)
  • internal/model/runtime.go

Comment thread frontend/src/components/layout/layout.tsx
Comment thread internal/bootstrap/app_bootstrap.go
Comment on lines +284 to +298
// force tailscale app url if listening on a tailscale address
if app.services.tailscaleService != nil && app.config.Tailscale.Listen {
tailscaleUrl := "https://" + app.services.tailscaleService.GetHostname()
if tailscaleUrl != app.runtime.AppURL {
app.log.App.Info().Msg("Listening on tailscale, replacing app url with tailscale hostname")
app.runtime.AppURL = tailscaleUrl
// also update cookie domain
cookieDomain, err := utils.GetCookieDomain(tailscaleUrl, app.config.Auth.SubdomainsEnabled)

if err != nil {
return fmt.Errorf("failed to get cookie domain: %w", err)
}

app.runtime.CookieDomain = cookieDomain
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh default OAuth callback URLs after replacing AppURL.

Default provider RedirectURLs are assigned earlier from the original runtime.AppURL at Lines 145-147. When Lines 284-298 replace runtime.AppURL for Tailscale listening, providers that relied on the default callback keep pointing at the old host, so OAuth callbacks can target an address this instance no longer serves.

Possible fix direction
+defaultedRedirectURLs := make(map[string]bool)
 for id, provider := range app.runtime.OAuthProviders {
     ...
     if provider.RedirectURL == "" {
+        defaultedRedirectURLs[id] = true
         provider.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + id
     }
     ...
 }

 ...
 if tailscaleUrl != app.runtime.AppURL {
     app.log.App.Info().Msg("Listening on tailscale, replacing app url with tailscale hostname")
     app.runtime.AppURL = tailscaleUrl
     ...
     app.runtime.CookieDomain = cookieDomain
+
+    for id, provider := range app.runtime.OAuthProviders {
+        if defaultedRedirectURLs[id] {
+            provider.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + id
+            app.runtime.OAuthProviders[id] = provider
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/bootstrap/app_bootstrap.go` around lines 284 - 298, After updating
app.runtime.AppURL and app.runtime.CookieDomain in the Tailscale listening
block, you must also refresh the OAuth provider RedirectURL values that were
initially assigned earlier based on the original AppURL. Locate where the
default provider RedirectURLs are set (around lines 145-147 mentioned in the
comment) and add code in the Tailscale block to re-assign these provider
redirect URLs using the new tailscaleUrl value, ensuring OAuth callbacks
correctly target the Tailscale hostname instead of the previously configured
host.

Comment thread internal/bootstrap/router_bootstrap.go Outdated
Comment thread internal/controller/oauth_controller.go Outdated
Comment thread internal/controller/oauth_controller.go
Comment thread internal/service/auth_service.go
Comment thread internal/utils/app_utils.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/controller/oauth_controller.go`:
- Around line 338-341: The port comparison in the conditional at the Port()
method calls fails because url.Parse returns an empty string for Port() when no
explicit port is specified, causing URLs like "https://example.com" (empty port
string) to incorrectly not match "https://example.com:443" (port "443"). Fix
this by normalizing the ports explicitly: extract the port using Port() for both
u and au, and if the port is empty, assign the default port based on the URL
scheme (443 for https, 80 for http), then compare the normalized ports.
Additionally, also compare the Hostname() values to ensure the hosts match as
well.
🪄 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: 3345f794-cd8b-4e20-b56c-7fc8f6e434ec

📥 Commits

Reviewing files that changed from the base of the PR and between e53cbf4 and c9337da.

📒 Files selected for processing (5)
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/controller/oauth_controller.go
  • internal/service/auth_service.go
  • internal/service/tailscale_service.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go

Comment on lines +338 to +341
if u.Port() != au.Port() {
controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Port comparison fails for default vs explicit port.

url.Parse returns an empty string for Port() when the port is not explicitly specified in the URL. Comparing "https://example.com" (port = "") against "https://example.com:443" (port = "443") will fail even though they resolve to the same endpoint.

Use Hostname() for host comparison and normalize ports explicitly:

Proposed fix
-	if u.Port() != au.Port() {
-		controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
-		return false
-	}
-
-	if strings.EqualFold(u.Host, au.Host) {
+	// Normalize ports: empty means default (443 for https, 80 for http)
+	getEffectivePort := func(u *url.URL) string {
+		if p := u.Port(); p != "" {
+			return p
+		}
+		if u.Scheme == "https" {
+			return "443"
+		}
+		return "80"
+	}
+
+	if getEffectivePort(u) != getEffectivePort(au) {
+		controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
+		return false
+	}
+
+	if strings.EqualFold(u.Hostname(), au.Hostname()) {
 		return true
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/oauth_controller.go` around lines 338 - 341, The port
comparison in the conditional at the Port() method calls fails because url.Parse
returns an empty string for Port() when no explicit port is specified, causing
URLs like "https://example.com" (empty port string) to incorrectly not match
"https://example.com:443" (port "443"). Fix this by normalizing the ports
explicitly: extract the port using Port() for both u and au, and if the port is
empty, assign the default port based on the URL scheme (443 for https, 80 for
http), then compare the normalized ports. Additionally, also compare the
Hostname() values to ensure the hosts match as well.

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

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant