Navigation

Scrabble JWT Authentication Guide

Implementation guide for Famous.ai Scrabble game to validate SnappyIdioms.com JWT tokens

๐Ÿšจ CRITICAL: Infinite Sign-In Loop Fix

Current Problem:

Users click "Play Scrabble" โ†’ redirected to play.snappyidioms.com/?auth=TOKEN โ†’ game redirects back to SnappyIdioms.com โ†’ infinite loop

โœ… Solution - 3 Critical Steps:

  1. 1. Read ?auth=TOKEN on page load - Extract from URL params immediately
  2. 2. Validate token server-side - Call SnappyIdioms.com/api/verifyGameToken
  3. 3. SKIP ALL AUTH MODALS if valid - Do NOT redirect to SnappyIdioms.com, do NOT show sign-in screens

โš ๏ธ Common Mistakes Causing Loops:

  • โŒ Checking for local user session before checking JWT token
  • โŒ Redirecting to SnappyIdioms.com login if no local session found
  • โŒ Showing auth modal even when valid JWT exists
  • โŒ Not removing ?auth=TOKEN from URL after validation
Flow Overview

When users click "Play Scrabble" from SnappyIdioms.com, they are redirected to:

https://play.snappyidioms.com/?auth=TOKEN

The Famous.ai Scrabble game MUST read this token, validate it, and proceed directly to gameplay.

๐Ÿ› ๏ธ Critical Feature Requirements

1. Reset Game Data Button

Add a "Reset Game Data" button in footer/settings that:

  • โ€ข Clears ALL localStorage: snappy_platform_token, snappy_username, snappy_is_guest, etc.
  • โ€ข Clears ALL sessionStorage with snappy_ prefix
  • โ€ข Shows confirmation dialog before clearing
  • โ€ข Reloads page after clearing

Why: Users stuck in broken auth states need a reset without manually clearing browser data

2. Visible Error Feedback

Replace console-only errors with user-visible toasts/banners:

  • โ€ข "โš ๏ธ Sign-in failed โ€” playing as guest"
  • โ€ข "โฐ Session expired โ€” sign in again to earn pearls"
  • โ€ข "๐Ÿ”’ Could not connect to platform โ€” progress saved locally only"

Why: Users don't understand why they're in guest mode

3. Quick Play Button

Add "Quick Play" button that:

  • โ€ข Bypasses AuthModal entirely
  • โ€ข Auto-generates guest username (Guest_5839)
  • โ€ข Goes directly to character selection
  • โ€ข Zero popups or additional modals

Why: Fastest path to gameplay for trial users

4. Debug Logging & Banner

Add detailed console logging to auth functions + debug UI:

// In initializePlatformAuth()
console.log('[Auth] URL:', window.location.href);
console.log('[Auth] Token extracted:', token ? 'YES' : 'NO');
console.log('[Auth] Validation result:', result);

// Debug banner (toggled via ?debug=true)
Shows: auth state, token expiry, errors

Why: Need visibility into where auth flow breaks

5. Offline/Standalone Mode

Detect offline OR add "Play Offline" option:

  • โ€ข Skip initializePlatformAuth() entirely
  • โ€ข Disable all database calls
  • โ€ข Use bundled idiom set from src/data/idioms.ts
  • โ€ข Store progress in localStorage only
  • โ€ข Show "๐Ÿ“ด Offline Mode" indicator

Why: Game should always be playable regardless of server status

6. System Status Page

Create /diagnostic route showing:

  • โ€ข Auth state (guest/platform/expired)
  • โ€ข Token validity & expiry countdown
  • โ€ข Database ping test (live)
  • โ€ข Edge function reachability test
  • โ€ข localStorage/sessionStorage keys (masked)
  • โ€ข Browser info & timestamp

Why: Users & developers need quick diagnostics without dev tools

JWT Token Structure

The JWT token contains the following payload:

{
  "email": "user@example.com",
  "name": "John Doe",
  "isPremium": false,
  "exp": 1708155600000,  // Expires in 24 hours
  "iat": 1708069200000,  // Issued at
  "nonce": "uuid",       // Unique per token
  "ip": "192.168.1.1"    // User IP for validation
}

โš ๏ธ Important:

  • โ€ข Token expires in 24 hours (full game session)
  • โ€ข Each token has a unique nonce (prevents replay attacks)
  • โ€ข Signed with HMAC-SHA256 using shared JWT_SECRET
Step 1: Read Token from URL

In your Famous.ai Scrabble game, read the auth parameter:

// JavaScript/TypeScript
const urlParams = new URLSearchParams(window.location.search);
const authToken = urlParams.get('auth');

if (authToken) {
  // Validate the token
  const isValid = await validateJWT(authToken);
  
  if (isValid) {
    // Skip sign-in modal
    // Proceed to game
  }
}
๐Ÿšจ CRITICAL: Server-Side Validation Only

โš ๏ธ SECURITY WARNING:

JWT signature validation MUST happen server-side. Never expose JWT_SECRET to the browser. Client-side code should only decode and check expiry, not verify signatures.

Step 2A: Client-Side (Browser)

Extract token and decode payload (NO signature verification):

// CLIENT-SIDE: Only decode and check expiry
async function handleAuthToken(token) {
  try {
    const [header, payload, signature] = token.split('.');
    
    // Decode payload (no secrets needed)
    const decodedPayload = JSON.parse(
      atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
    );
    
    // Check expiration locally
    if (Date.now() > decodedPayload.exp) {
      console.error('[Auth] Token expired');
      return { valid: false, reason: 'expired' };
    }
    
    // Send to server for signature verification
    const isValid = await verifyTokenOnServer(token);
    
    if (isValid) {
      // Store user data in localStorage
      localStorage.setItem('snappy_platform_token', token);
      localStorage.setItem('snappy_username', decodedPayload.name);
      localStorage.setItem('snappy_user_email', decodedPayload.email);
      localStorage.setItem('snappy_is_premium', decodedPayload.isPremium);
      
      return { valid: true, user: decodedPayload };
    }
    
    return { valid: false, reason: 'invalid_signature' };
  } catch (error) {
    console.error('[Auth] Token validation failed:', error);
    return { valid: false, reason: error.message };
  }
}

Step 2B: Server-Side (Edge Function/Backend)

SnappyIdioms.com provides a verification endpoint at:

POST https://snappyidioms.com/api/verifyGameToken

Call this endpoint from your server to verify JWT signatures:

// SERVER-SIDE: Verify token via SnappyIdioms.com
async function verifyTokenOnServer(token) {
  try {
    const response = await fetch('https://snappyidioms.com/api/verifyGameToken', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token })
    });
    
    const result = await response.json();
    return result.valid ? result.user : null;
  } catch (error) {
    console.error('[Auth] Server validation failed:', error);
    return null;
  }
}

// BACKEND ENDPOINT: /api/verify-token
export default async function handler(req) {
  const { token } = await req.json();
  
  try {
    const [header, payload, signature] = token.split('.');
    
    // Decode payload
    const decodedPayload = JSON.parse(
      atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
    );
    
    // Check expiration
    if (Date.now() > decodedPayload.exp) {
      return Response.json({ valid: false, reason: 'expired' });
    }
    
    // Verify signature using JWT_SECRET (server-side only!)
    const isValid = await verifyHMACSignature(
      `${header}.${payload}`,
      signature,
      process.env.JWT_SECRET
    );
    
    return Response.json({ 
      valid: isValid,
      user: isValid ? decodedPayload : null 
    });
  } catch (error) {
    return Response.json({ valid: false, reason: error.message });
  }
}

async function verifyHMACSignature(data, signature, secret) {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(secret);
  const messageData = encoder.encode(data);
  
  const key = await crypto.subtle.importKey(
    'raw', keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false, ['verify']
  );
  
  const signatureBytes = Uint8Array.from(
    atob(signature.replace(/-/g, '+').replace(/_/g, '/')),
    c => c.charCodeAt(0)
  );
  
  return await crypto.subtle.verify(
    'HMAC',
    key,
    signatureBytes,
    messageData
  );
}
Step 2.5: Session-Level Guard (CRITICAL)

๐Ÿ” THE ULTIMATE LOOP GUARD:

// src/lib/platformAuth.ts
export function cameFromPlatform(): boolean {
  return sessionStorage.getItem('__snappy_came_from_platform') === 'true';
}

// Set this flag when ?auth=TOKEN is detected
if (authToken) {
  sessionStorage.setItem('__snappy_came_from_platform', 'true');
}

โš ๏ธ Why This Matters:

  • โ€ข JWT tokens expire after 24 hours
  • โ€ข When expired, both isPlatformAuthLoading and isPlatformAuthenticated become false
  • โ€ข Without session guard, "Sign In" button redirects back to SnappyIdioms.com
  • โ€ข cameFromPlatform() survives JWT expiry - it's session-level
  • โ€ข Blocks redirects for entire browser tab session

โœ… Implementation in AppLayout.tsx:

import { cameFromPlatform } from '@/lib/platformAuth';

function handleSignInClick() {
  // Triple guard - ALL must be false to allow redirect
  if (isPlatformAuthLoading || isPlatformAuthenticated || cameFromPlatform()) {
    console.log('[Auth] Blocked redirect - user came from platform');
    return;
  }
  
  // Only reaches here if user opened game directly
  window.open(SNAPPY_LOGIN_URL, '_blank');
}
Step 3: CRITICAL - Prevent Sign-In Loop

๐Ÿšจ ANTI-PATTERN (Causes infinite loop):

// โŒ WRONG - Redirects user back to SnappyIdioms.com
if (!localUser) {
  window.location.href = 'https://snappyidioms.com/login';
}

// โŒ WRONG - Shows auth modal even with valid JWT
if (!authToken || !isValid) {
  setShowSignInModal(true);
}

This creates an endless loop: play.snappyidioms.com โ†’ SnappyIdioms.com โ†’ play.snappyidioms.com...

โœ… CORRECT IMPLEMENTATION:

// CORRECT - Check JWT FIRST, before any other auth logic
useEffect(() => {
  async function initAuth() {
    const urlParams = new URLSearchParams(window.location.search);
    const authToken = urlParams.get('auth');
    
    // Priority 1: JWT token from SnappyIdioms.com
    if (authToken) {
      console.log('[Auth] JWT token found, validating...');
      const result = await verifyTokenOnServer(authToken);
      
      if (result?.valid) {
        // Store user data & skip ALL auth modals
        localStorage.setItem('snappy_platform_token', authToken);
        localStorage.setItem('snappy_username', result.name);
        localStorage.setItem('snappy_user_email', result.email);
        
        setIsAuthenticated(true);
        setShowSignInModal(false); // โ† CRITICAL: Skip modal
        
        // Clean URL (remove token for security)
        window.history.replaceState({}, '', window.location.pathname);
        
        console.log('[Auth] โœ… JWT validated, proceeding to game');
        return; // โ† STOP HERE, don't check local storage
      } else {
        console.warn('[Auth] โŒ JWT invalid:', result?.error);
        // Fall through to guest mode (do NOT redirect)
      }
    }
    
    // Priority 2: Check localStorage for existing session
    const storedToken = localStorage.getItem('snappy_platform_token');
    if (storedToken) {
      setIsAuthenticated(true);
      setShowSignInModal(false);
      return;
    }
    
    // Priority 3: Guest mode (NO REDIRECTS!)
    console.log('[Auth] No valid token, entering guest mode');
    setIsAuthenticated(false);
    setShowSignInModal(true); // Show modal, but DO NOT redirect
  }
  
  initAuth();
}, []);

async function verifyTokenOnServer(token) {
  try {
    const response = await fetch('https://snappyidioms.com/api/verifyGameToken', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token })
    });
    return await response.json();
  } catch (error) {
    console.error('[Auth] Verification failed:', error);
    return { valid: false, error: error.message };
  }
}

๐Ÿ“‹ Checklist to Prevent Loops:

  • โ˜ Check ?auth=TOKEN parameter BEFORE checking localStorage
  • โ˜ If JWT valid โ†’ setIsAuthenticated(true) + setShowSignInModal(false)
  • โ˜ NEVER redirect to SnappyIdioms.com if user has JWT token
  • โ˜ Remove ?auth=TOKEN from URL after validation (security best practice)
  • โ˜ Guest mode shows modal but DOES NOT redirect
  • โ˜ Add console.log statements to track auth flow
โœ… No JWT_SECRET Needed (Secure Architecture)

โœ… Recommended Approach:

Famous.ai does NOT need JWT_SECRET. All signature verification happens via SnappyIdioms.com's /api/verifyGameToken endpoint.

๐Ÿ” Security Benefits:

  • โ€ข JWT_SECRET never leaves SnappyIdioms.com infrastructure
  • โ€ข Single source of truth for token validation
  • โ€ข Easy to rotate secrets without updating Famous.ai
  • โ€ข Zero risk of secret exposure in Famous.ai codebase

โš ๏ธ Alternative (Not Recommended):

If Famous.ai needs offline verification, contact SnappyIdioms.com to securely share JWT_SECRET via encrypted channel. Never commit to git or expose in client code.

๐Ÿ“‹ Implementation Checklist

Phase 1: Critical Fixes (Do First)

  • โ˜ Add console logging to initializePlatformAuth()
  • โ˜ Add console logging to extractTokenFromURL()
  • โ˜ Add visible error toasts/banners
  • โ˜ Fix white screens with error boundaries
  • โ˜ Test token validation end-to-end

Phase 2: UX Improvements

  • โ˜ Add "Reset Game Data" button
  • โ˜ Add "Quick Play" button
  • โ˜ Add debug mode with ?debug=true
  • โ˜ Improve error messages

Phase 3: Robustness

  • โ˜ Implement offline mode
  • โ˜ Create /diagnostic page
  • โ˜ Add network detection
  • โ˜ Add localStorage backup

Phase 4: Testing

  • โ˜ Test on Android Chrome
  • โ˜ Test on iOS Safari
  • โ˜ Test expired tokens
  • โ˜ Test invalid tokens
  • โ˜ Test offline mode
๐ŸŒ Platform API Proxy (Famous.ai Server-Side)

Famous.ai implements a server-side proxy to handle token verification and pearl operations without exposing JWT_SECRET:

๐Ÿ“ Endpoints:

  • โ€ข POST /platform-api - Main proxy endpoint
  • โ€ข Delegates token verification to SnappyIdioms.com
  • โ€ข Proxies pearl operations to Game API
  • โ€ข Implements token refresh flow (v4.4.0)

๐Ÿ”‘ Key Features:

  • โœ… Token verification via VERIFY_TOKEN_URL
  • โœ… Pearl operations via PLATFORM_API_URL
  • โœ… Health checks & diagnostics
  • โœ… Admin verification (server-side key)
  • โœ… Token refresh to prevent redirect loops
  • โœ… CORS headers for cross-origin requests
// Famous.ai Platform API Proxy (Deno Edge Function)
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
};

const PLATFORM_API_URL = 'https://app.base44.com/api/run/.../gameAPISupabase';
const VERIFY_TOKEN_URL = 'https://app.base44.com/api/run/.../verifyGameToken';

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }

  const body = await req.json();
  const { action, token } = body;

  // Health check (no token required)
  if (action === 'health_check' || action === 'ping') {
    return new Response(JSON.stringify({ status: 'ok' }), {
      status: 200,
      headers: { 'Content-Type': 'application/json', ...corsHeaders }
    });
  }

  // Token refresh (v4.4.0) - prevents redirect loops
  if (action === 'refresh_token') {
    const { email, user_id } = body;
    
    // Re-validate token with SnappyIdioms.com
    const verifyResponse = await fetch(VERIFY_TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
      body: JSON.stringify({ token })
    });
    
    const verifyData = await verifyResponse.json();
    
    if (verifyResponse.ok && verifyData) {
      // Extend session by 24 hours
      const newExpiresAt = Date.now() + (24 * 60 * 60 * 1000);
      return new Response(JSON.stringify({
        success: true,
        refreshed: true,
        user_id: verifyData.user_id || user_id,
        email: verifyData.email || email,
        expires_at: newExpiresAt,
        message: 'Session refreshed successfully'
      }), { status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders } });
    }
    
    // Token expired - try identity-based refresh
    if (email || user_id) {
      const refreshResponse = await fetch(PLATFORM_API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'refresh_session',
          email: email,
          user_id: user_id
        })
      });
      
      const refreshData = await refreshResponse.json();
      
      if (refreshResponse.ok && refreshData?.success) {
        return new Response(JSON.stringify({
          success: true,
          refreshed: true,
          user_id: refreshData.user_id,
          new_token: refreshData.token,
          expires_at: Date.now() + (24 * 60 * 60 * 1000)
        }), { status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders } });
      }
    }
    
    return new Response(JSON.stringify({
      success: false,
      error: 'Token refresh failed'
    }), { status: 401, headers: { 'Content-Type': 'application/json', ...corsHeaders } });
  }

  // Token verification (delegates to SnappyIdioms.com)
  if (action === 'validate_token' || action === 'verify_token') {
    const verifyResponse = await fetch(VERIFY_TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
      body: JSON.stringify({ token })
    });
    
    const verifyData = await verifyResponse.json();
    
    return new Response(JSON.stringify({
      valid: verifyResponse.ok,
      ...verifyData
    }), {
      status: verifyResponse.ok ? 200 : 401,
      headers: { 'Content-Type': 'application/json', ...corsHeaders }
    });
  }

  // Pearl operations (proxy to game API)
  if (action === 'award_pearls') {
    const platformResponse = await fetch(PLATFORM_API_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
      body: JSON.stringify(body)
    });
    
    const responseData = await platformResponse.json();
    
    return new Response(JSON.stringify(responseData), {
      status: platformResponse.status,
      headers: { 'Content-Type': 'application/json', ...corsHeaders }
    });
  }
});

โœ… Token Refresh Flow (v4.4.0):

  1. 1. Client detects JWT expiry approaching (< 30 min)
  2. 2. Client calls action: 'refresh_token' with current token
  3. 3. Proxy re-validates token with SnappyIdioms.com
  4. 4. If valid: extend session by 24 hours
  5. 5. If expired: attempt identity-based refresh
  6. 6. Return new expiry + optional new token
  7. 7. Client extends local session without redirecting

This prevents redirect loops by keeping sessions alive server-side without sending users back to SnappyIdioms.com

โš™๏ธ Centralized Config System

Replace all hardcoded URLs with environment-based configuration to prevent domain issues:

๐Ÿ“– View Config System Guide โ†’

Implements src/lib/config.ts with environment variables for GAME_URL, SNAPPY_LOGIN_URL, etc.

Testing the Integration

Step 1: Test SnappyIdioms.com JWT Flow (Proves it works)

Open Auth Flow Test Page โ†’

Generate token at /JWTDiagnostic, then visit /GameAuthTest?auth=TOKEN to see working implementation

Step 2: Use the JWT Diagnostic Tool:

Open JWT Diagnostic Tool โ†’
  1. 1. Generate a test token
  2. 2. Copy the redirect URL
  3. 3. Test in incognito/private window
  4. 4. Check console for debug logs
  5. 5. Verify token validation works

Original Testing Checklist:

  • User clicks "Play Scrabble" on SnappyIdioms.com
  • Redirect to play.snappyidioms.com/?auth=TOKEN
  • Token validated successfully
  • Sign-in modal skipped
  • User plays immediately
  • Test expired token
  • Test invalid signature