Skip to content

Fix infinite sign-in redirect loop under base-path deploys#1692

Closed
aryeh-stark wants to merge 3 commits into
BuilderIO:mainfrom
21-Stark-AI:fix/signin-redirect-loop-guard
Closed

Fix infinite sign-in redirect loop under base-path deploys#1692
aryeh-stark wants to merge 3 commits into
BuilderIO:mainfrom
21-Stark-AI:fix/signin-redirect-loop-guard

Conversation

@aryeh-stark

Copy link
Copy Markdown
Contributor

Problem

RequireSession has no guard against redirecting to the sign-in page from the sign-in page. When the authenticated app shell (wrapped in RequireSession) is served at the sign-in path — e.g. a base-path deploy where the SPA answers /<app>/_agent-native/sign-in with no client-resolvable session — it redirects sign-in → sign-in, nesting and re-encoding the current URL as a fresh ?return= on every hop:

/<app>/_agent-native/sign-in?return=%2F<app>%2F_agent-native%2Fsign-in%3Freturn%3D%252F<app>%252F…

…growing without bound until the browser/server gives up. Hit in production behind a /<app> path prefix once a session cookie existed but the client session check couldn't resolve it.

Fix

  • RequireSession: new exported isOnSignInPage() helper; the gate never redirects when window.location.pathname already equals agentNativePath("/_agent-native/sign-in") (matches with or without an app base path). Falls through to the loading fallback instead.
  • safeReturnPath (server): collapse any return that resolves back to …/_agent-native/sign-in to /, so the post-sign-in 302 can't re-enter the loop either (defense in depth).

Tests

  • require-session.spec.tsx: new case — never redirects when already on the sign-in page.
  • auth.spec.ts: safeReturnPath collapses sign-in returns (root + base-path + nested-encoded), leaves normal app paths intact.

Includes a changeset (patch, @agent-native/core).

RequireSession had no guard against redirecting to the sign-in page from the
sign-in page. When the app shell is served at the sign-in path (e.g. a base-path
deploy where the SPA answers /<app>/_agent-native/sign-in with no client-
resolvable session), it redirected sign-in -> sign-in, nesting and re-encoding
the current URL as a fresh ?return= on every hop (the runaway
.../sign-in?return=%252F...sign-in URL).

- RequireSession: new isOnSignInPage() guard — never redirect to sign-in when
  already on it.
- safeReturnPath: collapse any return that resolves back to
  .../_agent-native/sign-in to "/" (server-side defense in depth).

Adds tests for both paths and a changeset.
@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Visual recap — skipped

The visual recap job did not run for this pull request. This is informational only and does not block the PR.

Recap skipped for bd01ac2: external fork PR requires a maintainer to apply the recap label to the current head SHA.

@builder-io-integration builder-io-integration 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.

Builder reviewed your changes and found 1 potential issue 🔴

Review Details

This PR fixes an auth redirect loop in two layers: RequireSession now avoids redirecting when the browser is already on the framework sign-in entrypoint, and safeReturnPath() collapses sign-in self-returns to prevent post-auth re-entry into the same loop. The overall approach is sound and the added tests are targeted and useful. I classified this as high risk because it changes authentication/session redirect behavior in shared core code that affects mounted/base-path deployments.

Key finding:

  • 🔴 HIGH — the new server-side collapse-to-"/" behavior loses the app base path in the /_agent-native/sign-in success redirect, so base-path deployments can send authenticated users to the host root instead of the mounted app.

Good patterns here: the fix adds defense in depth across client and server, preserves normal return-path sanitization, and documents the production failure mode clearly in tests/changelog. However, the remaining base-path regression means the auth flow is not yet safe to merge as-is.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +545 to +550
// Never return to the sign-in entry point itself. A `return` that resolves
// back to `…/_agent-native/sign-in` re-enters the redirect loop — each hop
// nests the prior sign-in URL as a fresh `?return=`. Collapse it to "/" so
// the post-sign-in 302 lands on the app, not another sign-in page. Matches
// with or without an app base-path prefix (`/<app>/_agent-native/sign-in`).
if (parsed.pathname.endsWith("/_agent-native/sign-in")) return "/";

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.

🔴 Loop guard drops base-path deployments onto site root

When safeReturnPath() collapses a looping sign-in return to /, the /_agent-native/sign-in handler later redirects to that raw path instead of remapping it to getAppBasePath() || "/". On base-path deploys, a successful sign-in can therefore land on the host root (or 404) instead of the mounted app.

Additional Info
Found by 2/4 review passes; validated against auth.ts lines 1798-1808 (`Location: safeReturn`) and 1819-1827 (existing base-path-aware `/login`/`/signup` handling).

Fix in Builder

@steve8708

Copy link
Copy Markdown
Contributor

thanks @aryeh-stark ! one comment above looks useful to fix

OAuth client credentials (GOOGLE_CLIENT_ID/SECRET, GOOGLE_SIGN_IN_CLIENT_*,
GITHUB_CLIENT_*) are deployment-wide identity, not per-tenant secrets: every
user authorizes the same app for their own account and gets their own tokens.
But resolveSecret refused to fall back to the deploy env for signed-in users in
a hosted runtime, so a deployment that set GOOGLE_CLIENT_* still showed each
logged-in user the "bring your own OAuth app" setup wizard and /google/auth-url
returned missing_credentials. Let these keys use the deploy env fallback (same
as Builder credential keys); a per-tenant BYO client still wins (resolved
first). Also fix GoogleConnectBanner computing "configured" across every env
key (Slack/Resend/LLMs) instead of just the Google client keys.

