Skip to content

Cross domain handoffs#1458

Merged
N2D4 merged 5 commits into
devfrom
cross-domain-handoff
May 22, 2026
Merged

Cross domain handoffs#1458
N2D4 merged 5 commits into
devfrom
cross-domain-handoff

Conversation

@N2D4
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 commented May 21, 2026

Note

Medium Risk
Changes core OAuth/cross-domain redirect and session-resolution behavior in the client SDK, which is security- and flow-critical and could cause login/redirect regressions if edge cases are missed. Risk is mitigated by expanded E2E/unit coverage and stricter trust checks.

Overview
Adds nested cross-domain auth handoffs for hosted flows by attaching source-session identifiers and callback URLs to cross-origin handler redirects, then completing a two-step bounce that mints a cross-domain code only when the source session and trusted redirect URLs match.

Hardens hosted OAuth callback behavior and session consistency: hosted callbacks now auto-run only when a matching stack-oauth-outer-${state} cookie exists, the redirect URI for hosted callbacks is derived from the current page with stale OAuth params stripped, and startup auth transitions are tracked as pending promises that _getSession/react-like _useSession can await (with opt-out for re-entrant flows).

Centralizes redirect URL validation by moving trusted-domain wildcard/port/localhost logic into stack-shared (validateRedirectUrl, getTrustedParentDomain) and updating backend/client trust checks to use it; project client read schema now includes allow_localhost.

Tightens handler URL configuration rules by preventing absolute oauthCallback targets and avoiding inheriting absolute default targets for oauthCallback, plus updates the demo config to use hosted defaults and adds extensive new E2E/unit tests around these flows.

Reviewed by Cursor Bugbot for commit dfb6d61. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Nested cross-domain authentication for hosted flows and hosted OAuth callback handling.
  • Improvements

    • Centralized, stronger redirect-URL validation against trusted domains with parent-domain rules and optional localhost allowance.
    • Option to suppress missing-query-parameter warnings during OAuth callback handling.
    • Improved session race handling by tracking and optionally awaiting pending auth resolutions; hosted callback detection now requires a matching verifier cookie.
  • Tests

    • Expanded e2e and unit tests covering cross-domain auth, hosted callback flows, and redirect-url validation.
  • Documentation

    • Added Q&A on client-side OAuth callback flows and pending-auth resolution behavior.

Review Change Stack

Copilot AI review requested due to automatic review settings May 21, 2026 19:30
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 22, 2026 12:14am
stack-auth-mcp Ready Ready Preview, Comment May 22, 2026 12:14am
stack-auth-skills Ready Ready Preview, Comment May 22, 2026 12:14am
stack-backend Ready Ready Preview, Comment May 22, 2026 12:14am
stack-dashboard Ready Ready Preview, Comment May 22, 2026 12:14am
stack-demo Ready Ready Preview, Comment May 22, 2026 12:14am
stack-docs Ready Ready Preview, Comment May 22, 2026 12:14am
stack-preview-backend Ready Ready Preview, Comment May 22, 2026 12:14am
stack-preview-dashboard Ready Ready Preview, Comment May 22, 2026 12:14am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 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
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a267123d-0db4-4510-a488-c3bfa2330bb8

📥 Commits

Reviewing files that changed from the base of the PR and between 7c34aed and dfb6d61.

📒 Files selected for processing (1)
  • .claude/CLAUDE-KNOWLEDGE.md

📝 Walkthrough

Walkthrough

Centralizes redirect-URL validation, enforces relative oauthCallback targets, adds hosted-aware OAuth callback detection/URIs, implements nested cross-domain auth handoffs with pending auth-resolution tracking, and updates client/backend wiring, tests, demo config, and docs.

Changes

Hosted OAuth callbacks and nested cross-domain auth

Layer / File(s) Summary
Shared redirect URL validation utilities
packages/stack-shared/src/utils/redirect-urls.tsx
New module exports isAcceptedNativeAppUrl, validateRedirectUrl, and getTrustedParentDomain to normalize ports, parse wildcard patterns, and match URLs against exact/wildcard trusted domain patterns with optional localhost allowance; includes Vitest coverage.
Project configuration for localhost allowance
packages/stack-shared/src/interface/crud/projects.ts
Extend projectsCrudClientReadSchema to include config.allow_localhost for per-project localhost redirect acceptance.
Backend redirect validation delegation
apps/backend/src/lib/redirect-urls.tsx
Refactor backend redirect validation to delegate to shared validateRedirectUrl and re-export isAcceptedNativeAppUrl.
OAuth callback URL relative path validation
packages/template/src/lib/stack-app/url-targets.ts, packages/template/src/lib/stack-app/url-targets.test.ts
Add assertion requiring oauthCallback targets be relative and update resolveHandlerUrls and tests to enforce this.
OAuth callback query parameter handling options
packages/template/src/lib/auth.ts
Add optional dontWarnAboutMissingQueryParams to callback param consumption and thread it through callOAuthCallback.
Client-side hosted OAuth callbacks and nested cross-domain auth orchestration
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Detect hosted OAuth callbacks, compute hosted-aware redirect URIs (_getOAuthCallbackRedirectUri), track/await pending auth-resolution promises (_trackPendingAuthResolution, _awaitPendingAuthResolutions, _usePendingAuthResolutions), inject/handle nested cross-domain auth params, validate redirects against project trusted config (_getTrustedRedirectConfig + shared validators), and update OAuth flows to use dynamic hosted URIs.
E2E tests for hosted callbacks and nested cross-domain auth
apps/e2e/tests/js/cross-domain-auth.test.ts
Add hosted-flow and nested cross-domain auth tests covering callback sanitization, pending-resolution rejection isolation, nested param propagation and continuation, mocked refresh-token checks, and negative trust/mismatch cases.
Demo configuration and documentation
examples/demo/src/stack.tsx, .claude/CLAUDE-KNOWLEDGE.md
Update demo stackServerApp default URL type to hosted and add knowledge base entries documenting pending auth resolution behavior and hosted callback auto-start gating.

Sequence Diagram

sequenceDiagram
  participant Browser
  participant HostedAuth as Hosted Auth Server
  participant CallbackPage as Hosted Callback Page
  participant ClientApp
  participant TokenStore as Persistent Token Store
  participant SessionConsumer

  Browser->>HostedAuth: authenticate -> redirect with code/state
  HostedAuth->>Browser: redirect to hosted callback URL (code/state)
  Browser->>CallbackPage: load hosted callback
  CallbackPage->>ClientApp: callOAuthCallback(options)
  ClientApp->>ClientApp: _trackPendingAuthResolution (promise)
  ClientApp->>TokenStore: exchange code -> tokens
  TokenStore->>ClientApp: tokens stored
  ClientApp->>ClientApp: resolve pending auth promise
  SessionConsumer->>ClientApp: _getSession(awaitPendingAuthResolutions: true)
  ClientApp->>ClientApp: _awaitPendingAuthResolutions()
  ClientApp->>SessionConsumer: return session
  ClientApp->>Browser: if nested params -> build nested redirect and navigate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • nams1570
  • BilalG1

Poem

🐰 Hopping through callbacks with a twitchy nose,
Awaiting promises where the startup wind blows,
Nested handoffs tucked in query-string art,
Sessions pause gently till each step can start,
A rabbit cheers: race-free flows—what a smart!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title 'Cross domain handoffs' accurately summarizes the main feature: support for nested cross-domain auth handoffs and safer hosted OAuth callback behavior.
Description check ✅ Passed The description provides comprehensive context: it includes a risk assessment, overview of key changes (nested cross-domain auth, session race prevention, redirect validation), and links to implementation details across multiple files.
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 cross-domain-handoff

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for cross-domain “hosted callback” and nested cross-domain auth handoffs, while centralizing redirect URL trust validation.

Changes:

  • Enforce relative OAuth callback handler targets in resolveHandlerUrls, with unit coverage.
  • Add hosted OAuth callback redirect-URI selection and nested cross-domain handoff flow in the client app implementation.
  • Introduce shared redirect URL validation utilities and reuse them in the backend tenancy redirect validation.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/url-targets.ts Adds validation that OAuth callback targets must be relative.
packages/template/src/lib/stack-app/url-targets.test.ts Adds tests ensuring absolute OAuth callback targets are rejected.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Implements hosted callback redirect URI logic, pending auth-resolution gating, nested cross-domain auth flow, and shared trusted-URL validation.
packages/template/src/lib/auth.ts Adds option to suppress warnings when OAuth callback params are missing; threads option through callback handling.
packages/stack-shared/src/utils/redirect-urls.tsx New shared helpers for redirect URL validation + trusted parent domain derivation.
packages/stack-shared/src/interface/crud/projects.ts Exposes allow_localhost on client project config schema.
examples/demo/src/stack.tsx Updates demo to use hosted default handler target.
apps/e2e/tests/js/cross-domain-auth.test.ts Updates existing expectations and adds new nested cross-domain auth E2E coverage.
apps/backend/src/lib/redirect-urls.tsx Replaces local redirect validation logic with shared stack-shared implementation.
.claude/CLAUDE-KNOWLEDGE.md Documents the new “pending auth resolution” gating approach.

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

Comment thread packages/template/src/lib/stack-app/url-targets.ts
Comment thread packages/stack-shared/src/utils/redirect-urls.tsx
Comment thread packages/stack-shared/src/utils/redirect-urls.tsx
Comment thread packages/stack-shared/src/utils/redirect-urls.tsx Outdated
Comment thread packages/stack-shared/src/utils/redirect-urls.tsx
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

🧹 Nitpick comments (2)
packages/stack-shared/src/utils/redirect-urls.tsx (1)

4-7: 💤 Low value

Align trustedDomains element type across exported APIs.

TrustedDomainConfig.trustedDomains is readonly (string | null | undefined)[] but getTrustedParentDomain takes readonly string[]. Callers reading trusted domains from a single source end up either coercing or filtering at the call site. Unifying the element type (and filtering nulls inside the function, as already done in validateRedirectUrl) would simplify usage.

♻️ Suggested signature unification
-export function getTrustedParentDomain(currentDomain: string, trustedDomains: readonly string[]): string | null {
+export function getTrustedParentDomain(
+  currentDomain: string,
+  trustedDomains: readonly (string | null | undefined)[],
+): string | null {
   const hostPatterns = trustedDomains
+    .filter((domain): domain is string => domain != null)
     .map((domain) => {

Also applies to: 88-88

🤖 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 `@packages/stack-shared/src/utils/redirect-urls.tsx` around lines 4 - 7,
TrustedDomainConfig.trustedDomains uses readonly (string | null | undefined)[]
but getTrustedParentDomain currently expects readonly string[]; unify the
element type by changing getTrustedParentDomain's trusted domains parameter to
accept readonly (string | null | undefined)[] (same as
TrustedDomainConfig.trustedDomains) and perform internal filtering of
null/undefined values inside getTrustedParentDomain (mirroring
validateRedirectUrl) so callers don’t need to coerce or filter; update any other
exported APIs that accept trusted domain arrays to use the unified element type
as well (e.g., the other occurrence referenced alongside
getTrustedParentDomain).
packages/template/src/lib/stack-app/url-targets.test.ts (1)

99-115: 💤 Low value

Prefer toThrowErrorMatchingInlineSnapshot for the new assertions.

Both new cases assert via .toThrowError(/regex/). Consider snapshotting the error message instead so that intent and message stay in sync. As per coding guidelines: "When writing tests, prefer .toMatchInlineSnapshot over other selectors in Vitest tests."

♻️ Proposed change
   it("rejects absolute OAuth callback string targets", () => {
-    expect(() => resolveHandlerUrls({
-      projectId: "project-id",
-      urls: {
-        oauthCallback: "https://app.example.test/oauth-callback",
-      },
-    })).toThrowError(/OAuth callback URLs must be relative/);
+    expect(() => resolveHandlerUrls({
+      projectId: "project-id",
+      urls: {
+        oauthCallback: "https://app.example.test/oauth-callback",
+      },
+    })).toThrowErrorMatchingInlineSnapshot();
   });

   it("rejects absolute OAuth callback custom targets", () => {
-    expect(() => resolveHandlerUrls({
-      projectId: "project-id",
-      urls: {
-        oauthCallback: { type: "custom", url: "https://app.example.test/oauth-callback", version: 0 },
-      },
-    })).toThrowError(/OAuth callback URLs must be relative/);
+    expect(() => resolveHandlerUrls({
+      projectId: "project-id",
+      urls: {
+        oauthCallback: { type: "custom", url: "https://app.example.test/oauth-callback", version: 0 },
+      },
+    })).toThrowErrorMatchingInlineSnapshot();
   });
🤖 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 `@packages/template/src/lib/stack-app/url-targets.test.ts` around lines 99 -
115, Replace the regex-based assertions in the two tests that call
resolveHandlerUrls with snapshot assertions: change the
expect(...).toThrowError(/OAuth callback URLs must be relative/) calls to use
toThrowErrorMatchingInlineSnapshot and capture the exact error message string
produced by resolveHandlerUrls (for both the string target and the { type:
"custom", url, version } case) so the tests assert the precise error text;
update the two test cases (the "rejects absolute OAuth callback string targets"
and "rejects absolute OAuth callback custom targets") to call
toThrowErrorMatchingInlineSnapshot and record the inline snapshot of the thrown
error message.
🤖 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 `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 692-704: The pending-auth deadlock happens because handlers passed
to _trackPendingAuthResolution await async work (callOAuthCallback /
_maybeHandleNestedCrossDomainAuth) and callers like MFA fallback call
_getSession which awaits the same pending chain; fix by ensuring the tracked
handlers are launched asynchronously and not awaited on the same promise chain —
change calls where you do this._trackPendingAuthResolution(async () => { await
this.callOAuthCallback(...) }) and similar for _maybeHandleNestedCrossDomainAuth
to start the async work without blocking the tracker (e.g. invoke the async
handler in a detached microtask: Promise.resolve().then(() =>
this.callOAuthCallback({...})) or otherwise call the handler without awaiting
inside the tracked callback) so _getSession and MFA fallback cannot deadlock;
update all occurrences (including the blocks around callOAuthCallback and
_maybeHandleNestedCrossDomainAuth mentioned) to use this non-blocking invocation
pattern.

---

Nitpick comments:
In `@packages/stack-shared/src/utils/redirect-urls.tsx`:
- Around line 4-7: TrustedDomainConfig.trustedDomains uses readonly (string |
null | undefined)[] but getTrustedParentDomain currently expects readonly
string[]; unify the element type by changing getTrustedParentDomain's trusted
domains parameter to accept readonly (string | null | undefined)[] (same as
TrustedDomainConfig.trustedDomains) and perform internal filtering of
null/undefined values inside getTrustedParentDomain (mirroring
validateRedirectUrl) so callers don’t need to coerce or filter; update any other
exported APIs that accept trusted domain arrays to use the unified element type
as well (e.g., the other occurrence referenced alongside
getTrustedParentDomain).

In `@packages/template/src/lib/stack-app/url-targets.test.ts`:
- Around line 99-115: Replace the regex-based assertions in the two tests that
call resolveHandlerUrls with snapshot assertions: change the
expect(...).toThrowError(/OAuth callback URLs must be relative/) calls to use
toThrowErrorMatchingInlineSnapshot and capture the exact error message string
produced by resolveHandlerUrls (for both the string target and the { type:
"custom", url, version } case) so the tests assert the precise error text;
update the two test cases (the "rejects absolute OAuth callback string targets"
and "rejects absolute OAuth callback custom targets") to call
toThrowErrorMatchingInlineSnapshot and record the inline snapshot of the thrown
error message.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9719bf2b-3cdb-43f9-89c9-fcadc4c413cf

📥 Commits

Reviewing files that changed from the base of the PR and between 0e85b05 and 09ec01f.

📒 Files selected for processing (10)
  • .claude/CLAUDE-KNOWLEDGE.md
  • apps/backend/src/lib/redirect-urls.tsx
  • apps/e2e/tests/js/cross-domain-auth.test.ts
  • examples/demo/src/stack.tsx
  • packages/stack-shared/src/interface/crud/projects.ts
  • packages/stack-shared/src/utils/redirect-urls.tsx
  • packages/template/src/lib/auth.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/url-targets.test.ts
  • packages/template/src/lib/stack-app/url-targets.ts

Comment thread packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR implements nested cross-domain handoffs: when a signed-in user on a.com is redirected to a hosted Stack handler on b.com, the client SDK automatically attaches a PKCE-based token exchange that transfers the session across domains. It also adds a hosted OAuth callback mode where the current page URL acts as the redirect URI instead of a fixed /handler/oauth-callback path.

  • Nested cross-domain auth: _maybeHandleNestedCrossDomainAuth tags cross-domain handler redirects with a refreshTokenId and callback URL; the target domain initiates a PKCE exchange back to the source domain, which validates the session and mints a one-time code.
  • Hosted OAuth callback: _isOAuthCallbackUrlHosted / _getOAuthCallbackRedirectUri allow any page to act as the OAuth callback; assertOAuthCallbackTargetIsRelative guards against misconfiguration; startup pending-auth-resolution tracking ensures session consumers wait for the callback to be processed.
  • Shared redirect-URL utilities: validateRedirectUrl, getTrustedParentDomain, and isAcceptedNativeAppUrl are extracted from the backend into stack-shared; allow_localhost is added to the client-readable project schema so the same trust rules can be applied client-side.

Confidence Score: 3/5

The nested cross-domain auth flow is architecturally sound and well-tested, but two gaps in the new hosted-callback path could cause real failures before this is safe to ship.

On the source domain, the redirectUri extracted from URL params is forwarded to _createCrossDomainAuthRedirectUrl without any client-side trust check — trust validation exists on the target-domain side but is asymmetrically absent here. Separately, _getOAuthCallbackRedirectUri({ forCallback: true }) strips only code and state, leaving OAuth error params in the redirect URI; a page that previously saw an OAuth error will misclassify a subsequent successful OAuth callback as a failure.

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts requires the most attention — specifically _maybeHandleNestedCrossDomainAuth (source-domain branch) and _getOAuthCallbackRedirectUri.

Important Files Changed

Filename Overview
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Core change: adds nested cross-domain auth initiation/handling, pending auth resolution tracking, and hosted OAuth callback redirect URI logic. Two issues found: redirectUri passed without client-side trust check, and hosted callback URI not stripping OAuth error params.
packages/stack-shared/src/utils/redirect-urls.tsx New shared utility extracting validateRedirectUrl, isAcceptedNativeAppUrl, and getTrustedParentDomain from backend-only code. Logic is functionally equivalent to what was previously in the backend.
packages/stack-shared/src/interface/crud/projects.ts Exposes allow_localhost in the client-readable project schema so the client can apply the same localhost-trust rules as the backend for nested cross-domain auth URL validation.
packages/template/src/lib/stack-app/url-targets.ts Adds assertOAuthCallbackTargetIsRelative guard that throws when an absolute URL is configured as the OAuth callback target, preventing misuse of hosted-callback mode.
apps/e2e/tests/js/cross-domain-auth.test.ts Adds six new E2E-style unit tests covering the nested cross-domain auth round-trip, security rejections (untrusted callback, session mismatch), and the updated OAuth callback redirect-URI behaviour.

Sequence Diagram

sequenceDiagram
    participant User
    participant SourceApp as Source App (a.com)
    participant TargetHandler as Hosted Handler (b.com)
    participant Backend as Stack Backend

    User->>SourceApp: Click link to hosted handler
    SourceApp->>SourceApp: _addNestedCrossDomainAuthParamsToRedirectUrl
    Note over SourceApp: Appends refreshTokenId + callbackUrl to URL
    SourceApp->>TargetHandler: Redirect with nested auth params

    TargetHandler->>TargetHandler: _maybeHandleNestedCrossDomainAuth
    TargetHandler->>TargetHandler: _isTrusted(callbackUrl)
    TargetHandler->>TargetHandler: Generate PKCE state and challenge
    TargetHandler->>SourceApp: Redirect with OAuth request params

    SourceApp->>SourceApp: _maybeHandleNestedCrossDomainAuth (source role)
    SourceApp->>SourceApp: Verify session matches refreshTokenId
    SourceApp->>Backend: _createCrossDomainAuthRedirectUrl
    Backend-->>SourceApp: URL with one-time auth code
    SourceApp->>TargetHandler: Redirect with code and state

    TargetHandler->>TargetHandler: callOAuthCallback
    TargetHandler->>Backend: Exchange code for session tokens
    Backend-->>TargetHandler: Session tokens
    TargetHandler->>User: Signed in on target domain
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts:844-872
**Missing client-side trust validation for `redirectUri` on source domain**

When a.com acts as the OAuth-code issuer in the nested flow, the `redirectUri` comes directly from URL query params supplied by b.com and is passed straight into `_createCrossDomainAuthRedirectUrl` without first calling `_isTrusted()` on it. Per custom rule `45e85dee`, redirect URLs must always be validated before use. An attacker who can lure the user to a crafted a.com URL with a matching `refreshTokenId` and a malicious `redirect_uri` would receive the cross-domain auth code. Adding an `_isTrusted(redirectUri)` guard before the `_createCrossDomainAuthRedirectUrl` call mirrors the existing check already applied on b.com via `_isTrusted(callbackUrlString)`.

### Issue 2 of 3
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts:786-800
**OAuth error params not stripped from hosted redirect URI**

`_getOAuthCallbackRedirectUri({ forCallback: true })` removes only `code` and `state`, but not the OAuth error params consumed by `consumeOAuthCallbackQueryParams` (`error`, `error_description`, `errorCode`, `message`, `details`). If a previous failed OAuth attempt left `errorCode` in the browser URL and the user retries OAuth from the same page, those stale params are baked into the registered `redirect_uri`. After the new successful OAuth flow completes, `consumeOAuthCallbackQueryParams` detects the stale error params first and reports the callback as a failure even though `code`+`state` are also present. Fixed-path callbacks like `/handler/oauth-callback` are never affected by this.

### Issue 3 of 3
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts:723-745
**Auth-resolution errors propagate unguarded to all session consumers**

When a pending resolution throws (e.g., `_maybeHandleNestedCrossDomainAuth` detecting an untrusted callback URL or mismatched session), the raw rejection is stored in `_pendingAuthResolutionPromises`. Every subsequent call to `_getSession` hits `Promise.all(...)` on those promises and surfaces internal errors to ordinary application code calling `getUser()`. A user navigating to a hosted handler via a link with stale nested auth params would see the page break entirely until the `finally` cleanup runs.

Reviews (1): Last reviewed commit: "Cross domain handoffs" | Re-trigger Greptile

Comment thread packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Outdated
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

Caution

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

⚠️ Outside diff range comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

692-698: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t auto-start hosted OAuth callback handling on every page load.

When oauthCallback is hosted, the constructor now queues callOAuthCallback() for every app instance. That makes hosted apps pay this path on ordinary pages, and pages that happen to use generic code/state or errorCode/message query params can be misclassified as Stack OAuth returns. Please gate this on a Stack-owned callback signal, not just on the hosted config.

🤖 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 `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 692 - 698, The constructor currently auto-queues
callOAuthCallback() when isBrowserLike() && this._isOAuthCallbackUrlHosted(),
which triggers hosted OAuth handling on every page; change the gating to require
a Stack-owned callback signal: add or use a validation method (e.g.
_isStackOAuthCallbackRequest()) and only call
this._trackPendingAuthResolution(...) when that method returns true. Implement
_isStackOAuthCallbackRequest() to verify the current window location matches the
expected Stack callback (for example by hostname/path matching the configured
oauth callback URL or by requiring a Stack-specific query marker or signed param
in addition to the usual code/state or errorCode/message), then update the check
in the constructor to: if (isBrowserLike() && this._isOAuthCallbackUrlHosted()
&& this._isStackOAuthCallbackRequest()) { ... } and keep calling await
this.callOAuthCallback({ dontWarnAboutMissingQueryParams: true }) inside the
existing _trackPendingAuthResolution wrapper.
🤖 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 `@packages/stack-shared/src/utils/redirect-urls.tsx`:
- Around line 38-52: hostPatternWithoutPort()/hostPatternHasExplicitPort()
currently only treats numeric ports as explicit so patterns ending with ":*"
remain and break normalization; update hostPatternHasExplicitPort to treat a
single "*" (and still numeric strings) as an explicit port (i.e., return true
when port === "*" OR every char is a digit) so hostPatternWithoutPort will strip
both numeric and wildcard port suffixes before hostname matching (refer to
functions hostPatternWithoutPort and hostPatternHasExplicitPort).

---

Outside diff comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 692-698: The constructor currently auto-queues callOAuthCallback()
when isBrowserLike() && this._isOAuthCallbackUrlHosted(), which triggers hosted
OAuth handling on every page; change the gating to require a Stack-owned
callback signal: add or use a validation method (e.g.
_isStackOAuthCallbackRequest()) and only call
this._trackPendingAuthResolution(...) when that method returns true. Implement
_isStackOAuthCallbackRequest() to verify the current window location matches the
expected Stack callback (for example by hostname/path matching the configured
oauth callback URL or by requiring a Stack-specific query marker or signed param
in addition to the usual code/state or errorCode/message), then update the check
in the constructor to: if (isBrowserLike() && this._isOAuthCallbackUrlHosted()
&& this._isStackOAuthCallbackRequest()) { ... } and keep calling await
this.callOAuthCallback({ dontWarnAboutMissingQueryParams: true }) inside the
existing _trackPendingAuthResolution wrapper.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82310389-c795-4816-8e0d-e0355ecc71b2

📥 Commits

Reviewing files that changed from the base of the PR and between 09ec01f and eb49655.

📒 Files selected for processing (5)
  • apps/e2e/tests/js/cross-domain-auth.test.ts
  • packages/stack-shared/src/utils/redirect-urls.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/url-targets.test.ts
  • packages/template/src/lib/stack-app/url-targets.ts

Comment thread packages/stack-shared/src/utils/redirect-urls.tsx Outdated
Comment thread packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Outdated
Comment thread packages/stack-shared/src/utils/redirect-urls.tsx Outdated
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)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

694-699: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep hosted OAuth error callbacks on the auto-start path.

callOAuthCallback() still recognizes errorCode + message, but the constructor now only auto-starts when _currentUrlLooksLikeStackOAuthCallback() sees code + state plus the verifier cookie. Hosted OAuth failures will now land on the callback page and never get processed. Keep the cookie requirement for the success path, but preserve the existing error-shape branch here too.

Suggested fix
 protected _currentUrlLooksLikeStackOAuthCallback(): boolean {
   if (typeof window === "undefined") {
     return false;
   }
   const currentUrl = new URL(window.location.href);
+  if (currentUrl.searchParams.has("errorCode") && currentUrl.searchParams.has("message")) {
+    return true;
+  }
   const state = currentUrl.searchParams.get("state");
   if (!currentUrl.searchParams.has("code") || state == null) {
     return false;
   }
   return getCookieClient(`stack-oauth-outer-${state}`) != null;
 }

Also applies to: 794-804

🤖 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 `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 694 - 699, The current auto-start gate uses
_currentUrlLooksLikeStackOAuthCallback() plus the verifier cookie, which blocks
hosted OAuth error callbacks; update the wrapper in the
_trackPendingAuthResolution call to allow an alternative branch that invokes
callOAuthCallback when the URL contains the OAuth error shape (errorCode and
message) even if the verifier cookie is missing. Specifically, change the
if-condition around
_isOAuthCallbackUrlHosted()/_currentUrlLooksLikeStackOAuthCallback() so it
triggers when either (a) the success path conditions are met (existing
_currentUrlLooksLikeStackOAuthCallback() and verifier cookie) or (b) the URL
looks like an OAuth error (check for errorCode and message), and then call
callOAuthCallback({ dontWarnAboutMissingQueryParams: true }) in that branch;
apply the same change to the second similar block (the one around lines 794-804)
to preserve hosted error callbacks.
🧹 Nitpick comments (1)
apps/e2e/tests/js/cross-domain-auth.test.ts (1)

189-191: ⚡ Quick win

Use inline snapshots for these new Vitest assertions.

Please switch these new expectations to .toMatchInlineSnapshot(...) to match the repo’s test-style rule.

Example adjustment
-      expect((clientApp as any)._currentUrlLooksLikeStackOAuthCallback()).toBe(false);
+      expect((clientApp as any)._currentUrlLooksLikeStackOAuthCallback()).toMatchInlineSnapshot(`false`);
       globalThis.document.cookie = "stack-oauth-outer-oauth-state=verifier";
-      expect((clientApp as any)._currentUrlLooksLikeStackOAuthCallback()).toBe(true);
+      expect((clientApp as any)._currentUrlLooksLikeStackOAuthCallback()).toMatchInlineSnapshot(`true`);

As per coding guidelines **/*.test.{ts,tsx}: “When writing tests, prefer .toMatchInlineSnapshot over other selectors in Vitest tests.”

Also applies to: 254-257, 291-293

🤖 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 `@apps/e2e/tests/js/cross-domain-auth.test.ts` around lines 189 - 191, Replace
the boolean equality assertions on (clientApp as
any)._currentUrlLooksLikeStackOAuthCallback() with inline snapshots: change
expect((clientApp as any)._currentUrlLooksLikeStackOAuthCallback()).toBe(false)
to expect((clientApp as
any)._currentUrlLooksLikeStackOAuthCallback()).toMatchInlineSnapshot('false')
and the .toBe(true) case to .toMatchInlineSnapshot('true'); apply the same
change for the other identical assertions in this test file (the other
occurrences around the later assertions that check the same
_currentUrlLooksLikeStackOAuthCallback() result).
🤖 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.

Outside diff comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 694-699: The current auto-start gate uses
_currentUrlLooksLikeStackOAuthCallback() plus the verifier cookie, which blocks
hosted OAuth error callbacks; update the wrapper in the
_trackPendingAuthResolution call to allow an alternative branch that invokes
callOAuthCallback when the URL contains the OAuth error shape (errorCode and
message) even if the verifier cookie is missing. Specifically, change the
if-condition around
_isOAuthCallbackUrlHosted()/_currentUrlLooksLikeStackOAuthCallback() so it
triggers when either (a) the success path conditions are met (existing
_currentUrlLooksLikeStackOAuthCallback() and verifier cookie) or (b) the URL
looks like an OAuth error (check for errorCode and message), and then call
callOAuthCallback({ dontWarnAboutMissingQueryParams: true }) in that branch;
apply the same change to the second similar block (the one around lines 794-804)
to preserve hosted error callbacks.

---

Nitpick comments:
In `@apps/e2e/tests/js/cross-domain-auth.test.ts`:
- Around line 189-191: Replace the boolean equality assertions on (clientApp as
any)._currentUrlLooksLikeStackOAuthCallback() with inline snapshots: change
expect((clientApp as any)._currentUrlLooksLikeStackOAuthCallback()).toBe(false)
to expect((clientApp as
any)._currentUrlLooksLikeStackOAuthCallback()).toMatchInlineSnapshot('false')
and the .toBe(true) case to .toMatchInlineSnapshot('true'); apply the same
change for the other identical assertions in this test file (the other
occurrences around the later assertions that check the same
_currentUrlLooksLikeStackOAuthCallback() result).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 413264f7-c689-455e-b9e9-659893a526ab

📥 Commits

Reviewing files that changed from the base of the PR and between 237c307 and 7c34aed.

📒 Files selected for processing (4)
  • .claude/CLAUDE-KNOWLEDGE.md
  • apps/e2e/tests/js/cross-domain-auth.test.ts
  • packages/stack-shared/src/utils/redirect-urls.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
✅ Files skipped from review due to trivial changes (1)
  • .claude/CLAUDE-KNOWLEDGE.md

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dfb6d61. Configure here.

protected _getLocalOAuthCallbackHandlerUrl(): string {
if (this._isOAuthCallbackUrlHosted()) {
return this._getOAuthCallbackRedirectUri();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server-side crash when hosted OAuth callback URL evaluated eagerly

Medium Severity

_getLocalOAuthCallbackHandlerUrl() is eagerly evaluated as an argument to planRedirectToHandler at line 2878, even in server-side contexts where currentUrl is null. When _isOAuthCallbackUrlHosted() returns true (e.g., with default: { type: "hosted" } as in the demo app change), it calls _getOAuthCallbackRedirectUri() which throws a StackAssertionError when window is undefined. Although planRedirectToHandler never uses localOAuthCallbackUrl when currentUrl is null (it returns early), JavaScript evaluates the argument before the call. This makes any server-side invocation of _redirectToHandler crash when the OAuth callback is configured as hosted.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dfb6d61. Configure here.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 10 files

Re-trigger cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants