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