strat-gameplay-webapp/OAUTH_IPAD_ISSUE.md
Cal Corum e90a907e9e CLAUDE: Implement server-side OAuth flow with HttpOnly cookies
Fixes iPad Safari authentication issue where async JavaScript is blocked
on OAuth callback pages after cross-origin redirects (Cloudflare + Safari ITP).

**Problem**: iPad Safari blocks all async operations (Promises, setTimeout,
onMounted) on the OAuth callback page, preventing frontend token exchange.

**Solution**: Move entire OAuth flow to backend with HttpOnly cookies,
eliminating JavaScript dependency on callback page.

## Backend Changes (7 files)

### New Files
- app/services/oauth_state.py - Redis-based OAuth state management
  * CSRF protection with one-time use tokens (10min TTL)
  * Replaces frontend sessionStorage state validation

- app/utils/cookies.py - HttpOnly cookie utilities
  * Access token: 1 hour, Path=/api
  * Refresh token: 7 days, Path=/api/auth
  * Security: HttpOnly, Secure (prod), SameSite=Lax

### Modified Files
- app/api/routes/auth.py
  * NEW: GET /discord/login - Initiate OAuth with state creation
  * NEW: GET /discord/callback/server - Server-side callback handler
  * NEW: POST /logout - Clear auth cookies
  * UPDATED: GET /me - Cookie + header support (backwards compatible)
  * UPDATED: POST /refresh - Cookie + body support (backwards compatible)
  * FIXED: exchange_code_for_token() accepts redirect_uri parameter

- app/config.py
  * Added discord_server_redirect_uri config
  * Added frontend_url config for post-auth redirects

- app/websocket/handlers.py
  * Updated connect handler to parse cookies from environ
  * Falls back to auth object for backwards compatibility

- .env.example
  * Added DISCORD_SERVER_REDIRECT_URI example
  * Added FRONTEND_URL example

## Frontend Changes (10 files)

### Core Auth Changes
- store/auth.ts - Complete rewrite for cookie-based auth
  * Removed: token, refreshToken, tokenExpiresAt state (HttpOnly)
  * Added: checkAuth() - calls /api/auth/me with credentials
  * Updated: loginWithDiscord() - redirects to backend endpoint
  * Updated: logout() - calls backend logout endpoint
  * All $fetch calls use credentials: 'include'

- pages/auth/callback.vue - Simplified to error handler
  * No JavaScript token exchange needed
  * Displays errors from query params
  * Verifies auth with checkAuth() on success

- plugins/auth.client.ts
  * Changed from localStorage init to checkAuth() call
  * Async plugin to ensure auth state before navigation

- middleware/auth.ts - Simplified
  * Removed token validity checks (HttpOnly cookies)
  * Simple isAuthenticated check

### Cleanup Changes
- composables/useWebSocket.ts
  * Added withCredentials: true
  * Removed auth object with token
  * Updated canConnect to use isAuthenticated only

- layouts/default.vue, layouts/game.vue, pages/index.vue, pages/games/[id].vue
  * Removed initializeAuth() calls (handled by plugin)

## Documentation
- OAUTH_IPAD_ISSUE.md - Problem analysis and investigation notes
- OAUTH_SERVER_SIDE_IMPLEMENTATION.md - Complete implementation guide
  * Security improvements summary
  * Discord Developer Portal setup instructions
  * Testing checklist
  * OAuth flow diagram

## Security Improvements
- Tokens stored in HttpOnly cookies (XSS-safe)
- OAuth state in Redis with one-time use (CSRF-safe)
- Follows OAuth 2.0 Security Best Current Practice
- Backwards compatible with Authorization header auth

## Testing
-  Backend OAuth endpoints functional
-  Token exchange with correct redirect_uri
-  Cookie-based auth working
-  WebSocket connection with cookies
-  Desktop browser flow verified
-  iPad Safari testing pending Discord redirect URI config

## Next Steps
1. Add Discord redirect URI in Developer Portal:
   https://gameplay-demo.manticorum.com/api/auth/discord/callback/server
2. Test complete flow on iPad Safari
3. Verify WebSocket auto-reconnection with cookies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 22:16:30 -06:00

2.2 KiB

iPad OAuth Authentication Issue

Problem Summary

Discord OAuth authentication fails on iPad browsers (Safari and Firefox) when accessing via https://gameplay-demo.manticorum.com.

Root Cause

All JavaScript async operations (Promises, setTimeout, onMounted) are blocked on the iPad browser when loading the OAuth callback page (/auth/callback). Synchronous JavaScript executes normally, but any async callback never fires.

Evidence

  • Login redirect works (anchor tag with href)
  • Callback page loads and renders
  • Script setup code executes
  • BUT: onMounted never fires, Promise.then() never executes, setTimeout never triggers

Symptoms

  • User clicks "Continue with Discord" → Redirects to Discord
  • User authenticates → Redirects back to callback page
  • Callback page shows "Authenticating..." spinner
  • Page hangs indefinitely (async operations blocked)

Tested Solutions (All Failed on iPad)

  1. Button with click handler → No effect
  2. Anchor tag with href → Works for login redirect
  3. onMounted hook → Never executes
  4. setTimeout → Never fires
  5. Promise callbacks (.then/.catch) → Never execute
  6. Direct $fetch call → Hangs
  7. Promise.race with timeout → Timeout never fires
  8. Form submission → document.createElement fails (SSR issue)

Likely Causes

  1. Content Security Policy (CSP) blocking async operations
  2. Nginx Proxy Manager injecting restrictive headers
  3. iOS Safari security restrictions
  4. Browser settings on the iPad

Investigation Steps

  1. Check browser console for CSP violations
  2. Inspect response headers from https://gameplay-demo.manticorum.com/auth/callback
  3. Test same flow on desktop browser
  4. Test on different iPad/iOS version
  5. Check Nginx proxy configuration

Workarounds

Option 1: Desktop Authentication

Authenticate on desktop first, then use iPad for gameplay

Option 2: Backend-Driven Flow

Modify backend to handle OAuth callback server-side and set cookies, eliminating need for frontend JavaScript

Option 3: Fix Environment

Debug and fix whatever is blocking async operations on iPad

Session Date

2025-11-27

Status

UNRESOLVED - Environmental issue, not code bug