379
docs/archive/MEDIA_AUTH_FIX_2025-10-31.md
Normal file
379
docs/archive/MEDIA_AUTH_FIX_2025-10-31.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# 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
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```python
|
||||
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:**
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
```html
|
||||
<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:**
|
||||
1. **Rate limiting** - Media endpoints limited to 100 requests/minute
|
||||
2. **Token expiration** - JWT tokens expire after 24 hours
|
||||
3. **Session tracking** - Sessions stored in database, can be revoked
|
||||
4. **HTTPS** - Already handled by nginx proxy, encrypts URLs in transit
|
||||
5. **Limited scope** - Only applies to media endpoints, not sensitive operations
|
||||
|
||||
**Alternatives considered:**
|
||||
1. ❌ **Make media public** - Defeats authentication purpose
|
||||
2. ❌ **Cookie-based auth** - Requires CSRF protection, more complex
|
||||
3. ✅ **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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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`
|
||||
|
||||
1. **Added new auth dependency** (line ~131):
|
||||
```python
|
||||
async def get_current_user_media(...)
|
||||
```
|
||||
|
||||
2. **Updated `/api/media/thumbnail` endpoint** (line ~1921):
|
||||
- Added `token: str = None` parameter
|
||||
- Changed auth from `get_current_user` to `get_current_user_media`
|
||||
|
||||
3. **Updated `/api/media/preview` endpoint** (line ~1957):
|
||||
- Added `token: str = None` parameter
|
||||
- Changed auth from `get_current_user` to `get_current_user_media`
|
||||
|
||||
### Frontend
|
||||
**File:** `/opt/media-downloader/web/frontend/src/lib/api.ts`
|
||||
|
||||
1. **Updated `getMediaPreviewUrl()`** (line ~435):
|
||||
- Reads token from localStorage
|
||||
- Appends `&token=...` to URL if token exists
|
||||
|
||||
2. **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)
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
```bash
|
||||
# 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:**
|
||||
```bash
|
||||
# Check for suspicious media access patterns
|
||||
sudo journalctl -u media-downloader-api | grep "media/thumbnail"
|
||||
```
|
||||
|
||||
### Security Audit
|
||||
|
||||
Run periodic checks:
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
1. **Login to application**
|
||||
2. **Navigate to Downloads or Media page**
|
||||
3. **Verify thumbnails loading**
|
||||
4. **Click thumbnail to open lightbox**
|
||||
5. **Verify full image/video loads**
|
||||
6. **Check browser console for no errors**
|
||||
|
||||
---
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Blob URL Implementation**
|
||||
- More secure, tokens not in URL
|
||||
- Requires frontend refactoring
|
||||
|
||||
2. **Token Rotation**
|
||||
- Short-lived tokens for media access
|
||||
- Separate media access tokens
|
||||
|
||||
3. **Watermarking**
|
||||
- Add user watermark to previews
|
||||
- Deter unauthorized sharing
|
||||
|
||||
4. **Access Logging**
|
||||
- Log who accessed what media
|
||||
- Analytics dashboard
|
||||
|
||||
5. **Progressive Loading**
|
||||
- Blur placeholder while loading
|
||||
- Better UX during auth check
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If issues occur, revert changes:
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
Reference in New Issue
Block a user