Skip to content

refactor: remove concurrent listeners and rework cookie logic#950

Merged
steveiliop56 merged 12 commits into
mainfrom
chore/remove-concurrent-listeners
Jun 23, 2026
Merged

refactor: remove concurrent listeners and rework cookie logic#950
steveiliop56 merged 12 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 (warns if enabled but listening is off).
  • Improvements

    • Redirect trust now follows the subdomain setting (replacing the prior trusted-domains list), including updated untrusted-redirect messaging.
    • Strengthened redirect-URI validation (scheme/host/port checks with conditional subdomain allowance).
    • Improved session cookie domain handling, app context reporting, and continue-page subtitle formatting.
    • Simplified server startup to select a single runtime listener mode (Tailscale, Unix socket, or HTTP).
  • Bug Fixes

    • Return HTTP 401 for missing/expired user context during auth flows.

@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

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
  • 🔍 Trigger review
📝 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 and subdomain-stripping behavior.
Redirect safety validation
internal/controller/oauth_controller.go, internal/controller/oauth_controller_test.go
Updates imports to remove publicsuffix-go and add dig; rewrites isRedirectSafe to parse both URLs, enforce exact scheme+port matching, allow exact host match or cookie-domain suffix when SubdomainsEnabled, and reject all other redirects; replaces trusted-domain/public-suffix logic with a fail-closed validator; expands test coverage with a table-driven test suite covering parse failures, case-insensitivity, and edge cases.
Context API response and handlers
internal/controller/context_controller.go, internal/controller/context_controller_test.go
Replaces ACRApp.TrustedDomains with ACRApp.SubdomainsEnabled; updates appContextHandler to populate SubdomainsEnabled from config; adjusts userContextHandler error logging to suppress ErrUserContextNotFound; updates test expectations accordingly.
Session cookies and OAuth service abstraction
internal/service/auth_service.go
Adds getCookieDomain() helper; uses it in CreateSession, RefreshSession, and DeleteSession; removes Tailscale-specific cookie-domain extraction; refactors OAuthPendingSession.Service to store IOAuthService; converts GetOAuthToken and GetOAuthUserinfo to direct calls on IOAuthService; updates GetOAuthService return type to IOAuthService.
OAuth broker service interface
internal/service/oauth_broker_service.go, internal/service/oauth_service.go
Replaces OAuthServiceImpl interface with IOAuthService (adds ID(), GetConfig(), UpdateConfig()); updates OAuthBrokerService.services map and GetService return type to use IOAuthService; adds OAuthService.GetConfig() and UpdateConfig() implementations that sync oauth2.Config with updated service config.
Bootstrap: AppURL normalization, listener selection, Tailscale integration
internal/bootstrap/app_bootstrap.go, internal/bootstrap/router_bootstrap.go, internal/service/tailscale_service.go
Normalizes AppURL to lowercase; initializes OAuth provider names during primary loop; calls GetCookieDomain unconditionally with SubdomainsEnabled; overwrites AppURL with Tailscale URL and recomputes CookieDomain instead of appending to TrustedDomains; forces OAuth broker redirect URLs to refresh when empty; removes BootstrapApp.listeners field and Listener type; adds getListenerFunc for deterministic single-handler selection (Tailscale → Unix socket → HTTP); adds Tailscale funnel listener support.
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; adds internal helpers for effective-port matching and domain validation; passes app.subdomainsEnabled from ContinuePage; conditionally dot-prefixes cookieDomain in the untrusted redirect subtitle; updates DomainWarning to compare currentUrl !== app.appUrl.
User context error handling
internal/controller/user_controller.go
Adds specific handling for ErrUserContextNotFound in totpHandler and tailscaleHandler; logs a warning and returns HTTP 401 for missing user context instead of routing through generic error paths.
Test infrastructure and fixtures
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 ContinuePage
  participant useRedirectUri
  participant OAuthController as isRedirectSafe

  Client->>ContinuePage: OAuth callback with redirect_uri
  ContinuePage->>useRedirectUri: useRedirectUri(redirect_uri, cookieDomain,<br/>appUrl, subdomainsEnabled)
  useRedirectUri->>useRedirectUri: isTrustedDomain check<br/>parse both URLs, verify scheme,<br/>verify port, check hostname
  useRedirectUri-->>ContinuePage: {valid, trusted}
  
  ContinuePage->>OAuthController: isRedirectSafe(redirectURI)<br/>backend validation
  OAuthController->>OAuthController: parse URLs, enforce<br/>scheme+port, allow exact<br/>host or SubdomainsEnabled suffix
  OAuthController-->>ContinuePage: safe or unsafe
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#847: Changes the same Tailscale integration wiring, bootstrap/routing, and auth/cookie-domain logic in the Go backend, replacing trustedDomains with subdomainsEnabled and refactoring listener selection.
  • tinyauthapp/tinyauth#923: Modifies the same useRedirectUri hook signature in frontend/src/lib/hooks/redirect-uri.ts and its parameter types.

Suggested reviewers

  • Rycochet
  • scottmckendry

🐇 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 16.67% 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 describes the main changes: removing concurrent listeners and refactoring cookie domain logic from trustedDomains to subdomainsEnabled throughout the codebase.
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 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.

@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 Outdated
Comment thread internal/bootstrap/app_bootstrap.go
Comment thread internal/bootstrap/app_bootstrap.go Outdated
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 thread internal/controller/oauth_controller.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/service/auth_service.go`:
- Around line 547-558: The svc.UpdateConfig(cfg) call is currently executed
unconditionally on every invocation, causing unnecessary writes to a shared
service instance. Move the svc.UpdateConfig(cfg) call inside the if
cfg.RedirectURL == "" conditional block so that the config is only persisted
when the RedirectURL was actually missing and needed to be set. This ensures
updates only happen when the configuration has been modified, reducing
concurrent write pressure on the shared service instance.
🪄 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: f828b36a-1bed-4f2f-a79c-eeb5b91ec816

📥 Commits

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

📒 Files selected for processing (8)
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/controller/context_controller.go
  • internal/controller/oauth_controller.go
  • internal/controller/user_controller.go
  • internal/service/auth_service.go
  • internal/service/oauth_broker_service.go
  • internal/service/oauth_service.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/controller/oauth_controller.go
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go

Comment thread internal/service/auth_service.go Outdated
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jun 21, 2026
@steveiliop56

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors runtime listener selection to a single active mode (Tailscale, Unix socket, or HTTP), and reworks redirect/cookie-domain handling to be driven by the configured App URL plus the subdomainsEnabled setting instead of a trusted-domains list. It also adds Tailscale Funnel listener support and aligns the frontend app-context/redirect validation logic with the backend.

Changes:

  • Simplify server startup to choose exactly one listener mode and add Tailscale Funnel listener support.
  • Replace trusted-domain redirect allowlisting with scheme/host/port checks against AppURL, plus conditional subdomain allowance based on cookieDomain and subdomainsEnabled.
  • Rework cookie domain resolution + app context payload/schema to support subdomain-enabled vs host-only cookie behavior.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/utils/app_utils.go Refactors cookie-domain derivation into a single function that accounts for subdomain enablement.
internal/utils/app_utils_test.go Updates tests for new cookie-domain behavior/signature and adds new cases.
internal/test/test.go Updates test config defaults to include SubdomainsEnabled and removes TrustedDomains.
internal/service/tailscale_service.go Adds Funnel mode warning and listener creation via ListenFunnel when enabled.
internal/service/oauth_service.go Adds config getter/updater to allow redirect URL updates post-construction.
internal/service/oauth_broker_service.go Renames/extends OAuth service interface to support runtime config updates.
internal/service/auth_service.go Removes pointer-to-interface usage for OAuth sessions and centralizes cookie-domain selection.
internal/model/runtime.go Removes TrustedDomains from runtime config.
internal/model/config.go Removes concurrent listener config, adds tailscale.funnel and tailscale.listen.
internal/controller/user_controller.go Returns HTTP 401 for missing user context during TOTP/Tailscale auth flows.
internal/controller/oauth_controller.go Reworks cookie domain behavior and strengthens redirect URI validation.
internal/controller/oauth_controller_test.go Replaces trusted-domains tests with AppURL/cookieDomain/subdomain-driven redirect validation tests.
internal/controller/context_controller.go App context response replaces trustedDomains with subdomainsEnabled; reduces logging for missing context.
internal/controller/context_controller_test.go Updates expected app context response for new schema.
internal/bootstrap/router_bootstrap.go Removes concurrent listener machinery; introduces single listener selection.
internal/bootstrap/app_bootstrap.go Normalizes AppURL, computes cookie domain with subdomain toggle, updates OAuth redirect URLs, and selects one listener.
frontend/src/schemas/app-context-schema.ts Updates schema to replace trustedDomains with subdomainsEnabled.
frontend/src/pages/continue-page.tsx Passes AppURL + subdomain flag into redirect validation and adjusts subtitle formatting.
frontend/src/lib/hooks/redirect-uri.ts Ports backend redirect safety logic (scheme/port/host + conditional subdomain allowance).
frontend/src/components/layout/layout.tsx Replaces trusted-domains UI warning gating with AppURL-based gating.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/utils/app_utils.go Outdated
Comment thread internal/utils/app_utils_test.go Outdated
Comment thread frontend/src/components/layout/layout.tsx Outdated
scottmckendry
scottmckendry previously approved these changes Jun 22, 2026

@scottmckendry scottmckendry left a comment

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.

more of a question rather than anything that needs to be changed. Ping me if you need a fresh review 🚀

Comment thread internal/bootstrap/app_bootstrap.go
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jun 22, 2026
@steveiliop56 steveiliop56 merged commit 69f4206 into main Jun 23, 2026
5 checks passed
@steveiliop56 steveiliop56 deleted the chore/remove-concurrent-listeners branch June 23, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants