Navigation

FamousAI Auth Implementation Guide

Complete copy-paste solution to fix redirect loops

Current Problem: Infinite Redirect Loop

1. User clicks "Play Scrabble" on SnappyIdioms.com

2. Redirected to: play.snappyidioms.com/?auth=TOKEN

3. FamousAI doesn't validate the token

4. FamousAI redirects back to SnappyIdioms.com

5. ♾️ INFINITE LOOP

Solution: Validate JWT Token

✅ Read ?auth=TOKEN from URL on page load

✅ Validate with SnappyIdioms API

✅ Store user data in localStorage

✅ Start game immediately - NO redirects

✅ Clean URL (remove ?auth=)

Step 1: Client-Side Auth (snappyAuth.js)
// ============================================
// FAMOUS.AI CLIENT-SIDE AUTH IMPLEMENTATION
// File: src/auth/snappyAuth.js
// ============================================

/**
 * CRITICAL: This must run BEFORE any other auth checks
 * Call this in your App.js or main game component useEffect
 */

export async function initSnappyAuth() {
  console.log('[SNAPPY-AUTH] Initializing...');
  
  // Step 1: Check if JWT token exists in URL
  const urlParams = new URLSearchParams(window.location.search);
  const jwtToken = urlParams.get('auth');
  
  if (!jwtToken) {
    console.log('[SNAPPY-AUTH] No JWT token in URL');
    return { authenticated: false, reason: 'no_token' };
  }

  console.log('[SNAPPY-AUTH] JWT token found, validating...');

  try {
    // Step 2: Validate token with SnappyIdioms backend
    const response = await fetch('https://snappyidioms.com/functions/verifyGameToken', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: jwtToken })
    });

    const result = await response.json();

    if (result.valid) {
      console.log('[SNAPPY-AUTH] ✅ Token valid for user:', result.email);
      
      // Step 3: Store user data in localStorage
      localStorage.setItem('snappy_platform_token', jwtToken);
      localStorage.setItem('snappy_username', result.name);
      localStorage.setItem('snappy_user_email', result.email);
      localStorage.setItem('snappy_is_premium', result.isPremium);
      localStorage.setItem('snappy_is_guest', 'false');
      localStorage.setItem('snappy_auth_timestamp', Date.now().toString());

      // Step 4: Clean URL (remove ?auth=token)
      const cleanUrl = window.location.origin + window.location.pathname;
      window.history.replaceState({}, document.title, cleanUrl);

      return {
        authenticated: true,
        user: {
          email: result.email,
          name: result.name,
          isPremium: result.isPremium
        }
      };
    } else {
      console.error('[SNAPPY-AUTH] ❌ Invalid token:', result.error);
      return { authenticated: false, reason: 'invalid_token' };
    }
  } catch (error) {
    console.error('[SNAPPY-AUTH] ❌ Validation failed:', error);
    return { authenticated: false, reason: 'validation_error', error };
  }
}

/**
 * Check if user is authenticated (call this for session checks)
 */
export function isSnappyAuthenticated() {
  const token = localStorage.getItem('snappy_platform_token');
  const timestamp = localStorage.getItem('snappy_auth_timestamp');
  
  if (!token || !timestamp) return false;
  
  // Token expires after 24 hours
  const age = Date.now() - parseInt(timestamp);
  if (age > 24 * 60 * 60 * 1000) {
    console.log('[SNAPPY-AUTH] Token expired');
    clearSnappyAuth();
    return false;
  }
  
  return true;
}

/**
 * Get current user data
 */
export function getSnappyUser() {
  return {
    email: localStorage.getItem('snappy_user_email'),
    name: localStorage.getItem('snappy_username'),
    isPremium: localStorage.getItem('snappy_is_premium') === 'true',
    isGuest: localStorage.getItem('snappy_is_guest') === 'true'
  };
}

/**
 * Clear auth data (for logout or reset)
 */
export function clearSnappyAuth() {
  localStorage.removeItem('snappy_platform_token');
  localStorage.removeItem('snappy_username');
  localStorage.removeItem('snappy_user_email');
  localStorage.removeItem('snappy_is_premium');
  localStorage.removeItem('snappy_is_guest');
  localStorage.removeItem('snappy_auth_timestamp');
}
Step 2: App.js Integration
// ============================================
// APP.JS INTEGRATION
// ============================================

import React, { useEffect, useState } from 'react';
import { initSnappyAuth, isSnappyAuthenticated, getSnappyUser } from './auth/snappyAuth';

