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:
44
docs/AUTH.md
44
docs/AUTH.md
@@ -77,6 +77,37 @@ and this server cannot end each other.
|
||||
With no tab attached, the keepalive holds the session to the 30-day hard expiry,
|
||||
and replacing the token becomes the monthly chore it looked like at first.
|
||||
|
||||
## Replacing the token without a restart
|
||||
|
||||
`schulcloud token set` (paste at a hidden prompt, or pipe it in) and the
|
||||
server's `/token` page both send a fresh token to `PUT /api/token`, behind the
|
||||
bearer check. `core/session-token.ts` then:
|
||||
|
||||
1. **Cleans the paste.** A bare value, `jwt=…; Path=/`, quotes and newlines all
|
||||
work.
|
||||
2. **Checks before it swaps.** A malformed or expired token is refused without
|
||||
asking Schulcloud; otherwise `GET /api/v3/me` must succeed *with the new
|
||||
token*, and for the same `userId` the current one carries. A refused token
|
||||
changes nothing. Switching accounts stays a deliberate act — change
|
||||
`TSC_JWT_COOKIE` and restart.
|
||||
3. **Swaps it in place.** Every request reads `config.jwt` at the moment it is
|
||||
sent, so the next one uses the new token; nothing caches a copy.
|
||||
4. **Restarts the keepalive**, which stopped for good on a 401. Its pings carry
|
||||
a generation number, so one still in flight with the old token cannot stop
|
||||
the new cycle when its 401 arrives.
|
||||
5. **Saves it** to `STATE_DIR` (0600, written beside itself and renamed). At
|
||||
startup the newer of the saved token and `TSC_JWT_COOKIE` wins, by `exp` —
|
||||
unless they belong to different accounts, when the environment does.
|
||||
|
||||
The token appears in no log line and no response; `/api/token` reports only
|
||||
the expiry, where the token came from, and the keepalive's state. The claims
|
||||
are decoded, never verified — Schulcloud verifies, this only reads dates.
|
||||
|
||||
The cookie is **HttpOnly**, so no script — no bookmarklet, no page on another
|
||||
origin — can read it out of the browser. The DevTools copy is the one manual
|
||||
step, and it cannot be automated away short of a headless browser holding the
|
||||
Schulportal password (see below).
|
||||
|
||||
### There is no longer window available
|
||||
|
||||
`config/default.schema.json` documents `JWT_EXTENDED_TIMEOUT_SECONDS`
|
||||
@@ -145,9 +176,9 @@ approach is the right trade: one manual step a month against re-implementing an
|
||||
OAuth client whose secret we cannot hold. If that monthly step ever becomes
|
||||
unacceptable, the honest options are a service account issued by the school's
|
||||
IDM, or driving the Keycloak login with a headless browser — not a
|
||||
reimplementation of the code exchange. If this ever needs to be unattended, the honest options are a service
|
||||
account issued by the school's IDM, or a headless browser login — not a
|
||||
reimplementation of the Keycloak exchange.
|
||||
reimplementation of the code exchange. A headless browser would have to hold
|
||||
the Schulportal password, which unlocks far more than this account's school
|
||||
files, so it is not done here.
|
||||
|
||||
## Protecting this server's own endpoint
|
||||
|
||||
@@ -170,5 +201,8 @@ Every path in this server is a `GET`, including the `api_get` escape hatch,
|
||||
which rejects anything not starting with `/api/` and anything carrying a scheme
|
||||
or host. Someone who obtained both the endpoint URL and `MCP_AUTH_TOKEN` could
|
||||
read this account's Schulcloud data; they could not post, submit, delete, or
|
||||
otherwise act as the user. Keep it that way — adding a single write tool would
|
||||
change that property entirely.
|
||||
otherwise act as the user. With `MCP_AUTH_TOKEN` they
|
||||
could also call `PUT /api/token`, but it accepts only a live token for the same
|
||||
account, so the most it can do is hand the server a session the owner already
|
||||
has. Keep it that way — adding a single write tool would change that property
|
||||
entirely.
|
||||
|
||||
Reference in New Issue
Block a user