Scrabble JWT Authentication Guide
Implementation guide for Famous.ai Scrabble game to validate SnappyIdioms.com JWT tokens
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. Read ?auth=TOKEN on page load - Extract from URL params immediately
- 2. Validate token server-side - Call SnappyIdioms.com/api/verifyGameToken
- 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
When users click "Play Scrabble" from SnappyIdioms.com, they are redirected to:
https://play.snappyidioms.com/?auth=TOKENThe Famous.ai Scrabble game MUST read this token, validate it, and proceed directly to gameplay.
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, errorsWhy: 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
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
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
}
}โ ๏ธ 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/verifyGameTokenCall 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
);
}๐ 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
isPlatformAuthLoadingandisPlatformAuthenticatedbecomefalse - โข 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');
}๐จ 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
โ 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.
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
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. Client detects JWT expiry approaching (< 30 min)
- 2. Client calls
action: 'refresh_token'with current token - 3. Proxy re-validates token with SnappyIdioms.com
- 4. If valid: extend session by 24 hours
- 5. If expired: attempt identity-based refresh
- 6. Return new expiry + optional new token
- 7. Client extends local session without redirecting
This prevents redirect loops by keeping sessions alive server-side without sending users back to SnappyIdioms.com
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.
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. Generate a test token
- 2. Copy the redirect URL
- 3. Test in incognito/private window
- 4. Check console for debug logs
- 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