Skip to content

fix(auth): surface OAuth errors on /auth/callback instead of silent timeout - #1258

Merged
ooples merged 5 commits into
masterfrom
fix/oauth-callback-error-visibility
May 5, 2026
Merged

ooples merged 5 commits into
masterfrom
fix/oauth-callback-error-visibility

Conversation

@ooples

@ooples ooples commented May 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Diagnostic complement to #1257 (GitHub OAuth broken on aidotnet.dev). The redirect to GitHub itself works — the failure is downstream, in the post-GitHub callback. The current `/auth/callback` page silently swallows URL-parameter errors, so any Supabase-side provider failure surfaces as a generic 10-second timeout with no actionable detail.

OAuth providers can fail at three different layers, each surfacing the error in a different place:

  1. PKCE / code-flow error → URL search params: `?error=...&error_description=...`
  2. Implicit-flow error → URL hash params: `#error=...&error_description=...`
  3. Session-exchange error → returned from `supabase.auth.getSession()`

The old code only checked layer 3 — that's why hitting `Continue with GitHub` could land here with `?error=server_error&error_description=...` and the user would still just see "Authentication timed out" after 10 seconds.

This PR:

  • Parses BOTH search and hash params for `error`, `error_code`, `error_description`
  • Renders the raw `error_description` in a monospace block so support / customer can copy it
  • Adds best-effort hints for the most common failure modes:
    • `server_error` → "OAuth provider misconfigured server-side; client secret rotated, callback URL drift, or provider disabled"
    • `access_denied` → "You declined permissions; try again or use a different method"
    • `unsupported_provider` / "not enabled" → "This sign-in provider is currently disabled on the server"
  • Keeps the 10-second fallback for the third-party-cookie-block case, but gives an actionable hint
  • `console.error`s every failure path so devtools captures it

Diagnostic data captured during this investigation

Headless probe of `/login` clicking `Continue with GitHub` from incognito:

So the first half of the OAuth flow is correct. The bug is in the post-GitHub callback. Most likely causes (cannot verify without dashboard access):

  1. GitHub OAuth app's `Authorization callback URL` doesn't match `https://yfkqwpgjahoamlgckjib.supabase.co/auth/v1/callback\` exactly — common after a Supabase project rename or regeneration. Fix: GitHub OAuth app settings → Authorization callback URL.
  2. Client secret rotated on the GitHub side and Supabase still has the old one. Fix: regenerate secret on GitHub, paste into Supabase Auth → Providers → GitHub → Client Secret.
  3. GitHub user has no public email AND the GitHub app doesn't request `read:user`. Fix: in Supabase auth provider config or in the OAuth app permissions, ensure email is requested.

After this PR ships, the user will SEE which of those three is firing on their next sign-in attempt — the page will display the actual `error_description` from Supabase instead of the generic timeout.

Test plan

  • Regression: trigger a normal Google sign-in; verify the redirect-and-session-exchange path still works (no behavior change when `error` is absent from the URL).
  • Manually navigate to `/auth/callback?error=server_error&error_description=test` — verify the error block renders with the description in a monospace block and the "server_error" hint.
  • Manually navigate to `/auth/callback#error=access_denied&error_description=user+denied` — verify hash params are parsed and the "access_denied" hint shows.
  • Trigger a real GitHub sign-in attempt (after dashboard fix per Login: GitHub OAuth broken on aidotnet.dev (only Google works) #1257) — verify the sign-in completes successfully (callback page redirects to `/account/`).

Out of scope

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved detection/precedence of OAuth/provider errors from both URL query and fragment; explicit handling for session-exchange failures and 10s timeout with clear messaging.
  • UI/UX Improvements

    • New HTML-safe fatal error view with contextual hints, “Back to login” link, spinner/heading/subtext stability, and wider error container for readability.
  • Tests

    • Added E2E tests for callback error rendering, precedence rules, session errors, timeout behavior, and edge-case decoding.
  • Chores

    • Added a dedicated auth e2e project to isolate auth flow tests.

Copilot AI review requested due to automatic review settings May 4, 2026 18:02
@vercel

vercel Bot commented May 4, 2026 •

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
aidotnet_website Ignored Ignored May 4, 2026 11:23pm
aidotnet-playground-api Ignored Ignored May 4, 2026 11:23pm

@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown
Contributor

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

The OAuth callback page now parses OAuth errors from URL search and hash params (hash wins) and immediately renders a sanitized fatal-error UI when present; otherwise it exchanges or waits for a Supabase session (10s unsubscribe timeout) and redirects on sign-in. Stable DOM IDs and spinner/error UI were added. BLOCKING: no stubs/TODOs detected.

Changes

Auth callback + E2E coverage

Layer / File(s) Summary
Data Shape / Parsing
website/src/pages/auth/callback.astro
Adds AuthErrorParts and parseAuthError() to extract error, error_code, and error_description from window.location.search and window.location.hash with hash precedence.
Error Rendering
website/src/pages/auth/callback.astro
Adds showFatalError() that escapes content, hides #auth-spinner, and populates #auth-heading, #auth-subtext, and #auth-error with a structured error block (headline, escaped <pre> technical details, optional hint, and a "Back to login" link).
Core Callback Flow
website/src/pages/auth/callback.astro
Replaces prior flow: prioritizes URL errors (renders fatal UI), otherwise calls supabase.auth.getSession() (renders fatal on SDK error, redirects if session), or subscribes to onAuthStateChange waiting for SIGNED_IN. Enforces a 10s timeout that unsubscribes, logs a specific timeout console error, and shows a timeout fatal message.
UI Markup / Targeting
website/src/pages/auth/callback.astro
Updates markup: adds stable IDs auth-spinner, auth-heading, auth-subtext, auth-error; changes error container to max-w-xl and sets ARIA alert attributes; removes old showError() flow.
E2E Tests
website/tests/e2e/auth/callback.spec.ts
Adds tests covering URL-error rendering (search/hash parsing, hash precedence, provider hints), non-URL failure paths (stubbed getSession() error and 10s timeout with console error), and percent-decoding regression for % in error_description.
Test Runner Config
website/playwright.config.ts
Adds a new Playwright project auth-anon scoped to ./tests/e2e/auth, excludes **/*.setup.ts, and sets a 1440×900 viewport.

Sequence Diagram

sequenceDiagram
  participant Browser as Browser (page)
  participant Callback as CallbackScript
  participant Supabase as Supabase SDK
  participant Redirect as Redirect Target
  Browser->>Callback: load /auth/callback (with search/hash)
  Callback->>Callback: parseAuthError()
  alt URL-embedded error present
    Callback-->>Browser: render fatal error UI (showFatalError)
  else no URL error
    Callback->>Supabase: supabase.auth.getSession()
    alt session returned
      Callback->>Redirect: location.replace(redirect)
    else no session
      Callback->>Supabase: onAuthStateChange listen for SIGNED_IN
      Supabase->>Callback: auth state = SIGNED_IN
      Callback->>Redirect: location.replace(redirect)
      Note right of Callback: 10s timeout -> unsubscribe, console.error, show fatal timeout UI
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🔐 A callback reads the URL line by line,
It picks the hash or query, then draws the sign.
Spinner fades, the error speaks clear,
Hints and details—no guessing here.
Back to login, tidy and fine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: fixing OAuth error visibility on the /auth/callback page instead of showing a silent timeout. It matches the core objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 fix/oauth-callback-error-visibility

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: 2

🤖 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 `@website/src/pages/auth/callback.astro`:
- Around line 157-177: The setTimeout started for the 10s fallback isn’t cleared
when the SIGNED_IN handler in supabase.auth.onAuthStateChange runs, so
showFatalError can still fire after navigation; store the timeout ID (e.g.,
const timeoutId = setTimeout(...)) and call clearTimeout(timeoutId) inside the
SIGNED_IN branch (before subscription.unsubscribe() and before setting
window.location.href) to prevent the error callback from running after a
successful sign-in.
- Around line 111-116: The code currently calls decodeURIComponent directly when
building detailLines (symbol: detailLines) which can throw on malformed
percent-encodings and also uses the raw urlError.errorDescription later for hint
checks; create a small safe decode helper (e.g., tryDecodeURIComponent) that
tries decodeURIComponent(errorDescription) in a try/catch and on failure returns
the original raw string (after replacing '+' with space) or a safe placeholder,
then compute a decodedDescription variable from urlError.errorDescription using
that helper and use decodedDescription both in the detailLines array (replace
the inline decode) and in the later hint check (the check currently at line
~129) so the UI won't crash and the hint triggers consistently.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: efa78051-d738-4ac1-874c-4fa3d84c2dd6

📥 Commits

Reviewing files that changed from the base of the PR and between 25c7918 and 5eb64a1.

📒 Files selected for processing (1)
  • website/src/pages/auth/callback.astro

Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro

Copilot AI 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.

Pull request overview

This PR updates the website’s OAuth callback page so authentication failures from Supabase/GitHub are surfaced directly in the UI instead of degrading into a generic timeout. It fits into the website auth flow by improving diagnostics on the /auth/callback handoff between the external provider and the existing Supabase session logic.

Changes:

  • Adds parsing of OAuth failures from both query-string and hash parameters on /auth/callback.
  • Replaces the old generic error rendering with a structured failure state, including raw provider details and tailored hints for common OAuth misconfiguration cases.
  • Keeps the existing session-based success redirect flow and timeout fallback, while adding logging for each failure path.

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

Comment thread website/src/pages/auth/callback.astro Outdated
Comment thread website/src/pages/auth/callback.astro Outdated
Comment thread website/src/pages/auth/callback.astro
…imeout

oauth providers can fail at three different layers, each surfacing
the error in a different place:

  1. pkce / code-flow error → url search params: ?error=...
  2. implicit-flow error    → url hash params:   #error=...
  3. session-exchange error → returned from getsession()

the previous version of /auth/callback only checked (3), so any
error routed through (1) or (2) — which is what every supabase-side
provider config failure produces — landed on a 10-second silent
timeout that printed only "Authentication timed out".

that made the github oauth bug (#1257) effectively undebuggable from
the client: a customer who clicks Continue with GitHub and ends up
back here with ?error=server_error&error_description=... in the url
would just see a generic timeout message and have no idea what
actually failed.

now we read all three layers, surface whichever fires first, render
the raw error_description in a monospace block for support, and
include a best-effort hint for the most common failure modes
(server_error, access_denied, unsupported_provider).

regression-safe: if no error is in the url and the session
materializes normally, behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ooples
ooples force-pushed the fix/oauth-callback-error-visibility branch from 5eb64a1 to db12c58 Compare May 4, 2026 18:16
@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit

#1+#3 (coderabbitai+copilot, callback.astro:114): URLSearchParams.get()
   already percent-decodes its return value AND maps '+' to space, so the
   prior `decodeURIComponent(...).replace(/\+/g, ' ')` was double-decoding.
   That threw URIError on legitimate descriptions containing literal '%'
   (e.g. an IdP message with '%25' that became '%' after the first decode
   and then crashed the second), dropping the user into the generic
   "An unexpected error occurred" branch and burying the real OAuth
   failure. Removed the second decode and the '+' replacement entirely;
   error_description now renders as the IdP sent it.

#2 (coderabbitai, callback.astro:177): the 10s setTimeout was never
   cleared on SIGNED_IN — a near-deadline success would still fire
   showFatalError after the redirect started, briefly flashing
   "did not complete in time" before navigation. Captured the timeout
   id and clearTimeout() it inside the SIGNED_IN handler.

#4 (copilot, callback.astro:129): the disabled-provider hint only
   checked errorCode === 'unsupported_provider', missing callbacks like
   ?error=unsupported_provider&error_description=... where Supabase
   puts the marker on `error` instead. Added a parallel check on the
   `error` field so the actionable hint fires either way.

#5 (copilot): no Playwright e2e for the new branches. Added
   tests/e2e/auth/callback.spec.ts (5 specs, listed cleanly) covering:
   - search-param server_error → provider-misconfig hint
   - search-param access_denied → user-cancelled hint
   - error=unsupported_provider on `error` (not error_code) → disabled
     hint (pins #4)
   - hash-param error precedence over search-param error
   - error_description with literal '%' renders intact (pins #1+#3)

playwright.config.ts: new auth-anon project that runs auth/*.spec.ts
without storageState (the existing auth/*.setup.ts files are excluded
via testIgnore). Keeps the unauthenticated error-path specs separate
from the user.setup / admin.setup login fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 4, 2026 19:06

@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: 2

🤖 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 `@website/src/pages/auth/callback.astro`:
- Around line 65-76: The current pick() merges hash and search per-key causing
mixed-layer auth errors; change logic so once an error is present in the hash
fragment all error fields (error, error_code, error_description) are read
exclusively from hash, otherwise read them from search. Concretely, detect const
error = hash.get('error') ?? search.get('error'); then if hash.has('error') use
hash.get(...) for errorCode and errorDescription (and for error), else use
search.get(...); update uses of pick, hash, and search and the returned object
(error, errorCode, errorDescription) accordingly so fields come from the same
layer.
- Around line 188-198: The timeout fallback branch for the 10s window inside the
callback handler does not log an error, leaving the third-party-cookie / missing
SIGNED_IN path invisible; update the timeout handler (the setTimeout callback
that calls subscription.unsubscribe() and showFatalError) to also call
console.error() with a descriptive message and any relevant state (e.g., that
getSession() did not emit SIGNED_IN within 10s and suspecting third-party cookie
blocking), so developers can see the failure in devtools when this branch
executes.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: cd3bbd5a-2be8-482d-9513-5c39a0064bb2

📥 Commits

Reviewing files that changed from the base of the PR and between db12c58 and 2c40c23.

📒 Files selected for processing (3)
  • website/playwright.config.ts
  • website/src/pages/auth/callback.astro
  • website/tests/e2e/auth/callback.spec.ts

Comment thread website/src/pages/auth/callback.astro Outdated
Comment thread website/src/pages/auth/callback.astro

Copilot AI 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.

Pull request overview

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


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

Comment thread website/src/pages/auth/callback.astro Outdated
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro Outdated
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro Outdated
Address the reviewer's accessibility, observability, and error-rendering
concerns on /auth/callback:

- Error container now has `role="alert"`, `aria-live="assertive"`,
  `aria-atomic="true"` so screen readers announce the dynamically-
  injected error message. Without these the hidden div's content
  flipping from empty to populated was silent for AT users.
- The 10-second timeout fallback now console.errors before showing
  the fatal-error UI, matching the PR description's claim that every
  failure path leaves a stack trace in devtools. Previously the
  timeout was the one exception.
- The `catch (err)` branch no longer renders `String(err)`, which
  prints `[object Object]` for non-Error throws (some Supabase SDK
  paths reject with plain objects). Render `err.message` for Error
  instances, fall back to `JSON.stringify` for plain objects, and
  fall back further to `String(err)` only as a last resort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@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: 2

🤖 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 `@website/src/pages/auth/callback.astro`:
- Around line 191-197: The redirect query param is used directly in two places
(params.get('redirect') → window.location.href in the immediate-session branch
and inside supabase.auth.onAuthStateChange handler), causing an open-redirect;
replace direct assignment with a validation step that clamps the redirect to
same-origin paths only (use or add a helper like sanitizeRedirect/clampRedirect
that returns a safe path or fallback to `${base}account/`), and call that helper
in both locations before assigning window.location.href (keep existing
timeoutId/ subscription cleanup logic intact).
- Around line 224-229: The JSON.stringify fallback can return undefined for
undefined, functions, or symbols which leaves detail undefined and causes
safe(detail).replace(...) to throw; update the error handling in the callback to
check the result of JSON.stringify(err) and if it is undefined, null, or an
empty string, set detail to a stable string (e.g., String(err) or a fixed
placeholder like "<unserializable value>"); specifically change the branch that
assigns detail after the try/catch so that the variable detail (used later with
safe(detail).replace(...)) is always a defined string even when JSON.stringify
returns undefined.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: b8c4bee9-2798-475d-9213-758968b7e965

📥 Commits

Reviewing files that changed from the base of the PR and between 2c40c23 and 552b3a3.

📒 Files selected for processing (1)
  • website/src/pages/auth/callback.astro

Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
Address the per-field-merge concern + missing-failure-mode coverage:

callback.astro parseAuthError():
- Switch from per-FIELD precedence (`hash.get(key) ?? search.get(key)`)
  to per-SOURCE precedence: if the hash carries any `error`, use ALL
  hash fields together; otherwise use ALL search fields together. The
  previous merge could splice an `error` from one source with an
  `error_description` from the OTHER, producing a synthetic error that
  didn't actually arrive from any single OAuth callback layer. Hash =
  implicit-flow returns; search = code-flow returns; they're different
  protocol layers and their fields shouldn't mix.

callback.spec.ts:
- New `per-source precedence` test: hash with `error` only + search
  with `error_description` must NOT splice the search description into
  the hash error.
- New `non-URL failure paths` describe block with two specs:
  * getSession() error path — routes the supabase module to a shim
    that returns an error tuple, asserts "Session exchange failed."
    is rendered AND the failure was console.error'd.
  * 10-second timeout path — uses page.clock.fastForward(10_500) to
    skip the wall-clock wait, asserts the timeout fatal-error UI
    surfaces AND the documented console.error trace fired (PR #1258
    promised every failure path would log; the timeout path was the
    one previously-uncovered branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 5, 2026 00:01

@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

♻️ Duplicate comments (2)
website/src/pages/auth/callback.astro (2)

241-243: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize the JSON.stringify fallback to a definite string.

This branch can still leave detail non-string. JSON.stringify() returns undefined for top-level pure values like undefined, functions, and symbols, so showFatalError(..., detail) can fail again when safe(details) calls .replace(...). (developer.mozilla.org)

Proposed fix
         } else {
-          try { detail = JSON.stringify(err); }
+          try {
+            detail = JSON.stringify(err) ?? String(err);
+          }
           catch { detail = String(err); }
         }
🤖 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 `@website/src/pages/auth/callback.astro` around lines 241 - 243, The fallback
for serializing the error in the else branch can leave detail non-string
(JSON.stringify may return undefined); update the block that sets detail (the
try/catch around JSON.stringify(err)) so that after attempting JSON.stringify
you coerce undefined/non-string results into a definite string (e.g., use the
JSON result if truthy else String(err) or a fixed fallback like "<unserializable
error>") before passing to showFatalError and safe(detail).

205-211: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Clamp redirect to a same-origin path before navigating.

Line 210 still assigns params.get('redirect') directly into window.location.href. A crafted query value can bounce the user off-origin after login, and the same unresolved sink still exists in the existing-session branch on Line 191. This needs a shared redirect sanitizer before either assignment.

Proposed fix
+    function getSafeRedirect(raw: string | null, fallback: string): string {
+      if (!raw) return fallback;
+      try {
+        const url = new URL(raw, window.location.origin);
+        if (url.origin !== window.location.origin) return fallback;
+        return `${url.pathname}${url.search}${url.hash}`;
+      } catch {
+        return fallback;
+      }
+    }
+
     async function handleCallback() {
@@
         if (data.session) {
           const params = new URLSearchParams(window.location.search);
-          const redirectTo = params.get('redirect') || `${base}account/`;
+          const redirectTo = getSafeRedirect(params.get('redirect'), `${base}account/`);
           window.location.href = redirectTo;
           return;
         }
@@
         const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
           if (event === 'SIGNED_IN' && session) {
             subscription.unsubscribe();
             if (timeoutId !== undefined) clearTimeout(timeoutId);
             const params = new URLSearchParams(window.location.search);
-            const redirectTo = params.get('redirect') || `${base}account/`;
+            const redirectTo = getSafeRedirect(params.get('redirect'), `${base}account/`);
             window.location.href = redirectTo;
           }
         });
🤖 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 `@website/src/pages/auth/callback.astro` around lines 205 - 211, The redirect
value obtained from params.get('redirect') is used directly in
window.location.href in the supabase.auth.onAuthStateChange handler (inside the
SIGNED_IN branch) and must be sanitized to a same-origin path first; implement a
shared sanitizer function (e.g., sanitizeRedirectPath) and call it from both the
onAuthStateChange handler and the existing-session branch before assigning
window.location.href, ensuring it rejects or normalizes external origins, strips
protocol/host, enforces a leading slash, and falls back to `${base}account/`
when invalid.
🤖 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 `@website/src/pages/auth/callback.astro`:
- Around line 205-208: The auth state change handler currently only checks for
event === 'SIGNED_IN', causing a race where an 'INITIAL_SESSION' is ignored;
update the onAuthStateChange handler (the subscription created via
supabase.auth.onAuthStateChange referenced as subscription) to treat both
'SIGNED_IN' and 'INITIAL_SESSION' as successful session events (i.e., if (event
=== 'SIGNED_IN' || event === 'INITIAL_SESSION') { ... }), keep the same logic to
unsubscribe the subscription and clear timeoutId, and ensure getSession() usage
remains unchanged so valid sessions loaded from storage won't trigger the
timeout.

---

Duplicate comments:
In `@website/src/pages/auth/callback.astro`:
- Around line 241-243: The fallback for serializing the error in the else branch
can leave detail non-string (JSON.stringify may return undefined); update the
block that sets detail (the try/catch around JSON.stringify(err)) so that after
attempting JSON.stringify you coerce undefined/non-string results into a
definite string (e.g., use the JSON result if truthy else String(err) or a fixed
fallback like "<unserializable error>") before passing to showFatalError and
safe(detail).
- Around line 205-211: The redirect value obtained from params.get('redirect')
is used directly in window.location.href in the supabase.auth.onAuthStateChange
handler (inside the SIGNED_IN branch) and must be sanitized to a same-origin
path first; implement a shared sanitizer function (e.g., sanitizeRedirectPath)
and call it from both the onAuthStateChange handler and the existing-session
branch before assigning window.location.href, ensuring it rejects or normalizes
external origins, strips protocol/host, enforces a leading slash, and falls back
to `${base}account/` when invalid.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 0bee4c85-cf8d-4194-9405-41051fb951d6

📥 Commits

Reviewing files that changed from the base of the PR and between 552b3a3 and a1160a2.

📒 Files selected for processing (2)
  • website/src/pages/auth/callback.astro
  • website/tests/e2e/auth/callback.spec.ts

Comment thread website/src/pages/auth/callback.astro

Copilot AI 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.

Pull request overview

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


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

Comment thread website/src/pages/auth/callback.astro Outdated
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
Comment thread website/src/pages/auth/callback.astro
…ON, phishing disclosure, errorCode-only parser, success-path tests

Resolves 8 unresolved review threads from PR #1258 second-pass review:

#1 sanitizeRedirect: redirect query param now rejects non-same-origin
   paths (//evil.example, https://attacker.com) — open-redirect/phishing
   guard. Falls back to base + account/ for null/empty/disallowed input.

#2 JSON.stringify undefined: catch block coalesces JSON.stringify(err)
   to String(err) when stringify returns undefined.

#3 INITIAL_SESSION race: onAuthStateChange now accepts both SIGNED_IN
   and INITIAL_SESSION events.

#4 errorCode-only parser: parseUrlError treats any of error,
   error_code, error_description as a failure marker.

#5 phishing details disclosure: raw IdP-supplied error text now lives
   behind a Show-technical-details disclosure.

#6 success-path tests: new describe block covers immediate
   getSession success, ?redirect=/settings/api-keys/ honored,
   ?redirect=//evil.example rejected.

#7 HTML-escape regression test: spec asserts script tag is escaped.

#8 errorCode === unsupported_provider variant test.

Spec file count: 9 → 14 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ooples
ooples merged commit b69c1d1 into master May 5, 2026
27 of 43 checks passed
@ooples
ooples deleted the fix/oauth-callback-error-visibility branch May 5, 2026 02:31
ooples added a commit that referenced this pull request May 10, 2026
* fix(website): unblock auth/callback e2e smoke tests in production deploys

5 callback.spec.ts tests were failing on every post-deploy run since
PR #1258 merged. Two independent root causes:

1. Astro/Vite production-bundles the supabase import into a hashed
   `_astro/<chunk>.js` file. The page.route('lib/supabase') glob
   used by 4 tests never matched the bundled URL, so the real
   supabase client always loaded and the tests fell into the
   SIGNED_IN-event-never-fires timeout instead of the branch the
   spec wanted to cover. Replaced with a tiny page-side test seam:
   callback.astro now reads `window.__authTestStub ?? defaultSupabase`,
   tests plant the stub via `page.addInitScript` BEFORE navigation,
   which works regardless of bundling.

2. The 'literal % does not crash' test asserted on text inside the
   <details> 'Show technical details' disclosure that PR #1258
   added as a phishing mitigation. <details> content is hidden until
   the summary is clicked. Test now opens the disclosure first.

Also: the success-path tests asserted on URL ending in /account/, but
/account/ runs its own auth-state check and redirects to /login/ when
the session isn't real. Added stubRedirectTargets() to fulfill /account/
and /settings/ HTML with no-op markup so the navigation lands and stays
where the callback page sent it.

Verified locally against `astro preview`: all 14 tests pass (vs. 5
failed pre-fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vercel): extract ignoreCommand to scripts (256-char limit)

Vercel preview deploys for both `aidotnet-playground-api` and
`aidotnet_website` started failing on every PR with:

  The `vercel.json` schema validation failed with the following
  message: `ignoreCommand` should NOT be longer than 256 characters

The previous inline commands were 324 and 267 chars respectively.
Move the same logic into shell scripts and reference them from
vercel.json (33 / 29 chars):

- scripts/vercel-ignore-api.sh — root project (rootDirectory=null);
  builds when api/ or vercel.json changes, always builds master.
- website/scripts/vercel-ignore.sh — website project (rootDirectory=website);
  builds when anything inside website/ changes, always builds master.

Same exit-code contract preserved (0 = ignore, 1 = build) so the
Vercel project config doesn't need adjustment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants