Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60

Merged
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ
Apr 15, 2026
Merged

Fix post-OAuth sign-in flow: session failures and Try-again 404#60
Jose-Gael-Cruz-Lopez merged 2 commits into
mainfrom
claude/sapling-codebase-analysis-quypQ

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two bugs that broke sign-in after Google OAuth completed — users saw a "Redirecting to sign in…" screen, then "Unable to complete sign-in", and then a 404 when clicking "Try again".

Changes Made

frontend/src/app/signin/callback/page.tsx

  • Removed router.replace('/signin') on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to /signin
  • Fixed "Try again" href from /api/auth/google (relative Next.js path → 404) to ${NEXT_PUBLIC_API_URL}/api/auth/google (the actual backend endpoint)
  • Distinguish missing is_approved param (inline error) from explicit is_approved=false (redirect to /pending) — previously both were treated as denial
  • Passes auth_token from the OAuth redirect to the session route when present

frontend/src/app/api/auth/session/route.ts

  • Added HMAC token verification path: if the backend includes a signed auth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)
  • Kept the old backend round-trip as a fallback for when SESSION_SECRET is not shared
  • Wrapped signSession() in try/catch so a missing SESSION_SECRET returns a descriptive 500 instead of an unhandled exception

backend/config.py + backend/routes/auth.py

  • Backend now signs the OAuth redirect URL with a short-lived HMAC token (SHA-256, 5-min TTY, keyed on SESSION_SECRET)
  • If SESSION_SECRET is not set on the backend, it omits the token and the frontend falls back to the old verification flow

frontend/src/__tests__/signinCallback.test.tsx (new)

  • 8 tests covering all callback branches: dashboard on success, /pending on 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missing is_approved, /pending on explicit false, /pending on error=not_approved

Environment variable required

SESSION_SECRET must be set to the same value in both:

  • Cloudflare Pages environment variables (frontend)
  • Backend .env

Minimum 32 characters. This is what enables the HMAC fast path and also what signSession() needs to create the session cookie.

Related Issues

Fixes the sign-in failure reported for @bu.edu accounts on saplinglearn.com.

Testing

  • All 162 frontend tests pass
  • Verified callback branches with unit tests
  • Inline error + Try-again link render correctly on failure

https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa

Summary by CodeRabbit

  • New Features

    • Optional short-lived auth tokens now enable faster, local verification for quicker sign-ins when configured.
    • Sign-in callback respects the configured API URL for "Try again" links.
  • Bug Fixes

    • Improved session verification with a backend fallback and clearer failure handling during sign-in.

