Add single-user GitHub OAuth, hardened for internet exposure
muse drives a download client and writes to the media library, so exposing it publicly needs more than a login form. It fails closed. Auth is on unless MUSE_AUTH=off is set explicitly, and when it is on with incomplete config the app refuses to start rather than serving open. The mode worth engineering against is not a broken login page, it is a misconfigured deploy quietly publishing an unauthenticated download client. Sessions are server-side: the cookie carries a 256-bit random token and nothing else, and only its SHA-256 is stored. There is no signed payload to get subtly wrong, a stolen database yields no usable cookies, and logout revokes for real. The allowlist is re-checked on every request so removing a user takes effect immediately rather than at session expiry. OAuth state is compared with secrets.compare_digest, so a callback the user did not initiate is rejected. MUSE_BASE_URL comes from config rather than X-Forwarded-* because those headers are attacker-controllable and this one is a redirect target. CSP is default-src 'self' with no unsafe-inline, which is why the stylesheet and script moved out of index.html into static files -- an injected <script> now cannot execute. Also frame-ancestors 'none', base-uri 'none', nosniff, DENY, no-referrer and HSTS. FastAPI's /docs, /redoc and /openapi.json are disabled so the login page does not hand out a map of every endpoint. /healthz is the only unauthenticated route and returns a bare ok, so the container healthcheck works without leaking anything. Verified: misconfigured auth refuses to boot; unauthenticated / redirects and /api/* returns 401; a forged state is rejected 400; a junk cookie is 401; a valid session for a non-allowlisted user is 401; all headers present; and the UI renders with no CSP violations in the console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
10ccc282d4
commit
bc46fd80b3
6 files changed
+486
-187
No files matched your search
@@ -105,6 +105,59 @@ can actually serve you (free upload slot, short queue), then filename
|
||||
plausibility, then bitrate. A marginally better file behind 200 queued users
|
||||
loses to a good-enough one available now.
|
||||
|
||||
## Authentication
|
||||
|
||||
Single-user GitHub OAuth, built so this can face the internet.
|
||||
|
||||
**It fails closed.** Auth is on unless `MUSE_AUTH=off` is set explicitly, and
|
||||
if it is on with incomplete config the app *refuses to start*. The failure mode
|
||||
worth engineering against isn't a broken login page — it's a misconfigured
|
||||
deploy quietly serving an unauthenticated download client to the internet.
|
||||
|
||||
### Setting it up
|
||||
|
||||
1. <https://github.com/settings/developers> → **New OAuth App**
|
||||
- Homepage URL: `https://muse.yourdomain.com`
|
||||
- Authorization callback URL: `https://muse.yourdomain.com/auth/callback`
|
||||
2. Generate a client secret.
|
||||
3. Set `MUSE_GITHUB_CLIENT_ID`, `MUSE_GITHUB_CLIENT_SECRET`,
|
||||
`MUSE_ALLOWED_USERS` (your GitHub login), and `MUSE_BASE_URL`.
|
||||
|
||||
| variable | default | meaning |
|
||||
|---|---|---|
|
||||
| `MUSE_AUTH` | `github` | `github` or `off` |
|
||||
| `MUSE_GITHUB_CLIENT_ID` | — | OAuth app client id |
|
||||
| `MUSE_GITHUB_CLIENT_SECRET` | — | OAuth app secret |
|
||||
| `MUSE_ALLOWED_USERS` | — | comma-separated GitHub logins; **nobody else gets in** |
|
||||
| `MUSE_BASE_URL` | — | external URL, e.g. `https://muse.yourdomain.com` |
|
||||
| `MUSE_COOKIE_SECURE` | `true` | set `false` only for plain-http LAN testing |
|
||||
| `MUSE_SESSION_DAYS` | `7` | session lifetime |
|
||||
|
||||
### What it does
|
||||
|
||||
- **Server-side sessions.** The cookie carries a 256-bit random token and
|
||||
nothing else; the database stores only its SHA-256. No signed payload to get
|
||||
wrong, a stolen database yields no usable cookies, and logout genuinely
|
||||
revokes. Cookie is `HttpOnly`, `Secure`, `SameSite=Lax`.
|
||||
- **Allowlist re-checked every request**, so removing someone locks them out
|
||||
immediately rather than whenever their session expires.
|
||||
- **OAuth `state`** is generated per login and compared with
|
||||
`secrets.compare_digest`, so a callback you didn't initiate is rejected.
|
||||
- **Strict CSP** — `default-src 'self'`, no `unsafe-inline`. This is why the
|
||||
stylesheet and script are separate files instead of inline in the page: an
|
||||
injected `<script>` cannot execute. Plus `frame-ancestors 'none'`,
|
||||
`base-uri 'none'`, `nosniff`, `X-Frame-Options: DENY`, `no-referrer`, HSTS.
|
||||
- **No API docs** — `/docs`, `/redoc` and `/openapi.json` are disabled, so the
|
||||
login page doesn't hand out a map of every endpoint.
|
||||
- **Denied logins are logged** with the GitHub username — on an
|
||||
internet-facing box that's your signal someone found the page.
|
||||
- `/healthz` is the only unauthenticated route and returns a bare `{"ok":true}`
|
||||
— no versions, no config, no counts.
|
||||
|
||||
Behind a reverse proxy, `MUSE_BASE_URL` is read from config rather than from
|
||||
`X-Forwarded-*` headers on purpose: those are attacker-controllable and this
|
||||
one is a redirect target.
|
||||
|
||||
## Running it
|
||||
|
||||
```yaml
|
||||
|
||||
Reference in new issue
Block a user