Authentication is a foundational requirement for modern web applications, yet building a secure, scalable authentication system from scratch is complex and error-prone. Firebase Authentication provides a battle-tested solution that supports multiple sign-in methods, including Google and GitHub OAuth, email/password combinations, and more. When combined with Next.js, developers get the best of both worlds: Firebase handles the authentication heavy lifting while Next.js provides performance optimization, server-side rendering, and excellent developer experience.
This guide walks through implementing a complete authentication system in Next.js 14+ using the App Router and Firebase Authentication. You'll learn how to set up Firebase, create a reusable authentication context, implement various sign-in methods, and protect routes to ensure only authorized users access your application.
For teams building full-stack applications, combining Firebase Authentication with our custom web development services creates a powerful foundation for secure, scalable user experiences.
Firebase Project Setup
Configure Firebase project, enable authentication providers, and set up environment variables for secure access
AuthContext Pattern
Create a React Context provider to manage authentication state across your Next.js application
OAuth Integration
Implement Google and GitHub sign-in using Firebase's unified authentication API
Email/Password Auth
Build secure registration, login, and password reset flows with Firebase Authentication
Route Protection
Protect routes using client-side checks and Next.js middleware for unauthorized access prevention
Security Best Practices
Implement email verification, session management, and abuse prevention measures
Setting Up Firebase for Next.js Authentication
Before writing any code, you need to configure Firebase to accept authentication requests from your Next.js application. The setup process involves creating a Firebase project, enabling the authentication providers you want to support, and configuring your application to communicate securely with Firebase services.
Creating a Firebase Project
Navigate to the Firebase Console and create a new project. During project creation, you can enable Google Analytics for additional insights into user behavior, though this is optional for authentication functionality. Once your project is ready, navigate to the Authentication section from the left sidebar and click "Get Started" to access the authentication dashboard.
Enabling Authentication Providers
For Google Sign-In, enable the Google provider in the Sign-in method tab. For GitHub integration, you'll need to register your application in the GitHub Developer settings to obtain a Client ID and Client Secret, then configure these credentials in the Firebase GitHub provider settings.
For production applications, configure authorized domains in the Firebase Console to prevent unauthorized sites from using your Firebase project. This adds an important security layer by restricting authentication to your registered domains.
Environment Variable Configuration
Store your Firebase configuration in environment variables rather than hardcoding them in your application:
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_project_id.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_project_id.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
The NEXT_PUBLIC_ prefix makes these variables available to client-side code where Firebase authentication occurs. Firebase's security model ensures these public API keys are safe to expose since they only grant access to the services you've enabled.
Creating the Firebase Initialization File
Create lib/firebase-config.ts with the initialization code:
import { initializeApp, getApps, type FirebaseApp } from 'firebase/app'
import { getAuth } 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,
}
let app: FirebaseApp
if (!getApps().length) {
app = initializeApp(firebaseConfig)
} else {
app = getApps()[0]
}
export const auth = getAuth(app)
We check for existing apps before initializing to prevent duplicate initialization errors during hot module replacement in development. This pattern ensures a single Firebase instance across your entire application, which is essential for consistent authentication state.
Security Best Practices for Configuration
Never commit .env.local to version control. Use separate Firebase projects for development and production environments to isolate data and prevent accidental production access during development. Consider implementing Firebase App Check for additional protection against abuse and unauthorized API calls.
For server-side operations that require elevated privileges, use the Firebase Admin SDK with credentials stored in environment variables without the NEXT_PUBLIC_ prefix. These admin credentials should never be exposed to the client-side code.
Following secure configuration practices aligns with our comprehensive approach to web security services that protect your applications from evolving threats.
Managing Authentication State with React Context
Authentication state needs to be accessible throughout your Next.js application, but passing user objects and authentication methods through props at every level leads to messy code. React Context provides a clean solution by making authentication state available to any component that needs it without explicit prop drilling. Combined with the useAuth hook pattern, you get a simple, reusable API for accessing authentication throughout your application.
Creating the AuthContext Provider
The AuthContext wraps your entire application and listens for changes to the Firebase authentication state. When a user signs in or out, Firebase triggers an onAuthStateChanged callback that updates the context, causing all subscribed components to re-render with the new state. This reactive approach ensures your UI always reflects the current authentication status.
'use client'
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
import { User, onAuthStateChanged, signOut as firebaseSignOut } from 'firebase/auth'
import { auth } from '../lib/firebaseConfig'
interface AuthContextType {
user: User | null
loading: boolean
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser || null)
setLoading(false)
})
return () => unsubscribe()
}, [])
const signOut = async () => {
await firebaseSignOut(auth)
}
return (
<AuthContext.Provider value={{ user, loading, signOut }}>
{children}
</AuthContext.Provider>
)
}
The useState hooks manage the user object and loading state. The useEffect sets up the authentication state listener and returns a cleanup function that unsubscribes when components unmount, preventing memory leaks. The useAuth hook provides a simple API for components to access auth state with built-in error handling.
Wrapping Your Application
Add the AuthProvider to your root layout to make authentication state available throughout your Next.js application:
import { AuthProvider } from './context/AuthContext'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
)
}
For optimal performance, consider lazy loading the AuthProvider if your application has many unauthenticated visitors. This pattern integrates seamlessly with Next.js's server components while maintaining security for protected routes.
Implementing authentication with Firebase complements our full-stack web development services that leverage modern frameworks and cloud platforms for scalable, secure applications.
Protecting Routes in Next.js
Route protection ensures that only authenticated users can access certain pages of your application. There are two primary approaches: client-side protection using the useAuth hook and server-side protection using Next.js middleware. Each approach has trade-offs between user experience and security that you should understand before implementing.
Client-Side Route Protection
For client-side protection, wrap protected page content with a check that redirects unauthenticated users. This approach is simple to implement and works well for pages that load quickly:
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useAuth } from '../context/AuthContext'
export default function DashboardPage() {
const { user, loading } = useAuth()
const router = useRouter()
useEffect(() => {
if (!loading && !user) {
router.push('/login')
}
}, [user, loading, router])
if (loading) return <p>Loading...</p>
if (!user) return null
return (
<div className="p-6">
<h1>Welcome, {user.displayName || user.email}</h1>
</div>
)
}
This pattern provides a smooth transition while authentication status is being determined. The loading state prevents content flash while Firebase confirms the user's session.
Server-Side Protection with Middleware
For enhanced security, implement route protection at the middleware level. Middleware runs before page rendering, so unauthenticated users never see protected content:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
Middleware-based protection provides stronger security for sensitive pages but requires additional setup to verify Firebase tokens server-side using the Firebase Admin SDK. Choose client-side protection for simpler implementations and faster development, middleware for production applications with strict security requirements.
When to Use Each Approach
Client-side protection works well for dashboards and pages where brief unauthenticated access isn't critical. Use middleware for pages containing sensitive data, payment flows, or administrative functions where unauthorized access could cause harm. Many applications benefit from a hybrid approach, using client-side checks for better user experience while implementing middleware for critical routes.
For applications requiring enterprise-grade security, our web security services provide comprehensive protection strategies tailored to your specific requirements.
Implementing OAuth Sign-In Providers
Social authentication through OAuth providers like Google and GitHub offers users a convenient sign-in experience without remembering additional passwords. Firebase Authentication supports these providers natively through a unified API that handles the OAuth flow, token exchange, and user creation or linking automatically.
Google Sign-In Implementation
Google Sign-In is the most straightforward OAuth provider to implement and offers the highest conversion rates for sign-up and sign-in because most users are already signed into their Google accounts:
'use client'
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
import { auth } from '../lib/firebaseConfig'
export default function GoogleSignInButton() {
const googleProvider = new GoogleAuthProvider()
const signInWithGoogle = async () => {
try {
await signInWithPopup(auth, googleProvider)
} catch (error) {
console.error('Google sign-in error:', error)
}
}
return (
<button onClick={signInWithGoogle}>
Sign in with Google
</button>
)
}
After the user grants permission, Firebase receives an ID token that identifies the user, creates or retrieves their Firebase account, and returns the authenticated user object. You can access profile information including name, email, and profile photo to personalize the experience.
GitHub Sign-In Integration
GitHub authentication requires additional setup in both Firebase and GitHub. In GitHub's developer settings, register your application with the callback URL Firebase provides, then enter the Client ID and Client Secret into Firebase's GitHub provider configuration:
'use client'
import { GithubAuthProvider, signInWithPopup } from 'firebase/auth'
import { auth } from '../lib/firebaseConfig'
export default function GitHubSignInButton() {
const githubProvider = new GithubAuthProvider()
const signInWithGitHub = async () => {
try {
await signInWithPopup(auth, githubProvider)
} catch (error) {
console.error('GitHub sign-in error:', error)
}
}
return (
<button onClick={signInWithGitHub}>
Sign in with GitHub
</button>
)
}
GitHub authentication is particularly valuable for applications targeting developers or those integrating with GitHub's API. Users authenticated through GitHub can access repositories, organizations, and other GitHub resources, enabling features like authenticated API calls on their behalf.
Error Handling Patterns
Handle common error scenarios like user cancellation, network errors, and cases where an account already exists with a different provider. Firebase provides specific error codes that enable graceful error handling and user-friendly messages.
Email and Password Authentication
While OAuth providers offer convenience, email and password authentication remains essential for users who prefer traditional accounts. Firebase Authentication provides comprehensive email/password functionality including registration, login, password reset, and email verification.
User Registration with Email Verification
Registration requires creating a user account and optionally sending an email verification message to confirm the email address is valid. Email verification adds an important security layer by ensuring users can only access their account after confirming they own the email address:
'use client'
import { createUserWithEmailAndPassword, sendEmailVerification, updateProfile } from 'firebase/auth'
import { auth } from '../lib/firebaseConfig'
export default function RegistrationForm() {
const handleRegister = async (email: string, password: string, displayName: string) => {
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password)
if (displayName) {
await updateProfile(userCredential.user, { displayName })
}
await sendEmailVerification(userCredential.user)
console.log('Verification email sent!')
} catch (error) {
console.error('Registration error:', error)
}
}
}
Login and Password Reset
The login process uses signInWithEmailAndPassword to authenticate users with their stored credentials. Upon successful authentication, Firebase returns a user object and automatically creates a session:
'use client'
import { signInWithEmailAndPassword, sendPasswordResetEmail } from 'firebase/auth'
import { auth } from '../lib/firebaseConfig'
export const signIn = async (email: string, password: string) => {
await signInWithEmailAndPassword(auth, email, password)
}
export const resetPassword = async (email: string) => {
await sendPasswordResetEmail(auth, email)
}
For enhanced security, implement session timeout policies that require re-authentication after periods of inactivity. Firebase provides built-in protection against brute force attacks by limiting authentication attempt rates.
Password reset functionality allows users who have forgotten their password to securely reset it without contacting support. The process sends a password reset email with a link that expires after a configurable period for security.
Implementing robust authentication systems is a core part of our custom web application development, ensuring secure user experiences across all platforms.
Security Best Practices
Email Verification Requirements
Enforcing email verification before granting access to sensitive features prevents users from creating accounts with invalid or fake email addresses. Without verification, attackers could create accounts with any email address for spam, abuse, or impersonation.
Implement verification checks by examining the user's emailVerified property after authentication:
if (user && !user.emailVerified) {
router.push('/verify-email')
}
For sensitive operations like changing account settings or making purchases, require re-verification if the email hasn't been confirmed.
Rate Limiting and Abuse Prevention
Firebase provides built-in protection against brute force attacks by limiting authentication attempt rates. For additional protection, implement Firebase App Check to verify authentication requests come from legitimate sources.
App Check validates that requests to your Firebase services originate from your actual application, preventing abuse from unauthorized clients. This is particularly important for applications where authentication endpoints might be targeted by automated attacks.
Secure Environment Configuration
Use Firebase Admin SDK for server-side operations like verifying tokens and managing user accounts. Admin SDK credentials should never be exposed to the client:
# Server-side only (never expose to client)
FIREBASE_ADMIN_PROJECT_ID=your_project_id
FIREBASE_ADMIN_CLIENT_EMAIL=your_service_account_email
FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
Store admin credentials in secure environment variables without the NEXT_PUBLIC_ prefix. For production deployments, use secret management services like Google Secret Manager to keep credentials isolated between development, staging, and production environments.
Session Security
Implement session timeout policies that require re-authentication after periods of inactivity. This prevents unauthorized access if a user leaves their device unattended. Firebase doesn't provide built-in session timeout, so implement this logic in your application or use custom claims to track session state.
The security measures outlined here complement our comprehensive application security services that protect your digital assets against evolving threats.
Frequently Asked Questions
Moving Forward: Expanding Your Authentication System
With a solid Firebase Authentication foundation in place, you can extend your implementation to support additional features like custom claims for role-based access control, linking multiple authentication providers to a single account, and integrating with Firebase's other products for a complete user management solution.
Consider implementing user profile management that allows users to update their display name, profile photo, and other account information. Firestore integration enables storing additional user data beyond what Firebase Authentication provides, creating a complete user profile system. Custom claims allow you to implement role-based permissions that control access to different application features based on user roles.
The patterns established in this guide scale to production applications serving thousands or millions of users. Firebase Authentication handles the infrastructure scaling automatically, while your Next.js application continues to provide the performance and user experience that distinguishes modern web applications.
For teams building more complex applications, explore our guide on type-safe API communication with tRPC to complement your authentication system with end-to-end type safety. Additionally, understanding React best practices helps you write cleaner, more maintainable authentication components.
Sources
- Djamware - Next.js 14 Firebase Authentication Tutorial - Comprehensive step-by-step tutorial covering Google, GitHub, and Email/Password authentication with TypeScript
- Firebase - Integrate Firebase with Next.js Codelab - Google's official guide demonstrating Firebase App Hosting integration and authentication in production