Authorization & Access Control
Overview
Authorization determines what an authenticated user can do. This document explains:
- Role-based access control (RBAC) in StockEase
- How permissions are enforced
- Protecting routes and endpoints
- Error handling and security considerations
Role-Based Access Control (RBAC)
Roles in StockEase
StockEase implements a two-tier role system:
| Role | Description | Permissions |
|---|---|---|
| ROLE_ADMIN | Administrator | Full system access, manage users, view all products, system settings |
| ROLE_USER | Regular User | Limited access, view own products, basic operations |
Where Roles Come From
1. During Login:
// Backend returns JWT with role
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"role": "ROLE_ADMIN" // Backend determines this
}2. Frontend Extracts Role:
// Login service extracts from JWT
const decodedPayload = JSON.parse(atob(token.split('.')[1]));
const role = decodedPayload.role;
// Store for later use
localStorage.setItem('role', role);3. Backend Assigns Role:
# Backend database stores user role
class User:
id: int
username: str
email: str
password_hash: str
role: str # "ROLE_ADMIN" or "ROLE_USER"
# When issuing token
payload = {
'user': user.username,
'role': user.role, # From database
'exp': datetime.utcnow() + timedelta(hours=24)
}Frontend Authorization
Route Protection
Using Protected Routes Component:
// In App.tsx
interface ProtectedRouteProps {
element: React.ReactElement;
requiredRole?: string;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
element,
requiredRole
}) => {
const token = localStorage.getItem('token');
const role = localStorage.getItem('role');
// No token = not authenticated
if (!token) {
return <Navigate to="/login" replace />;
}
// Has required role requirement?
if (requiredRole && role !== requiredRole) {
return <ErrorPage status={403} message="Forbidden" />;
}
// Authorized, render component
return element;
};
// Define routes
const router = createBrowserRouter([
{ path: '/login', element: <LoginPage /> },
{
path: '/user',
element: <ProtectedRoute element={<UserDashboard />} />
},
{
path: '/admin',
element: <ProtectedRoute
element={<AdminDashboard />}
requiredRole="ROLE_ADMIN"
/>
}
]);Route Protection Flow:
User visits /admin
β
ProtectedRoute checks: Do we have token?
ββ No β Redirect to /login
ββ Yes β Continue
β
ProtectedRoute checks: Do we have required role?
ββ No role required β Render component
ββ Role required β Check if user has it
ββ Yes β Render component
ββ No β Show 403 error page
Component-Level Authorization
Conditionally Render Admin Features:
import { useAuth } from '@hooks/useAuth';
const UserDashboard: React.FC = () => {
const { role, username } = useAuth();
return (
<div>
<h1>Welcome {username}</h1>
{/* All users see this */}
<ProductList />
{/* Only admins see this */}
{role === 'ROLE_ADMIN' && (
<AdminSection>
<UserManagement />
<SystemSettings />
</AdminSection>
)}
</div>
);
};useAuth Hook
Implementation:
// src/hooks/useAuth.ts
import { useEffect, useState } from 'react';
interface AuthContext {
isAuthenticated: boolean;
role: 'ROLE_ADMIN' | 'ROLE_USER' | null;
username: string | null;
logout: () => void;
}
export const useAuth = (): AuthContext => {
const [auth, setAuth] = useState<AuthContext>({
isAuthenticated: false,
role: null,
username: null,
logout: () => {}
});
useEffect(() => {
// Read from localStorage
const token = localStorage.getItem('token');
const role = localStorage.getItem('role');
const username = localStorage.getItem('username');
setAuth({
isAuthenticated: !!token,
role: (role as any) || null,
username: username || null,
logout: () => {
localStorage.removeItem('token');
localStorage.removeItem('role');
localStorage.removeItem('username');
window.location.href = '/login';
}
});
}, []);
return auth;
};Usage:
const { isAuthenticated, role, username, logout } = useAuth();
// Check authentication
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
// Check specific role
if (role === 'ROLE_ADMIN') {
// Show admin features
}
// Logout
<button onClick={logout}>Logout</button>Backend Authorization
Endpoint Protection
Backend must verify authorization on every request:
from functools import wraps
from flask import request, jsonify
def require_role(required_role):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Get Authorization header
auth_header = request.headers.get('Authorization')
if not auth_header:
return jsonify({'error': 'Missing Authorization header'}), 401
try:
# Extract token
token = auth_header.replace('Bearer ', '')
# Verify signature and decode
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# Check role
if payload['role'] != required_role:
return jsonify({'error': 'Forbidden'}), 403
# Token valid and authorized, call endpoint
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidSignatureError:
return jsonify({'error': 'Invalid token'}), 401
return decorated_function
return decorator
# Usage:
@app.route('/api/admin/users', methods=['GET'])
@require_role('ROLE_ADMIN') # Only admins can access
def get_all_users():
return jsonify(users)
@app.route('/api/products', methods=['GET'])
def get_products(): # Anyone authenticated can access
# (no @require_role, so token just needs to be valid)
return jsonify(products)Permission Levels
Common approach:
# Level 0: Public endpoint (no auth required)
@app.route('/api/health', methods=['GET'])
def health_check():
return jsonify({'status': 'ok'})
# Level 1: Authenticated users (any role)
@app.route('/api/products', methods=['GET'])
def get_products():
# Require valid token
token = verify_token(request) # Raises 401 if invalid
return jsonify(products)
# Level 2: Specific role required
@app.route('/api/admin/settings', methods=['GET'])
@require_role('ROLE_ADMIN')
def get_settings():
return jsonify(settings)
# Level 3: Multiple roles allowed
@app.route('/api/reports', methods=['GET'])
@require_any_role(['ROLE_ADMIN', 'ROLE_MANAGER'])
def get_reports():
return jsonify(reports)Permission Matrix
StockEase Permissions
Endpoint ROLE_USER ROLE_ADMIN
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
GET /api/products β
β
POST /api/products β
* β
PUT /api/products/{id} β
* β
DELETE /api/products/{id} β
* β
GET /api/admin/users β β
POST /api/admin/users β β
PUT /api/admin/users/{id} β β
DELETE /api/admin/users/{id} β β
GET /api/admin/logs β β
POST /api/admin/settings β β
Key:
- β = Allowed
- β = Forbidden (403 error)
- β * = Allowed but restricted (e.g., can only modify own products)
Resource-Level Authorization
Users can only modify their own products:
@app.route('/api/products/<int:product_id>', methods=['PUT'])
def update_product(product_id):
# Get user from token
token = verify_token(request)
user_id = token['user_id']
# Get product from database
product = Product.query.get(product_id)
# Check ownership (unless admin)
if product.user_id != user_id and token['role'] != 'ROLE_ADMIN':
return jsonify({'error': 'Forbidden'}), 403
# User owns product or is admin, update allowed
product.update(request.json)
return jsonify(product)Flow:
User (ID: 5) tries to update product (ID: 123, owner: ID: 5)
β
Backend checks: product.user_id (5) == user_id (5)?
ββ Yes β Allow update
ββ No β Check is_admin?
ββ Yes β Allow update
ββ No β Return 403 Forbidden
Error Handling
Authentication Errors (401)
When: Token is missing, invalid, or expired
{
"status": 401,
"error": "Unauthorized",
"message": "Invalid or expired token"
}Causes:
- No Authorization header
- Invalid token signature
- Expired token
- Malformed token
Frontend response:
// In API response interceptor
if (error.response?.status === 401) {
// Clear session
localStorage.removeItem('token');
localStorage.removeItem('role');
// Redirect to login
navigate('/login');
}Authorization Errors (403)
When: User is authenticated but doesn't have required role
{
"status": 403,
"error": "Forbidden",
"message": "Insufficient privileges"
}Causes:
- User role doesn't match endpoint requirement
- User trying to access admin feature
- User trying to modify another user's resource
Frontend response:
// Show error to user
if (error.response?.status === 403) {
setError('You do not have permission to access this resource');
}
// In ProtectedRoute
if (requiredRole && role !== requiredRole) {
return <ErrorPage status={403} message="Forbidden" />;
}Common Patterns
Pattern 1: Owner or Admin
Allow if user owns resource OR is admin:
const canModifyProduct = (
product: Product,
currentUserId: string,
userRole: string
): boolean => {
// Owner can modify
if (product.userId === currentUserId) {
return true;
}
// Admin can modify
if (userRole === 'ROLE_ADMIN') {
return true;
}
// Otherwise, cannot modify
return false;
};Usage:
if (canModifyProduct(product, currentUserId, userRole)) {
// Show edit button
<button onClick={editProduct}>Edit</button>
} else {
// Hide edit button
}Pattern 2: Role-Based Feature Flags
Show features based on role:
const features = {
canViewReports: role === 'ROLE_ADMIN',
canManageUsers: role === 'ROLE_ADMIN',
canDeleteProducts: role === 'ROLE_ADMIN',
canViewProductStats: role === 'ROLE_ADMIN' || role === 'ROLE_USER'
};
// Usage
{features.canViewReports && <ReportsSection />}
{features.canManageUsers && <UserManagement />}Pattern 3: Conditional API Calls
Only make API call if authorized:
const fetchAdminData = async () => {
const role = localStorage.getItem('role');
if (role !== 'ROLE_ADMIN') {
setError('Not authorized');
return;
}
try {
const data = await apiClient.get('/api/admin/data');
setData(data);
} catch (error) {
// Handle 403 Forbidden
}
};Security Best Practices
β DO:
- β Always verify token on backend
- β Always check role on backend
- β Check ownership of resources
- β Log authorization failures
- β Use clear role names
- β Fail securely (default to deny)
- β Validate on both frontend and backend
- β Clear token on logout
β DON'T:
- β Rely only on frontend authorization
- β Trust client-provided role information
- β Skip authorization checks
- β Use vague permission names
- β Fail open (default to allow)
- β Store passwords with roles
- β Change role in localStorage
- β Share tokens between users
Additional Safeguards
1. Don't Trust Frontend Authorization:
// β WRONG: Trust frontend
const role = localStorage.getItem('role');
if (role === 'ROLE_ADMIN') {
// This could be faked!
}
// β
CORRECT: Backend verifies
const response = await apiClient.get('/api/admin/users');
// Backend checks token and role2. Implement Rate Limiting:
# Backend: Limit API calls per user
from flask_limiter import Limiter
limiter = Limiter(app, key_func=lambda: get_current_user_id())
@app.route('/api/products', methods=['GET'])
@limiter.limit('100 per hour') # Max 100 requests per hour
def get_products():
return products3. Audit Authorization Failures:
# Backend: Log failed authorization attempts
def log_auth_failure(user_id, endpoint, reason):
AuthLog.create(
user_id=user_id,
endpoint=endpoint,
reason=reason,
timestamp=datetime.utcnow()
)
# Usage
@app.route('/api/admin/users')
def admin_endpoint():
if role != 'ROLE_ADMIN':
log_auth_failure(user_id, '/api/admin/users', 'insufficient_privileges')
return {'error': 'Forbidden'}, 403Testing Authorization
Frontend Tests
// Test protected route redirects unauthorized users
describe('ProtectedRoute', () => {
it('redirects to login if no token', () => {
localStorage.removeItem('token');
render(<ProtectedRoute element={<AdminPage />} />);
expect(screen.getByText('redirected to')).toHaveTextContent('/login');
});
it('shows forbidden if role not matching', () => {
localStorage.setItem('token', 'valid-token');
localStorage.setItem('role', 'ROLE_USER'); // Not admin
render(
<ProtectedRoute
element={<AdminPage />}
requiredRole="ROLE_ADMIN"
/>
);
expect(screen.getByText('Forbidden')).toBeInTheDocument();
});
});Backend Tests
# Test endpoint authorization
def test_admin_endpoint_requires_admin_role():
# User token
user_token = create_token(role='ROLE_USER')
response = client.get(
'/api/admin/users',
headers={'Authorization': f'Bearer {user_token}'}
)
assert response.status_code == 403
assert 'Forbidden' in response.json['error']
def test_admin_endpoint_allows_admin():
# Admin token
admin_token = create_token(role='ROLE_ADMIN')
response = client.get(
'/api/admin/users',
headers={'Authorization': f'Bearer {admin_token}'}
)
assert response.status_code == 200Troubleshooting
Issue: User sees 403 Forbidden on Admin Page
Possible causes:
- User role is not 'ROLE_ADMIN'
- Token doesn't contain role claim
- Backend doesn't recognize role
Solution:
// Check what role is stored
console.log('Stored role:', localStorage.getItem('role'));
// Check JWT token contents
const token = localStorage.getItem('token');
const decoded = JSON.parse(atob(token.split('.')[1]));
console.log('Token role:', decoded.role);
// They should match
// If not, the issue is during loginIssue: Admin can't access protected endpoint
Possible causes:
- Token not being sent in request
- Authorization header malformed
- Backend not recognizing token
Solution:
// Check if Authorization header is being sent
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log('Sending token:', token.substring(0, 20) + '...');
}
return config;
});
// Check network tab in DevTools
// Look for Authorization: Bearer ... headerRelated Files
- Protected Routes:
src/App.tsx - Auth Hook:
src/hooks/useAuth.ts - Login Page:
src/pages/LoginPage.tsx - Admin Dashboard:
src/pages/AdminDashboard.tsx - API Client:
src/services/apiClient.ts - Auth Service:
src/api/auth.ts
Last Updated: November 13, 2025
Status: Production-Ready