function App() {
  const [authState, setAuthState] = useState('checking');
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function handleAuth() {
      // CRITICAL: Check JWT token FIRST, before any other auth logic
      const result = await initSnappyAuth();
      
      if (result.authenticated) {
        setUser(result.user);
        setAuthState('authenticated');
        return; // STOP HERE - don't show any auth modals
      }

      // If no JWT, check existing session
      if (isSnappyAuthenticated()) {
        setUser(getSnappyUser());
        setAuthState('authenticated');
        return;
      }

      // No valid auth - show guest mode or sign-in
      setAuthState('unauthenticated');
    }

    handleAuth();
  }, []);

  if (authState === 'checking') {
    return <div>Loading...</div>;
  }

  if (authState === 'authenticated') {
    return <GameComponent user={user} />;
  }

  return <AuthModal />; // Only show if no JWT and no session
}
API Endpoint (Already exists on SnappyIdioms)

FamousAI calls: https://snappyidioms.com/api/verifyGameToken

// ============================================
// SNAPPYIDIOMS BACKEND - verifyGameToken
// File: functions/verifyGameToken
// (Already implemented - FamousAI just needs to call it)
// ============================================

import Deno from 'https://deno.land/x/deno@v1.32.0/mod.ts';

const JWT_SECRET = Deno.env.get('JWT_SECRET');

Deno.serve(async (req) => {
  // CORS headers
  if (req.method === 'OPTIONS') {
    return new Response('ok', {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type'
      }
    });
  }

  try {
    const { token } = await req.json();

    // Decode and verify JWT
    const [header, payload, signature] = token.split('.');
    const decodedPayload = JSON.parse(atob(payload));

    // Check expiration
    if (Date.now() > decodedPayload.exp) {
      return Response.json({ valid: false, error: 'Token expired' });
    }

    // Verify signature
    const isValid = await verifySignature(`${header}.${payload}`, signature, JWT_SECRET);

    if (isValid) {
      return Response.json({
        valid: true,
        user: {
          email: decodedPayload.email,
          name: decodedPayload.name,
          isPremium: decodedPayload.isPremium
        }
      });
    }

    return Response.json({ valid: false, error: 'Invalid signature' });
  } catch (error) {
    return Response.json({ valid: false, error: error.message }, { status: 500 });
  }
});
Implementation Checklist
// ============================================
// IMPLEMENTATION CHECKLIST FOR FAMOUS.AI
// ============================================

✅ STEP 1: Add snappyAuth.js to your project
   - Create file: src/auth/snappyAuth.js
   - Copy the "Client-Side Auth" code above

✅ STEP 2: Update App.js or main game component
   - Import: import { initSnappyAuth } from './auth/snappyAuth'
   - Call initSnappyAuth() in useEffect BEFORE any other auth checks
   - Do NOT show sign-in modal if authenticated

✅ STEP 3: Test the flow
   1. Go to https://snappyidioms.com
   2. Sign in as a user
   3. Click "Play Scrabble"
   4. Should redirect to: https://play.snappyidioms.com/?auth=TOKEN
   5. Game should validate token and start immediately
   6. NO redirect back to SnappyIdioms
   7. When done, user can navigate back manually

✅ STEP 4: Add "Reset Game Data" button (Settings/Footer)
   import { clearSnappyAuth } from './auth/snappyAuth';
   
   <button onClick={() => {
     if (confirm('Clear all game data?')) {
       clearSnappyAuth();
       window.location.reload();
     }
   }}>
     Reset Game Data
   </button>

✅ STEP 5: Environment Variable (Already set on FamousAI)
   - JWT_SECRET must match SnappyIdioms' secret
   - Contact Samuel if you don't have it

⚠️ CRITICAL: DO NOT redirect to SnappyIdioms.com when JWT is valid
⚠️ CRITICAL: Remove any code that redirects back when auth succeeds
Troubleshooting Loops
// ============================================
// TROUBLESHOOTING INFINITE LOOPS
// ============================================

❌ WRONG: Checking session before JWT
if (!localStorage.getItem('user')) {
  window.location.href = 'https://snappyidioms.com'; // CAUSES LOOP
}

✅ CORRECT: Check JWT first
const auth = await initSnappyAuth();
if (auth.authenticated) {
  // Start game immediately
  return;
}
// Then check local session
if (isSnappyAuthenticated()) {
  // Continue playing
  return;
}
// Only now show auth modal

❌ WRONG: Always redirecting on any error
if (!token || error) {
  window.location.href = 'https://snappyidioms.com'; // CAUSES LOOP
}

✅ CORRECT: Show error in-game
if (!token || error) {
  showErrorMessage('Session expired. Please return to SnappyIdioms.');
  // DO NOT auto-redirect
}
Testing the Fix
  1. 1. Sign in to snappyidioms.com
  2. 2. Click "Play Scrabble"
  3. 3. Should see: play.snappyidioms.com/?auth=TOKEN
  4. 4. Game validates token and starts playing
  5. 5. URL cleans to: play.snappyidioms.com
  6. 6. User plays the game
  7. 7. When done, user navigates back manually (browser back button)
  8. 8. ✅ NO INFINITE LOOP