Replace the Schulcloud token without a restart
A token lasts 30 days and only a browser login yields one — the account is federated, so the server cannot mint it. Replacing it meant editing .env and recreating the container, every month. `schulcloud token set` (a hidden prompt, or piped input) and a /token page both send it to PUT /api/token. The server checks it with Schulcloud first — well-formed, unexpired, still logged in, the same account — then swaps it into the config every request reads, restarts the keepalive and saves it in STATE_DIR, a new volume, with mode 0600. At startup the newer of the saved token and TSC_JWT_COOKIE wins, unless they belong to different accounts. A refused paste changes nothing, and the token is never logged. The keepalive's pings carry a generation, so a 401 for the old token that arrives after a swap cannot stop the new cycle. `schulcloud token`, whoami and the log report the expiry and warn a week ahead. Found on the way: a host that is off for more than two hours loses the session however long the token has left — this machine lost it overnight — which is what the always-on Pi is for. 174 tests. Smoke 72/72 on the local instance, and a real swap verified end to end there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,19 @@ schulcloud CLI ─────┘ └─ Caddy
|
||||
└──▶ schulcloud-thueringen.de
|
||||
```
|
||||
|
||||
Both front ends use the same hostname and the same bearer token. `/mcp` speaks
|
||||
MCP; `/api` serves the CLI's manifest, file bytes and re-crawl requests.
|
||||
Both front ends use the same hostname and the same bearer token. `/mcp` speaks MCP; `/api` serves the
|
||||
CLI's manifest, file bytes, re-crawl requests and token replacement; `/token` is
|
||||
a page for pasting a fresh Schulcloud token.
|
||||
|
||||
Claude's custom connectors call the endpoint from Anthropic's cloud, so it must
|
||||
be publicly reachable over real TLS — a localhost tunnel or self-signed cert
|
||||
will not do. The VPS provides the public address; Caddy on the Pi terminates
|
||||
TLS and obtains the certificate.
|
||||
Claude's custom connectors call the endpoint from Anthropic's cloud
|
||||
(`160.79.104.0/21`), so it must be publicly reachable over real TLS — a
|
||||
localhost tunnel or self-signed cert will not do. The VPS provides the public
|
||||
address; Caddy on the Pi terminates TLS and obtains the certificate.
|
||||
|
||||
**The Pi must stay up.** More than two hours offline ends the Schulcloud session
|
||||
however long the token has left — a laptop that sleeps overnight loses it every
|
||||
night. A replacement token fixes that without a restart (see *Replacing the
|
||||
Schulcloud token*), but an always-on host is what avoids needing one.
|
||||
|
||||
The container publishes no host port. Caddy reaches it over the shared Docker
|
||||
network, so the only way in from the internet is through Caddy and then through
|
||||
@@ -140,6 +146,26 @@ newline from a copy-paste.
|
||||
Ask *"which courses am I in?"* as a first check — that exercises auth, the
|
||||
Schulcloud token and the API in one call.
|
||||
|
||||
## Replacing the Schulcloud token
|
||||
|
||||
The token lasts 30 days at most and can only come from a browser login (see
|
||||
docs/AUTH.md), so this is the monthly chore — but it no longer needs a restart
|
||||
or an `.env` edit:
|
||||
|
||||
1. Log in to Schulcloud in a **private window** and copy the `jwt` cookie's
|
||||
value (DevTools → Application → Cookies).
|
||||
2. Either run `schulcloud token set` and paste it, or open
|
||||
`https://mcp.example.org/token` and paste it together with `MCP_AUTH_TOKEN`.
|
||||
3. **Close the private window.**
|
||||
|
||||
The server checks the token with Schulcloud first — right account, not
|
||||
expired, not logged out — then swaps it in, restarts the keepalive and saves it
|
||||
to the `state` volume (`STATE_DIR=/data/state`), so a restart keeps it. At
|
||||
startup the newer of the saved token and `TSC_JWT_COOKIE` wins, unless the two
|
||||
are for different accounts: changing `TSC_JWT_COOKIE` is how you switch
|
||||
accounts. `schulcloud token` shows the days left; from a week before expiry the
|
||||
log, `whoami` and the CLI warn about it.
|
||||
|
||||
## Running it locally instead
|
||||
|
||||
For Claude Code or Claude Desktop on your own machine, skip all of the above and
|
||||
@@ -179,9 +205,10 @@ npm run probe # re-verify the API assumptions
|
||||
invalidates them; Claude re-initializes transparently.
|
||||
- **Logs** are capped at 3 × 10 MB. The Authorization header is never logged.
|
||||
- **The container is read-only** with `cap_drop: ALL` and `no-new-privileges`,
|
||||
running as the unprivileged `node` user. The one writable path is the mirror
|
||||
volume at `/data/mirror`, which holds downloaded file bytes; everything else
|
||||
stays read-only.
|
||||
running as the unprivileged `node` user. The writable paths are the mirror
|
||||
volume at `/data/mirror`, which holds downloaded file bytes, and the state
|
||||
volume at `/data/state`, which holds a replaced Schulcloud token (mode 0600).
|
||||
Both belong in no backup that leaves the Pi unencrypted.
|
||||
- **The mirror grows.** It holds a copy of every course file under
|
||||
`MIRROR_MAX_BYTES` (64 MiB default). Larger files — videos, mostly — are
|
||||
indexed as metadata and proxied live on request instead. Budget a few GB.
|
||||
@@ -196,6 +223,8 @@ npm run probe # re-verify the API assumptions
|
||||
the session and its auto-logout will revoke it ~2h after login. Copy the
|
||||
cookie in a private window and close it — see docs/AUTH.md.
|
||||
- **Downtime longer than two hours lapses the session** and restarting does not
|
||||
recover it: a long power cut means pasting a fresh `TSC_JWT_COOKIE`.
|
||||
- **Monthly chore**: refresh `TSC_JWT_COOKIE` before its 30-day hard expiry.
|
||||
`npm run probe` reports both clocks.
|
||||
recover it: a long power cut means handing the server a fresh token
|
||||
(`schulcloud token set`), no restart needed.
|
||||
- **Monthly chore**: replace the token before its 30-day hard expiry — see
|
||||
*Replacing the Schulcloud token*. `schulcloud token` and `npm run probe`
|
||||
report the clocks.
|
||||
|
||||
Reference in New Issue
Block a user