music on atproto
plyr.fm
1# Security
2
3Overview of security mechanisms in plyr.fm.
4
5## Authentication
6
7We use **HttpOnly Cookies** for session management to prevent XSS attacks.
8See [Authentication](authentication.md) for details on the OAuth flow, token management, and environment architecture.
9
10For backend implementation details regarding ATProto identity resolution, see [backend/atproto-identity.md](backend/atproto-identity.md).
11
12## Rate Limiting
13
14We enforce application-side rate limits to prevent abuse.
15See [Rate Limiting](rate-limiting.md) for configuration and architecture details.
16
17## HTTP Security Headers
18
19The `SecurityHeadersMiddleware` in `src/backend/main.py` automatically applies industry-standard security headers to all responses:
20
21* **`Strict-Transport-Security` (HSTS):** Enforces HTTPS (Production only). Max-age set to 1 year.
22* **`X-Content-Type-Options: nosniff`:** Prevents browsers from MIME-sniffing a response away from the declared content-type.
23* **`X-Frame-Options: DENY`:** Prevents the site from being embedded in iframes (clickjacking protection).
24* **`X-XSS-Protection: 1; mode=block`:** Enables browser cross-site scripting filters.
25* **`Referrer-Policy: strict-origin-when-cross-origin`:** Controls how much referrer information is included with requests.
26
27## Supporter-Gated Content
28
29Tracks with `support_gate` set require atprotofans supporter validation before streaming.
30
31### Access Model
32
33```
34request → /audio/{file_id} → check support_gate
35 ↓
36 ┌──────────┴──────────┐
37 ↓ ↓
38 public gated track
39 ↓ ↓
40 307 → R2 CDN validate_supporter()
41 ↓
42 ┌──────────┴──────────┐
43 ↓ ↓
44 is supporter not supporter
45 ↓ ↓
46 presigned URL (5min) 402 error
47```
48
49### Storage Architecture
50
51- **public bucket**: `plyr-audio` - CDN-backed, public read access
52- **private bucket**: `plyr-audio-private` - no public access, presigned URLs only
53
54when `support_gate` is toggled, a background task moves the file between buckets.
55
56### Presigned URL Behavior
57
58presigned URLs are time-limited (5 minutes) and grant direct R2 access. security considerations:
59
601. **URL sharing**: a supporter could share the presigned URL. mitigation: short TTL, URLs expire quickly.
61
622. **offline caching**: if a supporter downloads content (via "download liked tracks"), the cached audio persists locally even if support lapses. this is **intentional** - they legitimately accessed it when authorized.
63
643. **auto-download + gated tracks**: the `gated` field is viewer-resolved (true = no access, false = has access). when liking a track with auto-download enabled:
65 - **supporters** (`gated === false`): download proceeds normally via presigned URL
66 - **non-supporters** (`gated === true`): download is skipped client-side to avoid wasted 402 requests
67
68### ATProto Record Behavior
69
70when a track is gated, the ATProto `fm.plyr.track` record's `audioUrl` changes:
71- **public**: points to R2 CDN URL (e.g., `https://cdn.plyr.fm/audio/abc123.mp3`)
72- **gated**: points to API endpoint (e.g., `https://api.plyr.fm/audio/abc123`)
73
74this means ATProto clients cannot stream gated content without authentication through plyr.fm's API.
75
76### Validation Caching
77
78currently, `validate_supporter()` makes a fresh call to atprotofans on every request. for high-traffic gated tracks, consider adding a short TTL cache (e.g., 60s in redis) to reduce latency and avoid rate limits.
79
80## CORS
81
82Cross-Origin Resource Sharing (CORS) is configured to allow:
83* **Localhost:** For development (`http://localhost:5173`).
84* **Production/Staging Domains:** `plyr.fm`, `stg.plyr.fm`, and Cloudflare Pages preview URLs (via regex).
85
86Configuration is managed in `src/backend/config.py` under `FrontendSettings`.