Authentication: JWT and OAuth 2.0 Complete Guide
Master authentication for web applications. Learn JWT tokens, OAuth 2.0 flows, refresh tokens, and implement secure authentication systems.
Moshiour Rahman
Advertisement
Authentication Fundamentals
Authentication verifies user identity, while authorization determines what they can access. Modern applications typically use token-based authentication for stateless, scalable systems.
Authentication Methods
| Method | Use Case |
|---|---|
| Session | Traditional web apps |
| JWT | APIs, SPAs, Mobile |
| OAuth 2.0 | Third-party login |
| API Keys | Server-to-server |
JWT (JSON Web Tokens)
JWT Structure
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header: Algorithm & token type
- Payload: Claims (user data)
- Signature: Verification hash
JWT Implementation
// auth.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '15m';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
// Generate tokens
export function generateTokens(user: User) {
const payload: TokenPayload = {
userId: user.id,
email: user.email,
role: user.role
};
const accessToken = jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
const refreshToken = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES_IN }
);
return { accessToken, refreshToken };
}
// Verify token
export function verifyToken(token: string): TokenPayload {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
}
// Hash password
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
// Compare passwords
export async function comparePasswords(
password: string,
hashedPassword: string
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
Authentication Middleware
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../auth';
declare global {
namespace Express {
interface Request {
user?: TokenPayload;
}
}
}
export function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based authorization
export function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Auth Routes
// routes/auth.ts
import express from 'express';
import { hashPassword, comparePasswords, generateTokens, verifyToken } from '../auth';
import { authenticate } from '../middleware/auth';
const router = express.Router();
// Register
router.post('/register', async (req, res) => {
try {
const { email, password, name } = 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 and create user
const hashedPassword = await hashPassword(password);
const user = await User.create({
email,
password: hashedPassword,
name,
role: 'user'
});
// Generate tokens
const { accessToken, refreshToken } = generateTokens(user);
// Store refresh token
await RefreshToken.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.status(201).json({
user: { id: user.id, email: user.email, name: user.name },
accessToken,
refreshToken
});
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
});
// Login
router.post('/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' });
}
// Verify password
const isValid = await comparePasswords(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const { accessToken, refreshToken } = generateTokens(user);
// Store refresh token
await RefreshToken.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.json({ accessToken, refreshToken });
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
});
// Refresh token
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
// Verify refresh token
const decoded = verifyToken(refreshToken);
// Check if token exists in database
const storedToken = await RefreshToken.findOne({
token: refreshToken,
userId: decoded.userId
});
if (!storedToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Get user
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// Generate new tokens
const tokens = generateTokens(user);
// Rotate refresh token
await RefreshToken.deleteOne({ token: refreshToken });
await RefreshToken.create({
token: tokens.refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.json(tokens);
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Logout
router.post('/logout', authenticate, async (req, res) => {
try {
const { refreshToken } = req.body;
// Delete refresh token
await RefreshToken.deleteOne({
token: refreshToken,
userId: req.user!.userId
});
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Logout failed' });
}
});
// Get current user
router.get('/me', authenticate, async (req, res) => {
const user = await User.findById(req.user!.userId).select('-password');
res.json(user);
});
export default router;
OAuth 2.0
OAuth Flows
| Flow | Use Case |
|---|---|
| Authorization Code | Server-side apps |
| PKCE | SPAs, Mobile apps |
| Client Credentials | Machine-to-machine |
| Implicit | Legacy (not recommended) |
Google OAuth Implementation
// config/passport.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
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 {
// Find or create user
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails?.[0].value,
name: profile.displayName,
avatar: profile.photos?.[0].value,
provider: 'google'
});
}
done(null, user);
} catch (error) {
done(error, undefined);
}
}
)
);
OAuth Routes
// routes/oauth.ts
import express from 'express';
import passport from 'passport';
import { generateTokens } from '../auth';
const router = express.Router();
// Initiate Google OAuth
router.get(
'/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
// Google OAuth callback
router.get(
'/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const user = req.user as User;
const { accessToken, refreshToken } = generateTokens(user);
// Redirect to frontend with tokens
res.redirect(
`${process.env.FRONTEND_URL}/auth/callback?` +
`accessToken=${accessToken}&refreshToken=${refreshToken}`
);
}
);
export default router;
PKCE Flow (For SPAs)
// Frontend implementation
import { generateCodeVerifier, generateCodeChallenge } from './pkce';
async function initiateOAuth() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store verifier for later
sessionStorage.setItem('code_verifier', codeVerifier);
const params = new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID,
redirect_uri: `${window.location.origin}/callback`,
response_type: 'code',
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: crypto.randomUUID()
});
window.location.href = `https://auth.example.com/authorize?${params}`;
}
async function handleCallback(code: string) {
const codeVerifier = sessionStorage.getItem('code_verifier');
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
code_verifier: codeVerifier,
redirect_uri: `${window.location.origin}/callback`
})
});
const { access_token, refresh_token } = await response.json();
// Store tokens
}
React Authentication
Auth Context
// context/AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
id: string;
email: string;
name: string;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
register: (email: string, password: string, name: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
async function checkAuth() {
const token = localStorage.getItem('accessToken');
if (!token) {
setIsLoading(false);
return;
}
try {
const response = await fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const user = await response.json();
setUser(user);
} else {
// Try to refresh token
await refreshToken();
}
} catch (error) {
logout();
} finally {
setIsLoading(false);
}
}
async function refreshToken() {
const refresh = localStorage.getItem('refreshToken');
if (!refresh) throw new Error('No refresh token');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: refresh })
});
if (!response.ok) throw new Error('Refresh failed');
const { accessToken, refreshToken: newRefresh } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefresh);
await checkAuth();
}
async function login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const { accessToken, refreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
await checkAuth();
}
function logout() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setUser(null);
}
async function register(email: string, password: string, name: string) {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const { accessToken, refreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
await checkAuth();
}
return (
<AuthContext.Provider value={{ user, isLoading, login, logout, register }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Protected Routes
// components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// Usage
function App() {
return (
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
</AuthProvider>
);
}
Security Best Practices
- Use HTTPS everywhere
- Store tokens securely (httpOnly cookies or secure storage)
- Short-lived access tokens (15min or less)
- Rotate refresh tokens on use
- Validate all inputs
- Use strong password hashing (bcrypt, argon2)
- Implement rate limiting
- Add CORS protection
Summary
| Concept | Purpose |
|---|---|
| JWT | Stateless authentication |
| Refresh Tokens | Extended sessions |
| OAuth 2.0 | Third-party authentication |
| PKCE | Secure OAuth for SPAs |
| Middleware | Route protection |
| Context | React auth state |
Proper authentication is crucial for application security and user trust.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
GraphQL API Development: Complete Guide with Node.js
Master GraphQL API development from scratch. Learn schema design, resolvers, queries, mutations, subscriptions, and authentication best practices.
JavaScriptBun: The Fast JavaScript Runtime and Toolkit
Master Bun for JavaScript development. Learn the runtime, package manager, bundler, test runner, and build faster applications with Bun.
JavaScriptMongoDB with Node.js: Complete Database Guide
Master MongoDB with Node.js and Mongoose. Learn CRUD operations, schema design, indexing, aggregation pipelines, and production best practices.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.