Two bugs were breaking sign-in for josec@bu.edu:
1. "Try again" link used href="/api/auth/google" (a relative Next.js
path that doesn't exist), causing a 404. Fixed to use the full
backend URL via NEXT_PUBLIC_API_URL.
2. The session API was calling back to the backend from Cloudflare's
edge to verify the user on every sign-in. If that call failed
(timeout, connectivity), users saw "Unable to complete sign-in"
even though OAuth succeeded.
Fix: the backend now signs the OAuth redirect URL with a short-lived
HMAC token (SHA-256, 5 min TTL, signed with SESSION_SECRET). The
frontend session route verifies the token cryptographically with no
backend round-trip. The backend round-trip is kept as a fallback when
SESSION_SECRET is not shared between services.
Also wraps signSession() in a try/catch so a missing SESSION_SECRET
returns a descriptive 500 instead of an unhandled exception.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@coderabbitai

coderabbitaiBot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change adds an optional short-lived HMAC-SHA256 auth token flow: the backend may emit a base64url(payload).base64url(signature) token on OAuth callback when SESSION_SECRET is set; the frontend can verify that token locally to create a session, falling back to backend verification when no token is present.

Changes

Cohort / File(s)Summary
Backend config & OAuth callback
backend/config.py, backend/routes/auth.py
Added SESSION_SECRET env var. google_callback now conditionally builds a short-lived HMAC-SHA256 auth_token (payload: { user_id, exp } as base64url + signature) and appends it to the redirect only when SESSION_SECRET is configured.
Frontend session endpoint
frontend/src/app/api/auth/session/route.ts
Added verifyAuthToken() to validate incoming auth_token via Web Crypto HMAC-SHA256, updated POST to accept { userId?, authToken? }. Fast-path: verify token locally and sign session; fallback: fetch backend /me for approval. Added error responses for invalid/expired tokens and signing failures tied to SESSION_SECRET.
Frontend sign-in callback page
frontend/src/app/signin/callback/page.tsx
Extracts auth_token from query, includes it conditionally in POST to /api/auth/session. Uses NEXT_PUBLIC_API_URL for the "Try again" URL when configured.

Sequence Diagram

sequenceDiagram
actor Browser
participant BE as Backend (OAuth)
participant FE_CB as Frontend (Callback Page)
participant FE_SE as Frontend (Session Endpoint)
participant BE_ME as Backend (User Verify)
Browser->>BE: Start Google OAuth
BE->>BE: User approves
BE->>BE: If SESSION_SECRET -> generate HMAC token (user_id, exp)
BE->>Browser: Redirect to /signin/callback?userId=...&auth_token=...
Browser->>FE_CB: GET /signin/callback?userId=...&auth_token=...
FE_CB->>FE_CB: Extract userId, auth_token
FE_CB->>FE_SE: POST /api/auth/session { userId?, authToken? }
rect rgba(0,150,100,0.5)
FE_SE->>FE_SE: If authToken present -> verify HMAC signature & exp
end
alt Token valid and unexpired
FE_SE->>FE_SE: Sign session cookie
FE_SE->>Browser: 200 + session cookie
else Token invalid/expired
FE_SE->>Browser: 401 Unauthorized
else No token (fallback)
FE_SE->>BE_ME: GET /api/user/me for userId
BE_ME->>FE_SE: Return user & is_approved
FE_SE->>FE_SE: If approved -> sign session cookie
FE_SE->>Browser: 200 + session cookie
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through OAuth fields so bright,
I signed a token for a fleeting night,
With HMAC whiskers and a hop so spry,
Sessions bloom fast — then wink, goodbye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title directly addresses the main bugs fixed: post-OAuth sign-in flow failures and the 404 error from the Try-again link, accurately summarizing the PR's primary purpose.
Description check✅ PassedThe description is comprehensive, covering all key changes, environment requirements, and testing. It follows the template structure with Summary, Changes Made, Related Issues, and Testing sections.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sapling-codebase-analysis-quypQ

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/signin/callback/page.tsx`:
- Line 7: The code uses the API_URL constant (const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? '') which can be empty and causes the "Try
again" link to point to a backend-only path (/api/auth/google) that 404s; update
the fallback and the link so frontend environments always resolve a valid client
URL: set a safe default (e.g., '/' or window.location.origin) for API_URL when
NEXT_PUBLIC_API_URL is missing, and ensure the "Try again" link uses that
resolved API_URL (or construct the href with window.location.origin) instead of
relying on an empty string so the retry route is a valid frontend-accessible
URL.
🪄 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 Plus

Run ID: 194ec838-916e-483a-9b8b-a1e1a0c4feb5

📥 Commits

Reviewing files that changed from the base of the PR and between a906c0c and 1e43645.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/routes/auth.py
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx

Comment threadfrontend/src/app/signin/callback/page.tsx
The previous commit broke the Cloudflare Pages build with a TypeScript
error: b64urlToBytes returned Uint8Array<ArrayBufferLike> (the default
when allocated via "new Uint8Array(length)"), which TS no longer
considers assignable to BufferSource for crypto.subtle.verify. Fix by
allocating a concrete ArrayBuffer first so the result is typed as
Uint8Array<ArrayBuffer>.
Also addresses CodeRabbit's comment on the Try-again link: if
NEXT_PUBLIC_API_URL is not configured, the link would resolve to a
relative "/api/auth/google" — exactly the 404 this PR was meant to
fix. Fall back to "/signin" instead, which the middleware handles.
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Apr 15, 2026

Copy link
Copy Markdown

Deploying web with Cloudflare Pages Cloudflare Pages

Latest commit:8040a73
Status: ✅ Deploy successful!
Preview URL:https://bac3b9cf.web-75h.pages.dev
Branch Preview URL:https://claude-sapling-codebase-anal.web-75h.pages.dev

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/api/auth/session/route.ts`:
- Around line 52-54: Wrap the call to request.json() in a try-catch inside the
exported POST function to handle empty/invalid JSON and return a controlled 400
response; specifically, catch JSON parsing errors from request.json(), log or
include a concise error message, and return an appropriate bad-request result
when you cannot extract the expected userId/authToken (the variables
destructured from body) so the handler no longer throws an unhandled exception
for malformed bodies.
🪄 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 Plus

Run ID: 96d04b0b-a9a3-43f3-a4f1-3c01dcc97f59

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43645 and 8040a73.

📒 Files selected for processing (2)
  • frontend/src/app/api/auth/session/route.ts
  • frontend/src/app/signin/callback/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/signin/callback/page.tsx

Comment on lines +52 to +54
export async function POST(request: NextRequest) {
const body = await request.json();
const { userId, authToken } = body as { userId?: string; authToken?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for malformed request body.

request.json() will throw if the body is empty or invalid JSON, resulting in an unhandled exception and a generic 500 response. While the primary caller (callback page) always sends valid JSON, adding a try-catch provides clearer error messages.

🛡️ Proposed fix
 export async function POST(request: NextRequest) {
- const body = await request.json();- const { userId, authToken } = body as { userId?: string; authToken?: string };+ let userId: string | undefined;+ let authToken: string | undefined;+ try {+ const body = await request.json();+ ({ userId, authToken } = body as { userId?: string; authToken?: string });+ } catch {+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/api/auth/session/route.ts` around lines 52 - 54, Wrap the
call to request.json() in a try-catch inside the exported POST function to
handle empty/invalid JSON and return a controlled 400 response; specifically,
catch JSON parsing errors from request.json(), log or include a concise error
message, and return an appropriate bad-request result when you cannot extract
the expected userId/authToken (the variables destructured from body) so the
handler no longer throws an unhandled exception for malformed bodies.

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 8b25e55 into mainApr 15, 2026
4 checks passed
@AndresL230
AndresL230 deleted the claude/sapling-codebase-analysis-quypQ branch April 19, 2026 00:06
Sign up for freeto 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

@Jose-Gael-Cruz-Lopez@claude