Innebygd innlogging (AUTH_MODE=app) med neon-login og proxy-bypass

Valgfri app-auth ved siden av proxy-modus (default uendret). Signert
sesjons-cookie (scrypt-passord, HMAC, ingen avhengigheter), gating av API +
WebSocket, og en selvstendig neon-login (public/login.html) for uautentiserte.

Trusted bypass: en proxy-identitet (X-Remote-User / Basic Auth) regnes som
innlogget uten app-passord (TRUST_PROXY_HEADER, default på), så man kan
bootstrappe: logg inn via NPM Basic Auth → sett app-passord i konto-modalen →
skru av Basic Auth. Konto-UI for sett/endre passord + logg ut.

AUTH_MODE leses per createServer-instans (testbart). 34 integrasjonstester
(8 nye for app-auth), alle grønne. README/CLAUDE.md/setup-skript oppdatert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-15 14:27:13 +02:00
parent 7287f33e47
commit f6268440d9
9 changed files with 711 additions and 42 deletions

View file

@ -29,13 +29,20 @@ npm test
|---|---|---|
| `PORT` | `3000` | HTTP-port |
| `HOST` | `0.0.0.0` | Lytteadresse |
| `DATA_DIR` | `./data` | Katalog for tasks.json / activity.json / users.json |
| `DISABLE_COOKIE_AUTH` | (av) | Sett til `1` for å skru av selvvalgt navn via cookie — da gjelder kun proxy-headere |
| `DATA_DIR` | `./data` | Datakatalog (tasks/activity/users/plans/categories/auth) |
| `AUTH_MODE` | `proxy` | `proxy` = stol på proxy-header/Basic Auth. `app` = innebygd innlogging (sesjons-cookie + passord) |
| `TRUST_PROXY_HEADER` | `1` | I app-modus: la en proxy-identitet (X-Remote-User / Basic) slippe inn uten app-passord (for bootstrapping). Sett `0` for å slå av |
| `SESSION_TTL_DAYS` | `30` | Levetid på app-sesjons-cookien |
| `AUTH_SECRET` | (auto) | HMAC-nøkkel for sesjons-cookies. Settes ikke den, genereres og lagres i `data/auth.secret` |
| `DISABLE_COOKIE_AUTH` | (av) | Proxy-modus: `1` skrur av selvvalgt navn via cookie — da gjelder kun proxy-headere |
## Autentisering
Påloggingslogikk er bevisst holdt utenfor appen. Brukernavn hentes fra,
i prioritert rekkefølge:
To modi, styrt av `AUTH_MODE`.
### `proxy` (default) — auth utenfor appen
Brukernavn hentes, i prioritert rekkefølge:
1. `X-Remote-User`-header — satt av reverse proxy (oauth2-proxy, Authelia, …)
2. `Authorization: Basic` — Basic Auth terminert i proxy, brukernavnet gjenbrukes
@ -44,6 +51,29 @@ i prioritert rekkefølge:
For team-bruk bak proxy: sett `DISABLE_COOKIE_AUTH=1` så ingen kan velge navn selv.
### `app` — innebygd innlogging
Egen neon-login (`/login`) med passord. Sesjon er en signert, langlevd cookie
(`SESSION_TTL_DAYS`, default 30) — ingen repeterte Basic Auth-prompts, og virker
sømløst med WebSocket. Passord hashes med scrypt (Node-kjerne, ingen
avhengigheter), lagret i `data/auth.json`.
**Trusted bypass / migrering fra Basic Auth:** Når `TRUST_PROXY_HEADER=1`
(default) regnes en proxy-identitet (X-Remote-User eller Basic Auth) som
innlogget, *selv uten app-passord*. Det gir en trygg overgang:
1. Behold NPM Access List (Basic Auth) og deploy med `AUTH_MODE=app`.
2. Åpne appen (du slipper inn via Basic Auth-bypass), klikk brukernavnet ditt
oppe til høyre → **sett et app-passord**. Gjenta for hver kollega.
3. Fjern NPM Access List. Nå møtes alle av neon-login og logger inn med passordet.
> Bypass krever at app-porten kun er nåbar fra proxyen (samme antakelse som
> proxy-modus). Vil du være streng, sett `TRUST_PROXY_HEADER=0` etter at alle
> har satt passord — da gjelder kun app-sesjoner.
Oppsett som tjeneste: legg `Environment=AUTH_MODE=app` i systemd-enheten (se
under), eller kjør skriptet med `AUTH_MODE=app bash setup-debian13.sh`.
## Rask installasjon på Debian 13 (LXC/VM)
```bash
@ -74,11 +104,15 @@ frontend bruker absolutte stier, så sti-prefiks (`proxy.lan/todo/`) fungerer ik
- *Block Common Exploits:* valgfritt, fungerer fint sammen med appen
2. **SSL-fanen:** *Request a new SSL Certificate* (Let's Encrypt) + *Force SSL*.
Frontend bytter selv til `wss:` når siden er https.
3. **Auth (valgfritt):** *Access Lists* → lag liste med brukere (Basic Auth) og
knytt den til proxy-hosten. NPM/nginx sender `Authorization`-headeren videre,
og appen bruker brukernavnet derfra — da får hver kollega riktig attribusjon.
Sett i så fall `DISABLE_COOKIE_AUTH=1` på tjenesten. Uten access list velger
folk navn selv i UI-et (cookie).
3. **Auth — to alternativer:**
- *Innebygd app-innlogging (anbefalt — slipper repeterte Basic Auth-prompts):*
kjør tjenesten med `AUTH_MODE=app`. Bruk gjerne NPM Access List midlertidig
for å bootstrappe (se «app»-seksjonen over), og fjern den når alle har satt
passord. Da trenger ikke NPM Access List i det hele tatt.
- *Proxy Basic Auth:* *Access Lists* → lag brukere og knytt til proxy-hosten.
NPM sender `Authorization`-headeren videre og appen bruker brukernavnet
derfra. Sett `DISABLE_COOKIE_AUTH=1`. (Ulempe: nettleseren spør om passord
på nytt ved hver ny økt — derav app-innlogging som alternativ.)
**nginx:**
@ -122,7 +156,9 @@ Environment=PORT=3000
# container/host: fjern linjen (lytt på alt) og brannmur port 3000 til kun proxyen.
Environment=HOST=127.0.0.1
Environment=DATA_DIR=/var/lib/netops-todo
Environment=DISABLE_COOKIE_AUTH=1
# Innebygd innlogging (anbefalt). Bytt til proxy-modus ved å fjerne denne
# og evt. sette DISABLE_COOKIE_AUTH=1.
Environment=AUTH_MODE=app
Restart=on-failure
[Install]
@ -166,7 +202,11 @@ optimistisk låsing — send med ved update, få 409 hvis noen andre har lagret.
| Metode | Endepunkt | Beskrivelse |
|---|---|---|
| GET | `/api/state` | Bruker, oppgaver, aktivitet, kjente brukere, online, kategorier, dagens plan, planIndex |
| GET | `/api/state` | Bruker, auth-status, oppgaver, aktivitet, kjente brukere, online, kategorier, dagens plan, planIndex |
| GET | `/api/auth/status` | Auth-modus, innlogget?, bruker, via, har passord |
| POST | `/api/auth/login` | App-innlogging `{ user, password }` → sesjons-cookie |
| POST | `/api/auth/logout` | Tøm sesjons-cookie |
| POST | `/api/auth/password` | Sett/endre eget passord `{ password, current? }` |
| POST | `/api/tasks` | Opprett oppgave (`title` påkrevd) |
| PUT | `/api/tasks/:id` | Oppdater (delvis OK; send `v` for konfliktsjekk; valgfri `comment` festes som status-kommentar) |
| DELETE | `/api/tasks/:id` | Slett |