Famous.ai Auth Template
Copy-paste reference implementation to fix auth loops
// ============================================
// 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!)
// ============================================
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>
);
}🚨 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
// ============================================
✅ 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.comPriority 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.
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.
- Generate test token: Visit ScrabbleDiagnostic and click "Generate Test JWT Token"
- Copy redirect URL: Copy the full redirect URL (includes ?auth=TOKEN)
- Open in incognito: Paste URL in incognito/private browsing window
- Check console: Open DevTools and look for "[PlatformAuth]" logs
- Verify behavior: Game should skip auth modal and proceed directly to gameplay
- Check storage: DevTools → Application → Local Storage → verify snappy_* keys exist
- Test expiry: Wait for token to expire (24 hours) or modify exp claim, reload page
- Test guest mode: Clear localStorage and reload without ?auth param