Uh oh!
There was an error while loading. Please reload this page.
Fix post-OAuth sign-in flow: session failures and Try-again 404 - #60
Conversation
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
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
backend/config.pybackend/routes/auth.pyfrontend/src/app/api/auth/session/route.tsfrontend/src/app/signin/callback/page.tsx
Uh oh!
There was an error while loading. Please reload this page.
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
Deploying web with |
| 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 |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
frontend/src/app/api/auth/session/route.tsfrontend/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
| export async function POST(request: NextRequest) { | ||
| const body = await request.json(); | ||
| const { userId, authToken } = body as { userId?: string; authToken?: string }; |
There was a problem hiding this comment.
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.
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.tsxrouter.replace('/signin')on session failure — the callback page now shows an inline error with a "Try again" link instead of bouncing users to/signinhreffrom/api/auth/google(relative Next.js path → 404) to${NEXT_PUBLIC_API_URL}/api/auth/google(the actual backend endpoint)is_approvedparam (inline error) from explicitis_approved=false(redirect to/pending) — previously both were treated as denialauth_tokenfrom the OAuth redirect to the session route when presentfrontend/src/app/api/auth/session/route.tsauth_token, the session is established without a backend round-trip (eliminates the Cloudflare edge → backend call that was timing out)SESSION_SECRETis not sharedsignSession()in try/catch so a missingSESSION_SECRETreturns a descriptive 500 instead of an unhandled exceptionbackend/config.py+backend/routes/auth.pySESSION_SECRET)SESSION_SECRETis not set on the backend, it omits the token and the frontend falls back to the old verification flowfrontend/src/__tests__/signinCallback.test.tsx(new)/pendingon 403, inline error on server failure, inline error on network error, inline error on missing params, inline error on missingis_approved,/pendingon explicitfalse,/pendingonerror=not_approvedEnvironment variable required
SESSION_SECRETmust be set to the same value in both:.envMinimum 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.eduaccounts on saplinglearn.com.Testing
https://claude.ai/code/session_01U3mwGcNbKKXSb9P6cBEaqa
Summary by CodeRabbit
New Features
Bug Fixes