9.7 KiB
Media Authentication Fix
Date: 2025-10-31 Issue: Media thumbnails and images broken after adding authentication Status: ✅ FIXED
Problem
After implementing authentication on all API endpoints, media thumbnails and images stopped loading in the frontend. The issue was that <img> and <video> HTML tags cannot send Authorization headers, which are required for Bearer token authentication.
Error Symptoms
- All thumbnails showing as broken images
- Preview images not loading in lightbox
- Video previews failing to load
- Browser console: HTTP 401 Unauthorized errors
Root Cause
// Frontend code using img tags
<img src={api.getMediaThumbnailUrl(filePath, mediaType)} />
// The API returns just a URL string
getMediaThumbnailUrl(filePath: string, mediaType: string) {
return `/api/media/thumbnail?file_path=${filePath}&media_type=${mediaType}`
}
The browser makes a direct GET request for the image without any auth headers:
GET /api/media/thumbnail?file_path=...
(No Authorization header)
→ HTTP 401 Unauthorized
Solution
1. Backend: Query Parameter Token Support
Created a new authentication dependency that accepts tokens via query parameters in addition to Authorization headers:
async def get_current_user_media(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
token: Optional[str] = None
) -> Dict:
"""
Authentication for media endpoints that supports both header and query parameter tokens.
This allows <img> and <video> tags to work by including token in URL.
"""
auth_token = None
# Try to get token from Authorization header first
if credentials:
auth_token = credentials.credentials
# Fall back to query parameter
elif token:
auth_token = token
if not auth_token:
raise HTTPException(status_code=401, detail="Not authenticated")
payload = app_state.auth.verify_session(auth_token)
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return payload
Applied to endpoints:
/api/media/thumbnail- Get or generate thumbnails/api/media/preview- Serve full media files
Updated signatures:
# Before
async def get_media_thumbnail(
request: Request,
current_user: Dict = Depends(get_current_user),
file_path: str = None,
media_type: str = None
):
# After
async def get_media_thumbnail(
request: Request,
file_path: str = None,
media_type: str = None,
token: str = None, # NEW: query parameter
current_user: Dict = Depends(get_current_user_media) # NEW: supports query param
):
2. Frontend: Append Tokens to URLs
Updated API utility functions to append authentication tokens to media URLs:
// Before
getMediaPreviewUrl(filePath: string) {
return `${API_BASE}/media/preview?file_path=${encodeURIComponent(filePath)}`
}
// After
getMediaPreviewUrl(filePath: string) {
const token = localStorage.getItem('auth_token')
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : ''
return `${API_BASE}/media/preview?file_path=${encodeURIComponent(filePath)}${tokenParam}`
}
Now when the browser loads an image:
<img src="/api/media/thumbnail?file_path=...&media_type=image&token=eyJhbGci..." />
The token is included in the URL, and the backend can authenticate the request.
Security Considerations
Token in URL Query Parameters
Concerns:
- Tokens visible in browser history
- Tokens may appear in server logs
- Tokens could leak via Referer header
Mitigations:
- Rate limiting - Media endpoints limited to 100 requests/minute
- Token expiration - JWT tokens expire after 24 hours
- Session tracking - Sessions stored in database, can be revoked
- HTTPS - Already handled by nginx proxy, encrypts URLs in transit
- Limited scope - Only applies to media endpoints, not sensitive operations
Alternatives considered:
- ❌ Make media public - Defeats authentication purpose
- ❌ Cookie-based auth - Requires CSRF protection, more complex
- ✅ Token in query param - Simple, works with img/video tags, acceptable risk
Best Practices Applied
✅ Header authentication preferred (checked first) ✅ Query param fallback only for media ✅ Token validation same as header auth ✅ Session tracking maintained ✅ Rate limiting enforced ✅ HTTPS encryption in place
Testing Results
Thumbnail Endpoint
# With token
curl "http://localhost:8000/api/media/thumbnail?file_path=/path/to/image.jpg&media_type=image&token=JWT_TOKEN"
→ HTTP 200 (returns JPEG thumbnail)
# Without token
curl "http://localhost:8000/api/media/thumbnail?file_path=/path/to/image.jpg&media_type=image"
→ HTTP 401 {"detail":"Not authenticated"}
Preview Endpoint
# With token
curl "http://localhost:8000/api/media/preview?file_path=/path/to/video.mp4&token=JWT_TOKEN"
→ HTTP 200 (returns video file)
# Without token
curl "http://localhost:8000/api/media/preview?file_path=/path/to/video.mp4"
→ HTTP 401 {"detail":"Not authenticated"}
Frontend
✅ Thumbnails loading in Downloads page ✅ Thumbnails loading in Media Gallery ✅ Lightbox preview working for images ✅ Video playback working ✅ Token automatically appended to URLs ✅ No console errors
Files Modified
Backend
File: /opt/media-downloader/web/backend/api.py
-
Added new auth dependency (line ~131):
async def get_current_user_media(...) -
Updated
/api/media/thumbnailendpoint (line ~1921):- Added
token: str = Noneparameter - Changed auth from
get_current_usertoget_current_user_media
- Added
-
Updated
/api/media/previewendpoint (line ~1957):- Added
token: str = Noneparameter - Changed auth from
get_current_usertoget_current_user_media
- Added
Frontend
File: /opt/media-downloader/web/frontend/src/lib/api.ts
-
Updated
getMediaPreviewUrl()(line ~435):- Reads token from localStorage
- Appends
&token=...to URL if token exists
-
Updated
getMediaThumbnailUrl()(line ~441):- Reads token from localStorage
- Appends
&token=...to URL if token exists
Alternative Approaches
Option 1: Blob URLs with Fetch (Most Secure)
async function getMediaThumbnailUrl(filePath: string, mediaType: string) {
const response = await fetch(`/api/media/thumbnail?file_path=${filePath}`, {
headers: { 'Authorization': `Bearer ${token}` }
})
const blob = await response.blob()
return URL.createObjectURL(blob)
}
Pros:
- Token never in URL
- Most secure approach
- Standard authentication
Cons:
- More complex implementation
- Requires updating all components
- Memory management for blob URLs
- Extra network requests
Future consideration: If security requirements increase, this approach should be implemented.
Option 2: Cookie-Based Authentication
Set JWT as HttpOnly cookie instead of localStorage.
Pros:
- Automatic inclusion in requests
- Works with img/video tags
- HttpOnly protects from XSS
Cons:
- Requires CSRF protection
- More complex cookie handling
- Domain/path considerations
- Mobile app compatibility issues
Monitoring
Check for Token Leakage
Server logs:
# Check if tokens appearing in access logs
sudo grep "token=" /var/log/nginx/access.log | head -5
If tokens are being logged, update nginx config to filter query parameters from logs.
Rate limit monitoring:
# Check for suspicious media access patterns
sudo journalctl -u media-downloader-api | grep "media/thumbnail"
Security Audit
Run periodic checks:
# Test unauthenticated access blocked
curl -s "http://localhost:8000/api/media/thumbnail?file_path=/test.jpg&media_type=image"
# Should return: {"detail":"Not authenticated"}
# Test rate limiting
for i in {1..110}; do
curl -s "http://localhost:8000/api/media/thumbnail?..."
done
# Should hit rate limit after 100 requests
Deployment Notes
Service Restart
# API backend
sudo systemctl restart media-downloader-api
# Frontend (if using systemd service)
sudo systemctl restart media-downloader-frontend
# Or if using vite dev server, it auto-reloads
Verification
- Login to application
- Navigate to Downloads or Media page
- Verify thumbnails loading
- Click thumbnail to open lightbox
- Verify full image/video loads
- Check browser console for no errors
Future Improvements
-
Blob URL Implementation
- More secure, tokens not in URL
- Requires frontend refactoring
-
Token Rotation
- Short-lived tokens for media access
- Separate media access tokens
-
Watermarking
- Add user watermark to previews
- Deter unauthorized sharing
-
Access Logging
- Log who accessed what media
- Analytics dashboard
-
Progressive Loading
- Blur placeholder while loading
- Better UX during auth check
Rollback Procedure
If issues occur, revert changes:
# Backend
cd /opt/media-downloader
git checkout HEAD~1 web/backend/api.py
# Frontend
git checkout HEAD~1 web/frontend/src/lib/api.ts
# Restart services
sudo systemctl restart media-downloader-api
Note: This will make media endpoints unauthenticated again. Only use in emergency.
Summary
✅ Issue: Media broken due to authentication on img/video tag endpoints ✅ Solution: Support token in query parameter for media endpoints ✅ Testing: Both thumbnail and preview endpoints work with token parameter ✅ Security: Acceptable risk given rate limiting, HTTPS, and token expiration ✅ Status: Fully operational
Impact: Media gallery and thumbnails now working with authentication maintained.