Core Concepts: Authentication
Understanding how to keep users safe and signed in
Before we build our login and register pages, let's understand the key concepts behind authentication. Authentication isn't magic — it's a series of patterns and tools working together to answer one question: "Are you really who you say you are?"
What is Authentication?
Authentication is the process of verifying that a user is who they claim to be. It's like checking an ID at a restaurant — they say they're 21, so you ask for their license to verify.
Authorization is different: it's about what an authenticated user is allowed to do. A waiter can enter the kitchen, but a customer can't. Both are authenticated (the restaurant knows who they are), but their authorization levels differ.
In web apps, authentication typically works with passwords or third-party services: you prove who you are by providing credentials that only you know. Once authenticated, the app remembers you across different pages and sessions. Without authentication, every time you navigated to a new page, the app would ask "who are you?" all over again.
Apps need authentication because they store user data (emails, preferences, content), and you only want the right person accessing their own data. Imagine a banking app where anyone could see anyone else's account balance — that would be a disaster.
What is Firebase Authentication?
Firebase Authentication is a backend service from Google that handles user sign-ups, logins, and password resets. Instead of building your own user database and password-hashing system (which is risky!), you use Firebase to manage all that complexity.
Firebase provides several authentication methods:
- Email & Password — Users sign up with an email and password
- Google Sign-In — "Sign in with Google" button
- GitHub Sign-In — "Sign in with GitHub" button
- And more (Apple, Facebook, anonymous, etc.)
When a user logs in, Firebase verifies their credentials, generates a token (a secure string that proves they're authenticated), and sends it back to your app. Your app then uses that token for all future requests: "Here's my token, let me see my data."
We use Firebase because it handles the hard parts: securely storing passwords (hashing and salting), preventing brute-force attacks, and managing sessions. You don't have to be a security expert to build a safe app.
What is the Context API?
The Context API is React's built-in system for sharing state across many components without "prop drilling" (passing props through every component in between).
Imagine you have auth state (is the user logged in? what's their name?) that 15 different components need to know about. Without Context, you'd pass it as props through every parent component, even ones that don't care about it. That's messy.
With Context, you create an "Auth Context" at the top of your app. Any component can ask "what's the current auth state?" without passing props through every parent. Context solves this with two concepts:
Provider — A component that wraps your app and provides state. Like a store that hands out information.
Consumer/Hook — Components that request data from the Provider using the useContext hook. Like customers asking the store for information.
Context is perfect for authentication because:
- Auth state is needed in many places (navigation, protected routes, user profile)
- The auth state rarely changes (except on login/logout)
- It avoids passing
isLoggedInanduserthrough every component
What are React Hooks?
React Hooks are special functions that let components manage state and side effects. They're called "hooks" because they "hook into" React features.
The three hooks you need for authentication:
useState — Manage Component State
useState lets a component remember things between renders.
// Without useState: the count would always be 0
function Counter() {
let count = 0;
return <button onClick={() => count++}>{count}</button>;
}
// With useState: React remembers the count
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}For authentication, you'd use useState for form inputs:
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');useEffect — Run Side Effects
useEffect runs code when a component mounts (loads) or when dependencies change. Side effects are anything "outside" React: API calls, timers, subscriptions.
// Run once when component mounts
useEffect(() => {
console.log('Component mounted!');
}, []);
// Run when 'userId' changes
useEffect(() => {
fetchUserData(userId);
}, [userId]);For authentication, you'd use useEffect to check if the user is already logged in:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
});
return unsubscribe; // Cleanup
}, []);useContext — Access Shared State
useContext lets a component access data from a Context Provider.
// Create context
const AuthContext = createContext();
// In a component far down the tree
const { user, isLoggedIn } = useContext(AuthContext);This avoids prop drilling: you don't need to pass user through 5 parent components.
What are Controlled Components?
Controlled components are form inputs whose values are controlled by React state. The input's value comes from state, and the state updates when the user types.
// Uncontrolled: input manages its own value
<input type="email" />
// Controlled: React manages the value
const [email, setEmail] = useState('');
return <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />;Controlled components are the React way because:
- React knows the value — You can validate or manipulate it instantly
- Easy to reset — Just
setEmail('')to clear the form - Easy to pre-fill — Set initial state to existing data
- Real-time validation — Show errors as users type
For a login form, controlled inputs let you enable/disable the submit button based on whether the email and password are valid:
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const isValid = email.includes('@') && password.length >= 6;
return (
<form>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button disabled={!isValid}>Login</button>
</form>
);
}What is Form Validation?
Form validation is checking that user input is correct before submitting it. There are two kinds:
Client-side validation (in the browser) gives instant feedback to the user: "Your email needs an @". It's fast and happens before any data leaves the browser.
// Client-side: quick feedback
const isValidEmail = email.includes('@') && email.includes('.');
if (!isValidEmail) {
setError('Please enter a valid email');
return;
}Server-side validation (on Firebase/backend) double-checks that data is safe before storing it. This is crucial because a malicious user could bypass client-side checks.
Think of it like a bouncer at a club:
- Client-side = The bouncer at the door asking "Do you look 21?" (quick, prevents most issues)
- Server-side = Checking the ID under a blacklight (thorough, the real verification)
You always need both because:
- Client-side makes the app feel responsive and user-friendly
- Server-side prevents malicious data from being stored
For authentication, validation might check:
- Email is properly formatted:
user@example.com - Password is strong enough: 8+ characters, uppercase, numbers
- Required fields aren't empty
- Passwords match when registering
What are Protected Routes?
Protected routes are pages that only authenticated users can access. If someone tries to visit /dashboard without logging in, they get redirected to /login.
// Regular route: anyone can access
<Route path="/login" element={<LoginPage />} />
// Protected route: only authenticated users
<Route path="/dashboard" element={
currentUser ? <Dashboard /> : <Navigate to="/login" />
} />Protected routes answer the question: "Should this user see this page?"
The pattern is simple:
- Check if
currentUserexists (they're logged in) - If yes: render the protected component
- If no: redirect to login
You can also create a <ProtectedRoute> component that wraps this logic:
function ProtectedRoute({ children, currentUser }) {
return currentUser ? children : <Navigate to="/login" />;
}
// Usage
<Route path="/dashboard" element={
<ProtectedRoute currentUser={currentUser}>
<Dashboard />
</ProtectedRoute>
} />How It All Fits Together
Here's how authentication works in our app:
- User visits the app → App checks Firebase: "Are we logged in?"
- Firebase responds → "Yes, here's the user" or "No one's logged in"
- State updates → App's
AuthContextstores the user - Components access state → Navigation shows "Hello, John!" if logged in, or "Sign Up" button if not
- User clicks login → Form submits to Firebase
- Firebase authenticates → Verifies email/password and sends back a token
- Token stored → Firebase SDK keeps it automatically (securely)
- State updates → AuthContext now has the user, components re-render
- Protected routes work → Dashboard is now accessible because
currentUserexists
All of this happens without the user thinking about it. They just sign in and see their dashboard.
Component Structure
// App.tsx - Root component
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
// ProtectedRoute.tsx
import { useAuth } from './context/AuthContext';
import { Navigate } from 'react-router-dom';
function ProtectedRoute({ children }) {
const { currentUser } = useAuth();
return currentUser ? children : <Navigate to="/login" />;
}
// LoginPage.tsx
import { useAuth } from './context/AuthContext';
import { useState } from 'react';
function LoginPage() {
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
await login(email, password);
}
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
/>
<button type="submit">Log In</button>
</form>
);
}
// context/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from 'react';
import { auth } from '../firebase-config';
import { signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut } from 'firebase/auth';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const login = (email, password) => {
return signInWithEmailAndPassword(auth, email, password);
};
const register = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
};
const logout = () => {
return signOut(auth);
};
return (
<AuthContext.Provider value={{ currentUser, login, register, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}Visual Diagrams
Authentication Flow
┌─────────────────────────────────────────────────────────────┐
│ User visits app for first time │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ Check localStorage │
│ for Firebase token │
└──────────┬───────────┘
│
┌───────┴────────┐
│ │
Token exists No token
│ │
▼ ▼
┌────────────┐ ┌──────────────┐
│ Restore │ │ Show Login │
│ user state │ │ /Register │
│ (logged in)│ │ (logged out) │
└────────────┘ └──────────────┘
│ │
└────────┬───────┘
│
▼
┌──────────────────┐
│ User navigates │
│ to pages │
└──────────────────┘
│
┌────────┴─────────┐
│ │
Public route Protected route
│ │
▼ ▼
Show login page currentUser?
(no auth needed) │
┌────┴────┐
│ │
YES NO
│ │
▼ ▼
Show page Redirect to
(protected) /loginContext API Pattern
┌─────────────────────────────────────────────────────────┐
│ App Component │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ <AuthProvider> (wraps entire app) │ │
│ │ │ │
│ │ Manages: │ │
│ │ - currentUser state │ │
│ │ - login() function │ │
│ │ - logout() function │ │
│ │ - register() function │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ <BrowserRouter> │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │
│ │ │ │ LoginPage │ │ Dashboard │ ... │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ useAuth() ◄──┼──┼──► useAuth() │ │ │ │
│ │ │ │ gets user │ │ gets user │ │ │ │
│ │ │ └──────────────┘ └──────────────┘ │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Any component can do: const { currentUser } = useAuth()
without passing props through every parent componentProtected Route Decision Tree
User tries to access /dashboard
│
▼
Is there a currentUser?
(from AuthContext)
│
┌────┴────┐
│ │
YES NO
│ │
▼ ▼
Render Redirect to
Dashboard /login page
component (use Navigate)
│ │
└────┬─────┘
│
User can see
dashboard OR
prompted to loginForm to Firebase Flow
User fills in login form
│
▼
Clicks "Log In" button
│
▼
onSubmit handler
(validate form)
│
┌────┴────┐
│ │
Invalid Valid
│ │
▼ ▼
Show error Call login()
message (from AuthContext)
│
▼
Send email +
password to
Firebase
│
┌────┴────┐
│ │
Success Failed
│ │
▼ ▼
Firebase Show
generates error
token message
│
▼
Token saved
(automatically)
│
▼
currentUser
updates
│
▼
Navigation re-renders
(shows user's name)
OR
ProtectedRoute
now allows accessWhy These Patterns Matter
Authentication is the foundation of personalized web apps. Without it, you'd need usernames in the URL like /dashboard?user=john — anyone could change it to see someone else's data. With proper authentication, you have trust:
- Users trust you — Their passwords are secure, their data is private
- You trust users — You know who's accessing what
- Your app is useful — Each user sees their own data, not everyone's
React's Context API + Firebase makes authentication approachable. You're not writing password-hashing algorithms or session management from scratch. You're using industry-standard tools that have been battle-tested by millions of apps.
What You'll Build
In this slice, you'll create:
- ✅ An
AuthContextthat manages login state across the app - ✅ A
RegisterPagewith form validation - ✅ A
LoginPagewith email/password inputs - ✅ A
ProtectedRoutecomponent that guards the dashboard - ✅ Logout functionality in the navigation
- ✅ Error handling and success messages
- ✅ Auto-restore login state when the app refreshes
By the end, you'll have a complete authentication system. Users can sign up, log in, stay logged in across page refreshes, and access protected pages only when authenticated.
Next Steps
Now that you understand the concepts, let's start building: