Navigation

Famous.ai Auth Template

Copy-paste reference implementation to fix auth loops

Core Authentication Logic
// ============================================
// COPY THIS INTO YOUR FAMOUS.AI GAME
// File: src/utils/platformAuth.ts (or similar)
// ============================================

const VERIFY_TOKEN_URL = 'https://snappyidioms.com/api/verifyGameToken';

interface AuthResult {
  valid: boolean;
  user_id?: string;
  email?: string;
  name?: string;
  isPremium?: boolean;
  expires_at?: number;
  error?: string;
  expired?: boolean;
}

export async function initializePlatformAuth(): Promise<AuthResult | null> {
  console.log('[PlatformAuth] Starting initialization...');
  
  // ✅ STEP 1: Check for JWT token FIRST (highest priority)
  const urlParams = new URLSearchParams(window.location.search);
  const jwtToken = urlParams.get('auth');
  
  if (jwtToken) {
    console.log('[PlatformAuth] JWT token found in URL, validating...');
    
    try {
      // Validate token server-side
      const response = await fetch(VERIFY_TOKEN_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: jwtToken })
      });
      
      const result: AuthResult = await response.json();
      
      if (result.valid) {
        console.log('[PlatformAuth] ✅ JWT validated, user:', result.email);
        
        // Store auth data in localStorage
        localStorage.setItem('snappy_platform_token', jwtToken);
        localStorage.setItem('snappy_user_email', result.email || '');
        localStorage.setItem('snappy_username', result.name || '');
        localStorage.setItem('snappy_is_premium', String(result.isPremium || false));
        localStorage.setItem('snappy_is_guest', 'false');
        
        // Clean URL (remove token for security)
        const cleanUrl = window.location.pathname;
        window.history.replaceState({}, '', cleanUrl);
        
        return result;
      } else {
        console.warn('[PlatformAuth] ❌ JWT validation failed:', result.error);
        
        // Token invalid but DO NOT redirect - fall through to guest mode
        if (result.expired) {
          console.log('[PlatformAuth] Token expired, clearing storage');
          localStorage.removeItem('snappy_platform_token');
        }
      }
    } catch (error) {
      console.error('[PlatformAuth] Verification request failed:', error);
    }
  }
  
  // ✅ STEP 2: Check localStorage for existing session
  const storedToken = localStorage.getItem('snappy_platform_token');
  const storedEmail = localStorage.getItem('snappy_user_email');
  
  if (storedToken && storedEmail) {
    console.log('[PlatformAuth] Found stored session for:', storedEmail);
    return {
      valid: true,
      email: storedEmail,
      name: localStorage.getItem('snappy_username') || '',
      isPremium: localStorage.getItem('snappy_is_premium') === 'true'
    };
  }
  
  // ✅ STEP 3: Guest mode (no redirects!)
  console.log('[PlatformAuth] No valid auth, entering guest mode');
  localStorage.setItem('snappy_is_guest', 'true');
  
  return null; // Indicates guest mode
}

// ============================================
// HOW TO USE IN YOUR GAME COMPONENT
// ============================================

/*
import { initializePlatformAuth } from './utils/platformAuth';

function App() {
  const [authState, setAuthState] = useState<'loading' | 'authenticated' | 'guest'>('loading');
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function authenticate() {
      const result = await initializePlatformAuth();
      
      if (result && result.valid) {
        setAuthState('authenticated');
        setUser(result);
        // ✅ SKIP AUTH MODAL - proceed directly to game
      } else {
        setAuthState('guest');
        // ✅ Show guest modal but DO NOT redirect to SnappyIdioms.com
      }
    }
    
    authenticate();
  }, []);

  if (authState === 'loading') {
    return <LoadingScreen />;
  }

  if (authState === 'guest') {
    return <GuestModeModal />; // Let them play as guest or sign up
  }

  return <GameInterface user={user} />;
}
*/
Guest Mode Modal (No Redirects)
// ============================================
// GUEST MODE MODAL (NO REDIRECTS!)
// ============================================

function GuestModeModal() {
  const [guestName, setGuestName] = useState('');
  
  const playAsGuest = () => {
    const name = guestName || `Guest_${Math.floor(Math.random() * 10000)}`;
    localStorage.setItem('snappy_username', name);
    localStorage.setItem('snappy_is_guest', 'true');
    
    // Close modal and start game
    window.location.reload();
  };
  
  return (
    <div className="modal">
      <h2>Welcome to Scrabble!</h2>
      
      <div className="options">
        {/* Option 1: Quick Play as Guest */}
        <button onClick={playAsGuest} className="primary">
          🎮 Quick Play (Guest Mode)
        </button>
        
        {/* Option 2: Sign up on SnappyIdioms.com */}
        <a 
          href="https://snappyidioms.com/register" 
          target="_blank"
          className="secondary"
        >
          ✨ Create Free Account (Save Progress)
        </a>
        
        {/* ❌ NEVER do this - causes infinite loop: */}
        {/* <button onClick={() => window.location.href = 'https://snappyidioms.com/login'}>Sign In</button> */}
      </div>
      
      <input
        type="text"
        placeholder="Enter your name (optional)"
        value={guestName}
        onChange={(e) => setGuestName(e.target.value)}
      />
    </div>
  );
}
🔐 Session-Level Guard (MUST HAVE)

🚨 WHY THIS IS CRITICAL:

Without cameFromPlatform() guard in AppLayout.tsx, the game will redirect back to SnappyIdioms.com when the JWT token expires (after 24 hours), causing an infinite loop. This session-level flag survives JWT expiry and blocks ALL redirects for the entire browser tab session.

// ============================================
// 🔐 SESSION-LEVEL GUARD (ULTIMATE LOOP PREVENTER)
// ============================================

// Add this function to platformAuth.ts
export function cameFromPlatform(): boolean {
  // This flag persists even after JWT expires
  // Set once when ?auth=TOKEN is detected
  return sessionStorage.getItem('__snappy_came_from_platform') === 'true';
}

// Set flag in initializePlatformAuth when JWT detected:
if (jwtToken) {
  sessionStorage.setItem('__snappy_came_from_platform', 'true');
  // ... rest of JWT processing
}

// ============================================
// USE IN APPLAYOUT.TSX (CRITICAL)
// ============================================

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 (not from SnappyIdioms.com)
  const redirectUrl = `${SNAPPY_LOGIN_URL}?redirect=${encodeURIComponent(GAME_URL)}`;
  window.open(redirectUrl, '_blank');
}

// WHY THIS MATTERS:
// - JWT tokens expire after 24 hours
// - When expired, isPlatformAuthenticated becomes false
// - Without cameFromPlatform(), expired token triggers redirect → LOOP
// - cameFromPlatform() is session-level - survives JWT expiry
// - Blocks ALL redirects for entire browser tab session

// ============================================
// ALSO ADD TO ROUND COMPLETE / POST-GAME SCREENS
// ============================================

// In AppLayout.tsx or RoundComplete.tsx:
const onSignIn = () => {
  // Same triple guard as handleSignInClick
  if (isPlatformAuthLoading || isPlatformAuthenticated || cameFromPlatform()) {
    console.log('[Auth] Blocked redirect from game end screen');
    return;
  }
  
  window.open(`${SNAPPY_LOGIN_URL}?redirect=${encodeURIComponent(GAME_URL)}`, '_blank');
};

// Apply to ALL callback scenarios:
// - End of round "Sign in to save score"
// - Achievement unlock "Sign in to sync"
// - Leaderboard "Sign in to submit"
// - Any prompt that tries to redirect to SnappyIdioms.com
Pre-Launch Checklist
// ============================================
// PRE-LAUNCH CHECKLIST
// ============================================

✅ Session-level guard (cameFromPlatform) in AppLayout.tsx - MOST CRITICAL
✅ JWT token check happens FIRST (before localStorage)
✅ Token validated via https://snappyidioms.com/api/verifyGameToken
✅ If token valid → setAuthState('authenticated') + skip modal
✅ If token invalid/expired → fall through to guest mode (NO REDIRECT)
✅ Guest mode shows modal but NEVER redirects to SnappyIdioms.com
✅ URL cleaned after token extraction (window.history.replaceState)
✅ Console logs added for debugging auth flow
✅ localStorage keys use 'snappy_' prefix for consistency

❌ ANTI-PATTERNS (causes infinite loop):
❌ Missing cameFromPlatform() guard in AppLayout (allows loops after JWT expiry)
❌ Checking localStorage before checking JWT token
❌ Redirecting to SnappyIdioms.com/login when no session found
❌ Showing auth modal when JWT token exists
❌ Not removing ?auth=TOKEN from URL after validation

// Test the integration:
1. Generate token at https://snappyidioms.com/ScrabbleDiagnostic
2. Open game with ?auth=TOKEN in incognito window
3. Check console for "[PlatformAuth]" logs
4. Verify game skips auth modal and proceeds to gameplay
5. Check localStorage has snappy_* keys set correctly
6. Wait for JWT expiry (or modify token) and verify NO redirect back to SnappyIdioms.com
🔑 Critical Implementation Points

Priority Order Matters

Check JWT token FIRST, then localStorage, then guest mode. Never reverse this order.

Server-Side Validation Only

Always validate tokens via https://snappyidioms.com/api/verifyGameToken. Never validate client-side.

No Redirects to SnappyIdioms.com

Guest mode shows a modal with "Create Account" link that opens in new tab. Never redirect the current window.

Clean URL After Auth

Use window.history.replaceState() to remove ?auth=TOKEN from URL after validation for security.

Reusable for All Games

Once working for Scrabble, copy this exact code to Guess What, Crossword, Word Up, I Spy, etc.

🔄 Silent Token Refresh (Optional)

Prevent Token Expiry During Gameplay:

Instead of showing "Token Expired" modals, silently refresh the JWT when it's about to expire (30 minutes before).

// Check token expiry every 5 minutes
useEffect(() => {
  const checkTokenExpiry = async () => {
    const token = localStorage.getItem('snappy_platform_token');
    if (!token) return;
    
    try {
      const [, payloadB64] = token.split('.');
      const payload = JSON.parse(atob(payloadB64));
      const timeUntilExpiry = payload.exp - Date.now();
      const thirtyMinutes = 30 * 60 * 1000;
      
      if (timeUntilExpiry < thirtyMinutes && timeUntilExpiry > 0) {
        console.log('[Auth] Token expiring soon, refreshing...');
        
        const response = await fetch('https://snappyidioms.com/api/refreshGameToken', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ currentToken: token })
        });
        
        const result = await response.json();
        
        if (result.needsRefresh && result.token) {
          localStorage.setItem('snappy_platform_token', result.token);
          console.log('[Auth] ✅ Token refreshed silently');
        }
      }
    } catch (error) {
      console.error('[Auth] Token refresh failed:', error);
    }
  };
  
  const interval = setInterval(checkTokenExpiry, 5 * 60 * 1000); // Every 5 minutes
  checkTokenExpiry(); // Check immediately
  
  return () => clearInterval(interval);
}, []);

Note: This is optional. The cameFromPlatform() guard already prevents loops after expiry. Silent refresh just provides a better UX by keeping users authenticated indefinitely.

🧪 Testing Your Implementation
  1. Generate test token: Visit ScrabbleDiagnostic and click "Generate Test JWT Token"
  2. Copy redirect URL: Copy the full redirect URL (includes ?auth=TOKEN)
  3. Open in incognito: Paste URL in incognito/private browsing window
  4. Check console: Open DevTools and look for "[PlatformAuth]" logs
  5. Verify behavior: Game should skip auth modal and proceed directly to gameplay
  6. Check storage: DevTools → Application → Local Storage → verify snappy_* keys exist
  7. Test expiry: Wait for token to expire (24 hours) or modify exp claim, reload page
  8. Test guest mode: Clear localStorage and reload without ?auth param