@builder-io-integration builder-io-integration 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.

Builder reviewed your changes and found 1 potential issue 🟡

Review Details

This incremental update expands the PR beyond the original redirect-loop fix: it now also changes hosted secret resolution for OAuth client credentials and adjusts Mail’s Google connect readiness UI. I reviewed the updated auth/session, credential-resolution, and Mail UI paths at high risk because they affect shared authentication behavior, Google OAuth configuration, and cross-tenant secret handling in core code.

Current state: the previously reported blocking auth issue in packages/core/src/server/auth.ts still appears unresolved and remains visible on the PR, so the branch is still not ready to merge. In the new changes, I found one additional substantive regression in OAuth credential resolution and one lower-severity Mail UI state issue.

Key findings:

  • 🔴 HIGH (existing unresolved) — base-path sign-in loop guard still redirects authenticated users to host root.
  • 🟡 MEDIUM (new) — deploy-env fallback now resolves OAuth client keys independently, which can mix tenant and deploy credentials into an invalid pair.
  • LOW (new) — Mail’s banner still uses all env keys for most readiness UI, so unrelated optional secrets can keep Google setup looking incomplete.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +996 to +1004
const allowDeployFallback = isBuilderCredentialKey(key)
? canUseBuilderDeployCredentialFallbackForRequest()
: isDeploymentWideOAuthClientKey(key)
? // A shared OAuth client is deployment-wide identity, not a per-tenant
// secret — safe to use for authenticated users (BYO per-tenant client
// is still resolved first, above). See DEPLOYMENT_WIDE_OAUTH_CLIENT_KEYS.
true
: canUseDeployCredentialFallbackForRequest();
const envFallback = allowDeployFallback ? process.env[key] || null : null;

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.

🟡 OAuth client env fallback can silently mix tenant and deploy credentials

resolveSecret() now allows deploy-level fallback for Google OAuth client keys independently, so a tenant-scoped GOOGLE_CLIENT_ID can be paired with a deploy-scoped GOOGLE_CLIENT_SECRET (or vice versa). Mail and Calendar resolve those keys separately, which turns partial overrides into broken invalid_client/unauthorized_client OAuth flows instead of a clean missing-credentials state.

Additional Info
Found by 1/3 review passes; validated against templates/mail/server/lib/google-auth.ts:59-62 and templates/calendar/server/handlers/google-auth.ts:48-53, both of which resolve the pair via separate resolveSecret calls.

Fix in Builder

defaultOrgAppLinks hardcoded each template's *.agent-native.com prod URL, so a
path-prefixed deployment (served at <origin>/<app>/, e.g. apps.example.com/mail/)
linked every sibling app to the first-party hosted site instead of the current
deployment. Build <origin>/<app>/ links when the client is path-prefixed
(VITE_APP_BASE_PATH baked at build); keep the prod URLs for the subdomain layout.

@builder-io-integration builder-io-integration 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.

Builder reviewed your changes and found 1 potential issue 🟡

Review Details

This incremental update adds a path-prefixed org-switcher link change in workspace-app-links.ts on top of the earlier auth redirect-loop and OAuth credential fallback work, and it also attempts to tighten Mail’s Google setup readiness logic. I reviewed the updated branch at high risk because it still touches shared authentication flow, hosted credential resolution, and cross-app navigation/UI state in shared core code.

Current status: the two previously reported issues are still present and I left those existing comments in place rather than reposting them. The auth-side base-path redirect bug in packages/core/src/server/auth.ts remains the blocking item. I also found one additional UI-state regression in the new Mail banner logic.

Key findings:

  • 🔴 HIGH (existing unresolved) — authenticated /_agent-native/sign-in requests can still redirect base-path deployments to / instead of the mounted app.
  • 🟡 MEDIUM (existing unresolved) — OAuth client env fallback can still mix tenant and deploy credentials.
  • 🟡 MEDIUM (new) — Mail’s Google setup flow now uses a Google-only readiness check in fetchStatus(), but most render branches still key off envStatus.every(...), so unrelated optional secrets can keep the banner in the “not configured” state.

Good changes: the org-switcher path-prefix tests are targeted, and the sign-in loop guard / Google-credential helper remain easy to follow. However, with the unresolved auth bug and the new Mail UI inconsistency, this branch is not ready to merge.

🧪 Browser testing: Attempted, but the browser executor still lacks Chrome automation tools in this environment; I could only gather server-side evidence, not visual confirmation.

setEnvStatus(data);
const allConfigured = data.every((k) => k.configured);
if (allConfigured && data.length > 0) {
if (googleCredsConfigured(data)) {

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.

🟡 Google setup UI still depends on unrelated env keys

fetchStatus() now treats only the Google client keys as the readiness signal, but the rest of the component still branches on envStatus.every((k) => k.configured). That means missing optional keys like Slack/Anthropic/A2A can still leave the banner in the “not configured” state even when Google is ready.

Additional Info
Validated manually against GoogleConnectBanner.tsx: `googleCredsConfigured(data)` is used at line 199, while render state still uses `allConfigured = envStatus.length > 0 && envStatus.every(...)` for banner copy, CTA, and wizard visibility.

Fix in Builder

@aryeh-stark

Copy link
Copy Markdown
Contributor Author

Superseded by three focused PRs, one per independent fix: #1712 (sign-in redirect loop), #1713 (shared deployment OAuth client), #1714 (app-switcher same-origin links). Closing this combined PR.

@aryeh-stark aryeh-stark closed this Jul 1, 2026
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