Full-Stack Authentication
End-to-End Auth Implementation
Explanation
Full-Stack Authentication
Authentication spans frontend and backend. A complete solution includes registration, login, session management, and protected routes on both sides.
Components
| Layer | Responsibility | |-------|----------------| | Frontend | Forms, token storage, route guards | | Backend | Validation, token generation, middleware | | Database | User storage, sessions |
Demonstration
Example 1: Backend Authentication (Node.js)
// auth.controller.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('./user.model');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
// Register
exports.register = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
error: 'Email already in use'
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const user = await User.create({
name,
email,
password: hashedPassword
});
// Generate token
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
// Set HTTP-only cookie
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.status(201).json({
user: {
id: user._id,
name: user.name,
email: user.email
}
});
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
};
// Login
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({
user: {
id: user._id,
name: user.name,
email: user.email
}
});
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
};
// Logout
exports.logout = (req, res) => {
res.clearCookie('token');
res.json({ message: 'Logged out' });
};
// Get current user
exports.me = async (req, res) => {
const user = await User.findById(req.userId);
res.json({
user: {
id: user._id,
name: user.name,
email: user.email
}
});
};
Example 2: Auth Middleware
// auth.middleware.js
const jwt = require('jsonwebtoken');
exports.authenticate = async (req, res, next) => {
try {
// Get token from cookie or header
const token = req.cookies.token ||
req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
res.status(401).json({ error: 'Invalid token' });
}
};
// Optional auth (for routes that work with or without auth)
exports.optionalAuth = async (req, res, next) => {
try {
const token = req.cookies.token ||
req.headers.authorization?.replace('Bearer ', '');
if (token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
}
} catch {
// Ignore errors for optional auth
}
next();
};
// Role-based authorization
exports.authorize = (...roles) => {
return async (req, res, next) => {
const user = await User.findById(req.userId);
if (!roles.includes(user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};
// Routes
const router = require('express').Router();
const auth = require('./auth.controller');
const { authenticate, authorize } = require('./auth.middleware');
router.post('/register', auth.register);
router.post('/login', auth.login);
router.post('/logout', auth.logout);
router.get('/me', authenticate, auth.me);
// Protected admin route
router.get('/admin/users', authenticate, authorize('admin'), getUsers);
Example 3: React Authentication Context
// AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Check auth on mount
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/me', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setUser(data.user);
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
const login = async (email, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Login failed');
}
const data = await response.json();
setUser(data.user);
return data.user;
};
const register = async (name, email, password) => {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ name, email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Registration failed');
}
const data = await response.json();
setUser(data.user);
return data.user;
};
const logout = async () => {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
setUser(null);
};
const value = {
user,
loading,
isAuthenticated: !!user,
login,
register,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Example 4: Protected Routes (React)
// ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
export function ProtectedRoute({ children, roles }) {
const { user, loading, isAuthenticated } = useAuth();
const location = useLocation();
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (roles && !roles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './AuthContext';
import { ProtectedRoute } from './ProtectedRoute';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
{/* Admin only */}
<Route path="/admin" element={
<ProtectedRoute roles={['admin']}>
<AdminPanel />
</ProtectedRoute>
} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
// Login.jsx
function Login() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [error, setError] = useState('');
const from = location.state?.from?.pathname || '/dashboard';
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
const formData = new FormData(e.target);
const email = formData.get('email');
const password = formData.get('password');
try {
await login(email, password);
navigate(from, { replace: true });
} catch (err) {
setError(err.message);
}
};
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
);
}
Example 5: Refresh Tokens
// Backend - Token refresh
exports.refreshToken = async (req, res) => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
// Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
// Check if token is in database (for revocation)
const storedToken = await RefreshToken.findOne({
token: refreshToken,
userId: decoded.userId
});
if (!storedToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: 'Token refresh failed' });
}
};
// Frontend - Auto refresh
class AuthService {
constructor() {
this.accessToken = null;
this.refreshPromise = null;
}
async fetch(url, options = {}) {
const response = await this.fetchWithToken(url, options);
if (response.status === 401) {
// Try to refresh
await this.refreshAccessToken();
return this.fetchWithToken(url, options);
}
return response;
}
async fetchWithToken(url, options) {
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
},
credentials: 'include'
});
}
async refreshAccessToken() {
// Prevent multiple simultaneous refreshes
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
})
.then(response => {
if (!response.ok) throw new Error('Refresh failed');
return response.json();
})
.then(data => {
this.accessToken = data.accessToken;
})
.finally(() => {
this.refreshPromise = null;
});
return this.refreshPromise;
}
}
Example 6: OAuth Integration
// Backend - OAuth routes
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
avatar: profile.photos[0].value
});
}
return done(null, user);
} catch (error) {
return done(error, null);
}
}));
// Routes
router.get('/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
router.get('/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const token = jwt.sign({ userId: req.user._id }, JWT_SECRET, {
expiresIn: '7d'
});
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
res.redirect('/dashboard');
}
);
// Frontend
function SocialLogin() {
return (
<div>
<a href="/api/auth/google" className="btn-google">
Continue with Google
</a>
<a href="/api/auth/github" className="btn-github">
Continue with GitHub
</a>
</div>
);
}
Key Takeaways:
- Use HTTP-only cookies for tokens
- Implement refresh token rotation
- Create auth context for React
- Protect routes on both sides
- Support OAuth providers
Imitation
Challenge 1: Complete Auth System
Task: Build authentication with email verification.
Solution
// Backend
exports.register = async (req, res) => {
const user = await User.create({
...req.body,
emailVerified: false,
emailVerifyToken: crypto.randomBytes(32).toString('hex')
});
await sendVerificationEmail(user.email, user.emailVerifyToken);
res.status(201).json({ message: 'Verification email sent' });
};
exports.verifyEmail = async (req, res) => {
const { token } = req.params;
const user = await User.findOneAndUpdate(
{ emailVerifyToken: token },
{ emailVerified: true, emailVerifyToken: null },
{ new: true }
);
if (!user) {
return res.status(400).json({ error: 'Invalid token' });
}
const jwtToken = generateToken(user._id);
res.cookie('token', jwtToken, cookieOptions);
res.json({ user: sanitizeUser(user) });
};
// Middleware to require verified email
exports.requireVerified = async (req, res, next) => {
const user = await User.findById(req.userId);
if (!user.emailVerified) {
return res.status(403).json({ error: 'Email not verified' });
}
next();
};
Practice
Exercise 1: Password Reset
Difficulty: Intermediate
Implement forgot/reset password flow.
Exercise 2: Two-Factor Auth
Difficulty: Advanced
Add TOTP-based 2FA to the auth system.
Summary
What you learned:
- Backend auth implementation
- JWT and refresh tokens
- React auth context
- Protected routes
- OAuth integration
Next Steps:
- Read: Security
- Practice: Add 2FA
- Explore: Passkeys, WebAuthn
