What is Authentication?
Authentication is the process of verifying who a user is. It's how your application knows that the person accessing it is actually who they claim to be.
Authentication vs Authorization:
AUTHENTICATION (AuthN) AUTHORIZATION (AuthZ)
─────────────────────────────────────────────────────────
"Who are you?" "What can you do?"
Verifies identity Checks permissions
Login/password Roles and permissions
Proves you are you Grants/denies access
Example: Example:
Logging into your Admin can delete users,
bank account Regular user cannot
Both are needed for secure applications!
Common Authentication Methods:
1. SESSION-BASED (Traditional)
Client ────login────► Server
◄───cookie────
Client ────cookie───► Server (validates session)
2. TOKEN-BASED (JWT)
Client ────login────► Server
◄───token────
Client ────token────► Server (validates token)
3. OAUTH (Third-party)
Client ──►Google──► Your Server
◄─token─┘
Password Hashing with bcrypt
Never store passwords in plain text. Always hash them with a strong algorithm like bcrypt.
# Install bcrypt
npm install bcrypt
// Password hashing
import bcrypt from 'bcrypt';
// Hash a password (during registration)
async function hashPassword(plainPassword) {
const saltRounds = 10; // Cost factor (higher = slower but more secure)
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
}
// Verify a password (during login)
async function verifyPassword(plainPassword, hashedPassword) {
const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
return isMatch;
}
// Usage in registration
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await User.create({
email,
password: hashedPassword
});
res.status(201).json({ message: 'User registered successfully' });
});
// Usage in login
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Password correct - generate token or session
res.json({ message: 'Login successful' });
});
// Why bcrypt?
// ✓ Salted - each password gets unique salt
// ✓ Slow by design - prevents brute force
// ✓ Adjustable cost - can increase as hardware improves
// ✗ Never use: MD5, SHA1, plain text!
JSON Web Tokens (JWT)
JWT is a compact, self-contained way to securely transmit information between parties as a JSON object.
JWT Structure:
─────────────────────────────────────────────────────────
header.payload.signature
HEADER (Algorithm & Token Type)
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD (Claims/Data)
{
"sub": "user123", // Subject (user ID)
"name": "John Doe",
"role": "admin",
"iat": 1516239022, // Issued at
"exp": 1516242622 // Expiration
}
SIGNATURE
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Result:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Install jsonwebtoken
npm install jsonwebtoken
// JWT implementation
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '1h';
// Generate token
function generateToken(user) {
const payload = {
id: user._id,
email: user.email,
role: user.role
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
}
// Verify token
function verifyToken(token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
return { valid: true, decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}
// Login endpoint
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Find and verify user
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = generateToken(user);
res.json({
message: 'Login successful',
token,
user: {
id: user._id,
email: user.email,
name: user.name
}
});
});
// Auth middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
const { valid, decoded, error } = verifyToken(token);
if (!valid) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
}
// Protected route
app.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
Access & Refresh Tokens
Use short-lived access tokens with refresh tokens for better security.
Token Strategy:
─────────────────────────────────────────────────────────
ACCESS TOKEN REFRESH TOKEN
Short-lived (15 min) Long-lived (7 days)
Stored in memory Stored in httpOnly cookie
Used for API requests Used to get new access token
Stateless Stored in database
Flow:
1. Login → Get both tokens
2. Use access token for requests
3. Access token expires → Use refresh token to get new one
4. Refresh token expires → User must login again
// Token generation
const ACCESS_TOKEN_EXPIRES = '15m';
const REFRESH_TOKEN_EXPIRES = '7d';
function generateTokens(user) {
const accessToken = jwt.sign(
{ id: user._id, email: user.email },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRES }
);
const refreshToken = jwt.sign(
{ id: user._id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES }
);
return { accessToken, refreshToken };
}
// Login endpoint
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const { accessToken, refreshToken } = generateTokens(user);
// Store refresh token in database
await RefreshToken.create({
token: refreshToken,
userId: user._id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// Send refresh token in httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({ accessToken });
});
// Refresh endpoint
app.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
// Verify refresh token exists in database
const storedToken = await RefreshToken.findOne({ token: refreshToken });
if (!storedToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Verify token is valid
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
const user = await User.findById(decoded.id);
// Generate new access token
const accessToken = jwt.sign(
{ id: user._id, email: user.email },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRES }
);
res.json({ accessToken });
} catch (error) {
// Delete invalid refresh token
await RefreshToken.deleteOne({ token: refreshToken });
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Logout endpoint
app.post('/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
// Delete refresh token from database
await RefreshToken.deleteOne({ token: refreshToken });
// Clear cookie
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
Passport.js Basics
Passport is authentication middleware for Node.js. It supports 500+ authentication strategies including local, OAuth, and more.
# Install passport
npm install passport passport-local passport-jwt
// passport/config.js
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import User from '../models/User.js';
import bcrypt from 'bcrypt';
// Local Strategy (username/password)
passport.use(new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return done(null, false, { message: 'Invalid password' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// JWT Strategy
passport.use(new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
},
async (payload, done) => {
try {
const user = await User.findById(payload.id);
if (!user) {
return done(null, false);
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}
));
export default passport;
// app.js
import express from 'express';
import passport from './passport/config.js';
const app = express();
app.use(express.json());
app.use(passport.initialize());
// Login with Local Strategy
app.post('/login',
passport.authenticate('local', { session: false }),
(req, res) => {
const token = generateToken(req.user);
res.json({ token, user: req.user });
}
);
// Protected route with JWT Strategy
app.get('/profile',
passport.authenticate('jwt', { session: false }),
(req, res) => {
res.json({ user: req.user });
}
);
// Error handling for authentication
app.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({ error: info.message });
}
req.user = user;
next();
})(req, res, next);
}, (req, res) => {
const token = generateToken(req.user);
res.json({ token });
});
OAuth with Passport (Google)
Allow users to login with their Google account.
# Install Google OAuth strategy
npm install passport-google-oauth20
// passport/google.js
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import passport from 'passport';
import User from '../models/User.js';
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
try {
// Check if user exists
let user = await User.findOne({ googleId: profile.id });
if (user) {
return done(null, user);
}
// Check if email is already registered
user = await User.findOne({ email: profile.emails[0].value });
if (user) {
// Link Google account to existing user
user.googleId = profile.id;
await user.save();
return done(null, user);
}
// Create new user
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0]?.value
});
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// Routes
// Redirect to Google
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
// Google callback
app.get('/auth/google/callback',
passport.authenticate('google', {
session: false,
failureRedirect: '/login?error=google_failed'
}),
(req, res) => {
// Generate JWT token
const token = generateToken(req.user);
// Redirect to frontend with token
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
}
);
// Frontend handling (React)
// pages/auth/callback.jsx
function AuthCallback() {
const router = useRouter();
const searchParams = useSearchParams();
useEffect(() => {
const token = searchParams.get('token');
if (token) {
localStorage.setItem('token', token);
router.push('/dashboard');
} else {
router.push('/login?error=auth_failed');
}
}, []);
return <div>Completing login...</div>;
}
Role-Based Access Control (RBAC)
// User model with roles
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: {
type: String,
enum: ['user', 'editor', 'admin'],
default: 'user'
}
});
// Role-based middleware
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: 'Not authorized' });
}
next();
};
}
// Usage
app.get('/users',
passport.authenticate('jwt', { session: false }),
authorize('admin'),
async (req, res) => {
const users = await User.find();
res.json(users);
}
);
app.put('/posts/:id',
passport.authenticate('jwt', { session: false }),
authorize('editor', 'admin'),
async (req, res) => {
const post = await Post.findByIdAndUpdate(req.params.id, req.body);
res.json(post);
}
);
// Permission-based access (more granular)
const permissions = {
admin: ['create', 'read', 'update', 'delete', 'manage_users'],
editor: ['create', 'read', 'update'],
user: ['read']
};
function hasPermission(permission) {
return (req, res, next) => {
const userPermissions = permissions[req.user.role] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Permission denied' });
}
next();
};
}
app.delete('/posts/:id',
authenticate,
hasPermission('delete'),
async (req, res) => {
await Post.findByIdAndDelete(req.params.id);
res.json({ message: 'Deleted' });
}
);
Frontend Authentication (React)
// context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '../services/api';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
// Check for existing token on mount
const token = localStorage.getItem('accessToken');
if (token) {
fetchUser();
} else {
setLoading(false);
}
}, []);
async function fetchUser() {
try {
const response = await api.get('/auth/me');
setUser(response.data.user);
} catch (error) {
localStorage.removeItem('accessToken');
} finally {
setLoading(false);
}
}
async function login(email, password) {
const response = await api.post('/auth/login', { email, password });
const { accessToken, user } = response.data;
localStorage.setItem('accessToken', accessToken);
setUser(user);
navigate('/dashboard');
}
async function register(name, email, password) {
await api.post('/auth/register', { name, email, password });
await login(email, password);
}
async function logout() {
try {
await api.post('/auth/logout');
} finally {
localStorage.removeItem('accessToken');
setUser(null);
navigate('/login');
}
}
const value = {
user,
loading,
login,
register,
logout,
isAuthenticated: !!user
};
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;
}
// services/api.js - Axios with interceptors
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
withCredentials: true // For cookies
});
// Add token to requests
api.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const response = await api.post('/auth/refresh');
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
// components/LoginForm.jsx
function LoginForm() {
const { login } = useAuth();
const [error, setError] = useState('');
async function handleSubmit(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);
} catch (err) {
setError(err.response?.data?.error || 'Login failed');
}
}
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>
);
}
Security Best Practices
- Hash passwords: Always use bcrypt with salt rounds >= 10
- Use HTTPS: Never transmit tokens over plain HTTP
- Short-lived access tokens: 15 minutes max for access tokens
- Secure refresh tokens: Store in httpOnly cookies, not localStorage
- Rate limit login: Prevent brute force attacks
- Validate input: Sanitize all user input to prevent injection
- Use secure headers: helmet.js for Express security headers
- Rotate secrets: Have a plan to rotate JWT secrets
- Logout everywhere: Implement token blacklisting for logout
- Monitor failed attempts: Log and alert on suspicious activity
# Security packages
npm install helmet express-rate-limit express-mongo-sanitize
// Security middleware setup
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import mongoSanitize from 'express-mongo-sanitize';
// Security headers
app.use(helmet());
// Rate limiting for auth routes
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later'
});
app.use('/auth/login', authLimiter);
app.use('/auth/register', authLimiter);
// Prevent NoSQL injection
app.use(mongoSanitize());
// Password requirements
const passwordSchema = {
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1
};
function validatePassword(password) {
// Use a library like password-validator
return validator.validate(password, passwordSchema);
}
Build Secure Applications
Our Full Stack JavaScript program covers authentication in depth. Learn to build secure applications with expert guidance.
Explore JavaScript Program