Files
media-downloader/docs/archive/MEDIA_AUTH_FIX_2025-10-31.md
Todd 0d7b2b1aab Initial commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:42:55 -04:00

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:

  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

# 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

  1. Added new auth dependency (line ~131):

    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)

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.

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

  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:

# 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.