Firebase Authentication Guide
This application uses Firebase Authentication for secure user authentication with Google OAuth, email/password, and session management.
Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Firebase Auth Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. User clicks "Sign in with Google" β
β β β
β 2. Firebase opens Google OAuth popup β
β β β
β 3. User authenticates with Google β
β β β
β 4. Firebase returns UserCredential β
β β β
β 5. App redirects to /access (Smart Routing Proxy) β
β β β
β 6. /access determines: β
β - First-time user (< 5 min old) β /onboarding β
β - Returning user β /dashboard/chat β
β - Custom redirect β specified URL β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββFile Structure
lib/
βββ firebase/
β βββ config.ts # Firebase config & initialization
β βββ auth.ts # Auth utilities
βββ services/
β βββ firebase-auth.service.ts # Main auth service (all methods)
β βββ firebase-user.service.ts # User profile management
β βββ firebase-project.service.ts # Project management
contexts/
βββ AuthContext.tsx # React context for auth state
app/
βββ (marketing)/auth/
β βββ login/page.tsx # Login page
β βββ register/page.tsx # Registration page
βββ __/auth/handler/page.tsx # Firebase redirect handler
βββ access/page.tsx # Smart routing proxy
components/
βββ auth/
βββ simple-auth-form.tsx # Auth form componentSetup & Configuration
Create Firebase Project
- Create a Firebase project at console.firebase.google.com (opens in a new tab)
- Enable Authentication β Google Sign-In
- Add authorized domains (localhost, your-domain.com)
- Get your Firebase config
Configure Environment Variables
Create .env.local:
# Firebase Configuration
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSy...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-app.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-app.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=G-ABC123XYZ
# Optional: Custom auth domain (for Next.js 16 compatibility)
NEXT_PUBLIC_FIREBASE_CUSTOM_AUTH_DOMAIN=altsportsleagues.aiInitialize Firebase
// lib/firebase/config.ts
import { initializeApp, getApps } from 'firebase/app'
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
}
// Initialize Firebase (singleton pattern)
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]
export const auth = getAuth(app)
export const googleProvider = new GoogleAuthProvider()
export default appAuthentication Methods
Popup Method (Recommended)
import { FirebaseAuthService } from '@/lib/services/firebase-auth.service'
const handleGoogleSignIn = async () => {
try {
await FirebaseAuthService.signInWithGoogle()
router.push('/access') // Smart routing proxy
} catch (error: any) {
if (error.message === 'POPUP_CLOSED') {
toast.info('Sign-in cancelled. Click "Continue with Google" to try again.')
} else if (error.message === 'Popup blocked - redirecting to Google sign-in') {
toast.info('Popup blocked. Redirecting to Google...')
} else {
toast.error('Failed to sign in with Google')
}
}
}Smart Routing Proxy
The /access route intelligently routes users based on their state:
// app/access/page.tsx
const getSmartDestination = async (user: User): Promise<string> => {
// 1. Check for external redirect URL
if (redirectUrl) return redirectUrl
// 2. Check for internal return URL
if (returnTo) return returnTo
// 3. Determine if first-time or returning user
const creationTime = user.metadata?.creationTime
if (creationTime) {
const minutesSinceCreation = getMinutesSince(creationTime)
// First-time user (< 5 minutes old)
if (minutesSinceCreation < 5) {
return '/onboarding'
}
}
// 4. Default: returning user
return '/dashboard/chat'
}Usage Examples
// Basic: Let smart routing decide
router.push('/access')
// With return URL (after protected route redirect)
router.push('/access?returnTo=/dashboard/markets')
// With external redirect (for cross-domain auth)
router.push('/access?redirectUrl=https://external.com/callback')API Reference
FirebaseAuthService Methods
// Sign-In Methods
static async signInWithGoogle(): Promise<UserCredential>
static async signInWithEmail(email: string, password: string): Promise<UserCredential>
static async createAccount(email: string, password: string, displayName?: string): Promise<UserCredential>
static async signOut(): Promise<void>
static async handleRedirectResult(): Promise<UserCredential | null>
// User Management
static getCurrentUser(): User | null
static toAuthUser(user: User): AuthUser
static isAuthenticated(): boolean
static async getIdToken(forceRefresh = false): Promise<string | null>
static onAuthStateChange(callback: (user: User | null) => void): () => void
// Profile Updates
static async resetPassword(email: string): Promise<void>
static async updateUserProfile(updates: { displayName?: string; photoURL?: string }): Promise<void>
static async updateUserEmail(newEmail: string, password: string): Promise<void>
static async updateUserPassword(currentPassword: string, newPassword: string): Promise<void>AuthContext Hook
'use client'
import { useAuth } from '@/contexts/AuthContext'
export function UserProfile() {
const {
user, // Firebase User | null
userProfile, // Extended profile data
loading, // Auth state loading
signInWithGoogle, // Sign-in method
signOut, // Sign-out method
} = useAuth()
if (loading) return <LoadingSpinner />
if (!user) return <LoginPrompt />
return <div>Welcome, {user.displayName}!</div>
}Protected Routes
Layout-Level Protection
// app/(dashboard)/dashboard/layout.tsx
'use client'
import { useAuth } from '@/contexts/AuthContext'
import { redirect } from 'next/navigation'
import { useEffect } from 'react'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth()
useEffect(() => {
if (!loading && !user) {
redirect('/auth/login')
}
}, [user, loading])
if (loading) return <LoadingScreen />
if (!user) return null
return <DashboardLayout>{children}</DashboardLayout>
}Error Handling
Common Firebase Auth Errors
const handleAuthError = (error: any) => {
switch (error.code) {
case 'auth/popup-closed-by-user':
toast.info('Sign-in cancelled. Try again when ready.')
break
case 'auth/popup-blocked':
toast.warning('Popup blocked. Redirecting to Google...')
break
case 'auth/user-not-found':
toast.error('No account found with this email.')
break
case 'auth/wrong-password':
toast.error('Incorrect password.')
break
case 'auth/email-already-in-use':
toast.error('Email already registered. Try signing in.')
break
case 'auth/weak-password':
toast.error('Password too weak. Use at least 8 characters.')
break
case 'auth/too-many-requests':
toast.error('Too many attempts. Try again later.')
break
default:
toast.error(error.message || 'Authentication failed')
}
}Best Practices
β οΈ
Use Popup Method for OAuth - Popup prevents redirect loops in Next.js 16 and provides better UX.
Recommended Patterns
// β
Good: Use popup method
await signInWithPopup(auth, googleProvider)
// β Avoid: Causes redirect loops in Next.js 16
await signInWithRedirect(auth, googleProvider)
// β
Good: Handle popup-blocked gracefully
try {
await signInWithPopup(auth, googleProvider)
} catch (error: any) {
if (error.code === 'auth/popup-blocked') {
await signInWithRedirect(auth, googleProvider)
toast.info('Popup blocked. Redirecting to Google...')
}
}
// β
Good: Always use smart routing
router.push('/access')
// β Avoid: Hardcoded destination
router.push('/dashboard/explore')Security Rules
Firestore Security Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User profiles - users can only read/write their own
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Leagues - read public, write owner only
match /leagues/{leagueId} {
allow read: if true;
allow write: if request.auth != null &&
request.auth.uid == resource.data.ownerUid;
}
}
}Troubleshooting
| Issue | Solution |
|---|---|
| "Popup closed by user" error | Handle gracefully - user cancelled |
| Popup blocked by browser | Fallback to redirect method |
| Redirect loop in Next.js 16 | Use popup method instead |
| "Firebase auth not initialized" | Check environment variables |
| User state not persisting | Ensure AuthContext wraps your app |