Authentication is one of the most critical parts of modern web applications, but building it from scratch can be complex and time-consuming. That’s where Firebase Authentication comes in — it offers a secure, scalable, and developer-friendly solution that supports multiple sign-in methods, including Google, GitHub, and Email/Password.
In this tutorial, we’ll walk through building a Next.js 14 application with Firebase Authentication from the ground up. We’ll integrate Google and GitHub OAuth providers, implement Email/Password authentication, manage user sessions, and protect routes to ensure only authorized users can access certain pages.
By the end, you’ll have a fully functional authentication system that you can deploy to production using Vercel — ready to serve real users.
What You’ll Learn
-
How to set up a Next.js 14 project with the new App Router.
-
How to configure and integrate Firebase Authentication.
-
How to implement Google, GitHub, and Email/Password sign-in methods.
-
How to manage authentication state and protect pages.
-
How to deploy to Vercel with environment variables.
Setup
Prerequisites
Before we begin, make sure you have:
-
Node.js 20 or later installed.
-
A Firebase account (free tier works fine).
-
Google Cloud Console account to set up OAuth for Google.
-
GitHub Developer account to set up OAuth for GitHub.
-
Basic knowledge of React or Next.js.
1. Create a New Next.js 14 Project
Open your terminal and run:
npx create-next-app@latest nextjs-firebase-auth
When prompted:
-
TypeScript: Yes (recommended)
-
ESLint: Yes
-
App Router: Yes
-
Tailwind CSS: Yes (optional, for styling)
-
Import alias:
@/*
Once the installation is complete:
cd nextjs-firebase-auth
npm run dev
This will start your app locally at http://localhost:3000.
2. Install Firebase
Next, install Firebase SDK:
npm install firebase
Setting Up Firebase
We’ll now create a new Firebase project, enable authentication methods, and configure it for use in our Next.js app.
1. Create a Firebase Project
-
Go to Firebase Console.
-
Click Add Project (or Create Project).
-
Enter your project name (e.g.,
nextjs-firebase-auth
). -
Disable Google Analytics for now (optional).
-
Click Create Project.
2. Enable Authentication Providers
Inside your new Firebase project:
Enable Google Sign-In
-
Navigate to Build → Authentication → Sign-in method.
-
Click Add new provider → Google.
-
Enable it and click Save.
Enable GitHub Sign-In
-
In the same Sign-in method tab, click Add new provider → GitHub.
-
You’ll be asked for:
-
Client ID
-
Client Secret
-
-
To get these, go to GitHub Developer Settings → OAuth Apps and:
-
Click New OAuth App.
-
Set Homepage URL to
http://localhost:3000
. -
Set Authorization callback URL to
http://localhost:3000
. -
Click Register Application.
-
Copy the Client ID and Client Secret to Firebase.
-
-
Click Save in Firebase.
Enable Email/Password Sign-In
-
Still under Sign-in method, click Add new provider → Email/Password.
-
Enable it and click Save.
3. Register Your Web App in Firebase
-
In Firebase Console, go to Project Settings (gear icon) → General.
-
Scroll to Your apps and click the Web icon (
</>
). -
Enter an app name (e.g.,
nextjs-auth-app
). -
Click Register App.
-
Firebase will now show you your Firebase SDK configuration — keep this tab open, as we’ll use it in our Next.js project.
4. Add Firebase Config to Next.js
Create a .env.local
file in your project root and add:
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
Replace all values with those from the Firebase config snippet you got in the previous step.
5. Create Firebase Initialization File
In lib/firebaseConfig.js
(or firebaseConfig.ts
if using TypeScript), add:
// lib/firebaseConfig.js
import { initializeApp } 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,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
Creating Auth Context
To make authentication available across the entire Next.js app, we’ll use React Context. This will allow us to store the authenticated user’s data, listen to authentication changes, and make it easy to access that information from any component.
1. Create the Auth Context File
Create a new folder context
in your project root, then a file AuthContext.tsx
inside it:
"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: () => 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;
};
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// Listen for authentication state changes
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser || null);
setLoading(false);
});
return () => unsubscribe();
}, []);
const signOut = () => {
firebaseSignOut(auth);
};
return (
<AuthContext.Provider value= {{ user, loading, signOut }
}>
{ children }
</AuthContext.Provider>
);
};
2. Wrap the App with AuthProvider
In Next.js 14 with the App Router, we’ll wrap our app in the AuthProvider
inside app/layout.js
(or layout.tsx
):
import { AuthProvider } from "./context/AuthContext";
import "./globals.css";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Next.js Firebase Auth",
description: "Google, GitHub, and Email Authentication"
};
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}
3. Using Auth State in a Component
Example app/dashboard/page.tsx
:
"use client";
import { useAuth } from "../context/AuthContext";
export default function DashboardPage() {
const { user, signOut } = useAuth();
if (!user) return <p>Please log in</p>;
return (
<div>
<h1>Welcome, {user.displayName || user.email}</h1>
<button onClick={signOut}>Sign Out</button>
</div>
);
}
With TypeScript types in place, our context is safer — TypeScript will now warn us if we misuse the useAuth()
hook or forget to wrap components with AuthProvider
.
Implementing Google & GitHub Sign-In (TypeScript)
We’ll use Firebase Auth’s signInWithPopup()
for both Google and GitHub providers.
"use client";
import {
GoogleAuthProvider,
GithubAuthProvider,
signInWithPopup
} from "firebase/auth";
import { auth } from "../lib/firebaseConfig";
export default function SignInButtons() {
const googleProvider = new GoogleAuthProvider();
const githubProvider = new GithubAuthProvider();
const signInWithGoogle = async () => {
try {
await signInWithPopup(auth, googleProvider);
} catch (error) {
console.error("Google sign-in error:", error);
}
};
const signInWithGitHub = async () => {
try {
await signInWithPopup(auth, githubProvider);
} catch (error) {
console.error("GitHub sign-in error:", error);
}
};
return (
<div className="space-y-4">
<button
onClick={signInWithGoogle}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Sign in with Google
</button>
<button
onClick={signInWithGitHub}
className="px-4 py-2 bg-gray-800 text-white rounded hover:bg-gray-900"
>
Sign in with GitHub
</button>
</div>
);
}
2. Create a Login Page
In app/login/page.tsx
:
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useAuth } from "../context/AuthContext";
import SignInButtons from "../components/SignInButtons";
export default function LoginPage() {
const { user } = useAuth();
const router = useRouter();
useEffect(() => {
if (user) {
router.push("/dashboard");
}
}, [user, router]);
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="mb-6 text-2xl font-bold">Login</h1>
<SignInButtons />
</div>
);
}
3. Protecting the Dashboard Page
We’ll make sure only logged-in users can access /dashboard
.
app/dashboard/page.tsx
:
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useAuth } from "../context/AuthContext";
export default function DashboardPage() {
const { user, signOut, 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 className="mb-4 text-xl font-bold">
Welcome, {user.displayName || user.email}
</h1>
<button
onClick={signOut}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Sign Out
</button>
</div>
);
}
At this point:
-
Google Sign-In ✅
-
GitHub Sign-In ✅
-
Route Protection ✅
Email/Password Authentication (TypeScript)
Great — below is a compact, production-friendly TypeScript implementation for Email/Password auth. It includes:
-
lib/firebaseConfig.ts
(TypeScript) -
Auth context additions for signUp / signIn / resetPassword
-
A reusable
EmailAuthForm
component that supports Register, Login, and Reset Password -
Notes on email verification and security
lib/firebaseConfig.ts
// lib/firebaseConfig.ts
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);
Update context/AuthContext.tsx
— add email methods
Add signUpWithEmail
, signInWithEmail
, and resetPassword
to the context so any component can call them.
"use client";
import {
createContext,
useContext,
useEffect,
useState,
ReactNode
} from "react";
import {
User,
onAuthStateChanged,
signOut as firebaseSignOut,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
sendPasswordResetEmail,
sendEmailVerification,
updateProfile
} from "firebase/auth";
import { auth } from "../lib/firebaseConfig";
interface AuthContextType {
user: User | null;
loading: boolean;
signOut: () => Promise<void>;
signUpWithEmail: (
email: string,
password: string,
displayName?: string
) => Promise<void>;
signInWithEmail: (email: string, password: string) => Promise<void>;
resetPassword: (email: string) => 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;
};
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
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);
};
const signUpWithEmail = async (
email: string,
password: string,
displayName?: string
) => {
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
if (displayName) {
await updateProfile(userCredential.user, { displayName });
}
// Optionally send email verification
await sendEmailVerification(userCredential.user);
// onAuthStateChanged will update `user`
};
const signInWithEmail = async (email: string, password: string) => {
await signInWithEmailAndPassword(auth, email, password);
};
const resetPassword = async (email: string) => {
await sendPasswordResetEmail(auth, email);
};
return (
<AuthContext.Provider
value={{
user,
loading,
signOut,
signUpWithEmail,
signInWithEmail,
resetPassword
}}
>
{children}
</AuthContext.Provider>
);
};
Notes:
-
We call
sendEmailVerification
after registration — good practice for verifying users. You can skip or conditionally require this. -
onAuthStateChanged
will pick up changes so the UI updates automatically.
components/EmailAuthForm.tsx
A single component that toggles between Register, Login, and Reset Password modes.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "../context/AuthContext";
type Mode = "login" | "register" | "reset";
export default function EmailAuthForm() {
const { signInWithEmail, signUpWithEmail, resetPassword } = useAuth();
const router = useRouter();
const [mode, setMode] = useState<Mode>("login");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [displayName, setDisplayName] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const clearState = () => {
setError(null);
setMessage(null);
setLoading(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setMessage(null);
setLoading(true);
try {
if (mode === "login") {
if (!email || !password)
throw new Error("Email and password are required");
await signInWithEmail(email, password);
router.push("/dashboard");
} else if (mode === "register") {
if (!email || !password)
throw new Error("Email and password are required");
await signUpWithEmail(email, password, displayName || undefined);
setMessage(
"Registration successful — verification email sent (check your inbox)."
);
// Optionally redirect after registration: router.push("/dashboard");
} else if (mode === "reset") {
if (!email) throw new Error("Please provide your email");
await resetPassword(email);
setMessage("Password reset email sent (check your inbox).");
}
} catch (err: any) {
setError(err.message || "An error occurred");
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded shadow">
<h2 className="text-2xl font-semibold mb-4">
{mode === "login"
? "Sign in"
: mode === "register"
? "Create account"
: "Reset password"}
</h2>
{error && <div className="mb-4 text-red-600">{error}</div>}
{message && <div className="mb-4 text-green-700">{message}</div>}
<form onSubmit={handleSubmit} className="space-y-4">
{mode === "register" && (
<div>
<label className="block text-sm">Display name (optional)</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full mt-1 p-2 border rounded"
placeholder="Your name"
/>
</div>
)}
<div>
<label className="block text-sm">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full mt-1 p-2 border rounded"
placeholder="[email protected]"
required
/>
</div>
{mode !== "reset" && (
<div>
<label className="block text-sm">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full mt-1 p-2 border rounded"
placeholder="••••••••"
required={mode === "login" || mode === "register"}
minLength={6}
/>
</div>
)}
<div className="flex items-center justify-between">
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-60"
>
{loading
? "Please wait..."
: mode === "login"
? "Sign in"
: mode === "register"
? "Register"
: "Send reset email"}
</button>
<div className="text-sm">
{mode === "login" && (
<>
<button
type="button"
onClick={() => {
clearState();
setMode("reset");
}}
className="underline"
>
Forgot?
</button>
<span className="mx-2">|</span>
<button
type="button"
onClick={() => {
clearState();
setMode("register");
}}
className="underline"
>
Create account
</button>
</>
)}
{mode === "register" && (
<button
type="button"
onClick={() => {
clearState();
setMode("login");
}}
className="underline"
>
Back to login
</button>
)}
{mode === "reset" && (
<button
type="button"
onClick={() => {
clearState();
setMode("login");
}}
className="underline"
>
Back to login
</button>
)}
</div>
</div>
</form>
</div>
);
}
Email Verification & Security Recommendations
-
Email verification: We call
sendEmailVerification
after registering. Consider blocking access untiluser.emailVerified === true
for sensitive pages, or show a banner on first login to remind users to verify. -
Password rules: Enforce a minimum password length (Firebase requires 6+ chars). Consider stronger rules on the client or server if needed.
-
Secure env vars: Never expose admin keys in the client — the
NEXT_PUBLIC_
prefixed values are fine for Firebase client usage. For server-side admin tasks (if any), use Firebase Admin SDK on server functions with secure env variables (notNEXT_PUBLIC_
). -
Rate-limits & brute force: Consider adding reCAPTCHA on signup/login if you expect abuse, or use Firebase’s blocking functions to enforce rules.
Conclusion
In this tutorial, we’ve successfully integrated Firebase Authentication into a Next.js 14 + TypeScript project with support for Google, GitHub, and Email/Password sign-in methods.
We started by setting up Firebase and connecting it to our Next.js app, then built a reusable Auth Context to manage authentication state across the application. From there, we implemented both OAuth providers (Google, GitHub) and a full email/password flow — including user registration, login, password reset, and optional email verification.
By following this pattern, you now have:
-
A scalable, context-based authentication system
-
Centralized auth methods for easy maintenance
-
Reusable UI components for different login flows
-
A security-conscious setup that can be extended with verification, custom claims, or role-based access
This foundation can be expanded further by integrating:
-
Firestore or Realtime Database for user profiles
-
Protected routes and role-based authorization
-
Custom UI styling with Tailwind CSS or your preferred library
-
Server-side auth checks for improved security
With Firebase handling the heavy lifting, you can focus on building product features while still offering a smooth and secure authentication experience. 🚀
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about the Next.js frameworks, you can take the following cheap course:
-
Build & Deploy Multi Ecommerce Website React Next. js 2025
-
A Guide to Next. js (new version 14)
-
Next. js for Beginners to Advanced (2025)
-
Next. js and Appwrite Masterclass - Build fullstack projects
-
Next. js With Tailwind CSS-Build a Frontend Ecommerce Project
Thanks!