feat: require a write token or session on mutating portal API routes

- Adds IPFS_WRITE_TOKEN to lib/config.ts
- Default-deny per route (not per method): the public explorer issues reads as POST
- Existing portal session JWT remains valid, so the logged-in UI keeps working
- Public gateway reads (/ipfs/<cid>) are unchanged
This commit is contained in:
maikrolf
2026-09-30 21:10:21 +02:00
parent d0632d4dcc
commit 88f2c7e5aa
6 changed files with 520 additions and 0 deletions
+52
View File
@@ -222,6 +222,58 @@ Catch-all API proxy. Routes:
---
## Write authentication
Muterende `/api/*`-endpoints eisen een token; lees-endpoints blijven publiek.
Ingang: `authorizeWrite()` in `src/app/api/[...path]/write-auth.ts`, aangeroepen
vanuit `POST` en `DELETE` in `route.ts`. De waarde komt uit
`ipfsWriteToken()` in `src/lib/config.ts` (env `IPFS_WRITE_TOKEN`).
| Endpoint | Methode | Write-gate |
|---|---|---|
| `/api/add` | POST | ✅ |
| `/api/files/upload` | POST | ✅ |
| `/api/files/<cid>` | DELETE | ✅ |
| `/api/pins` | POST | ✅ |
| `/api/pins/<cid>` | DELETE | ✅ |
| `/api/ipns/publish`, `/api/ipns/keys/gen/<naam>` | POST | ✅ |
| `/api/users/<naam>` | DELETE | ✅ |
| `/api/explorer/{ls,cat,stat,resolve}/<cid>` | POST | ❌ lees-only |
| `/api/pins`, `/api/files`, `/api/explorer/*`, `/api/health` | GET | ❌ publiek |
| `/ipfs/<cid>` (gateway) | GET | ❌ publiek |
| `/api/users` (registratie) | POST | ❌ open — eigen auth van Python-backend |
**Toegang tot een mutatie — één van beide volstaat:**
1. `Authorization: Bearer $IPFS_WRITE_TOKEN` (MAOS → portal, server-to-server).
Vergelijking is constant-time via SHA-256 digests (`crypto.timingSafeEqual`).
2. De bestaande portalsessie-JWT cookie `ipfs-portal-session` — identieke
verificatie als `src/middleware.ts`. Zónder dit pad zouden de eigen
upload-/pin-pagina's van de portal kapotgaan.
Geen van beide → `401` (geen `Authorization`-header) of `403` (header aanwezig,
verkeerde waarde), met `WWW-Authenticate: Bearer`.
**`IPFS_WRITE_TOKEN` niet geconfigureerd** → writes blijven **open** met een
eenmalige luide waarschuwing in de log. `src/lib/config.ts` heeft geen
"required in production"-patroon (alleen defaults, in tegenstelling tot
`JWT_SECRET` in `auth-server.ts`), en een harde fail zou elke nog niet
geprovisioneerde deploy direct breken — vandaar bewust fail-open + waarschuwing.
**Besluit — publieke lees-endpoints**: `GET /api/pins` en `GET /api/files`
blijven anoniem toegankelijk. Ze lekken alleen metadata (CID's, namen, maten);
content is sowieso publiek via de gateway. Ze achter de sessie zetten zou de
publieke explorer (pin-badges) én MAOS' pin-status/inventory-reads breken — die
sturen vandaag geen `Authorization`-header op GET. Zodra MAOS ook GET's met
Bearer verstuurt kan dit alsnog achter de gate.
**Rest-risico**: `POST /api/users` (registratie én admin-aanmaak, zelfde
endpoint) blijft open — een registrant heeft noch token noch sessie. De
Python-backend is daar de autoriteit (`X-Admin-Key` + doorgestuurd
`X-Session-Token`).
---
## Smart Contract (Chain 270 — zkSync Local)
**Address**: `0xCBc6b8aeea129c206F4836799621C833Bf8B9BDe`