Files
Schulcloud-MCP/.env.example
MechaCat02 d657ece436 Fix session lifetime: 2h sliding idle timeout, not 30 days
The JWT's exp claim says 30 days, and I took that as the session
lifetime. It is only an outer ceiling. The server also keeps a per-token
whitelist entry in Valkey (jwt:{accountId}:{jti}) whose TTL is
JWT_TIMEOUT_SECONDS — 7200s on this instance — and JwtStrategy.validate
re-sets it on every authenticated request. Two hours idle and the token
is rejected with 29 days still on exp.

Proven, not inferred: the token from yesterday returned 401 at 13.8h old.
The live instance publishes the values unauthenticated at
GET /api/v3/config/public — JWT_TIMEOUT_SECONDS 7200,
JWT_SHOW_TIMEOUT_WARNING_SECONDS 3600, the latter being exactly the
one-hour UI prompt that prompted this investigation.

refresh-session turns out not to be special: it extends through the same
guard as any other route, and uniquely only in returning the remaining
TTL. So the keepalive uses GET /api/v3/me instead, and the server stays
GET-only; the one POST in the repo is in scripts/probe.mjs, where it
reports the idle budget.

JWT_EXTENDED_TIMEOUT_SECONDS (~1 month) exists in the config schema but
is vestigial: privateDevice has no references in the current NestJS
source, and generateJwtAndAddToWhitelist never overrides the TTL.

Also fixes a real breakage this surfaced: TypeScript parameter
properties are rejected by Node's type stripping, so `npm run dev` and
`npm test` both failed on any file reaching them. Rewritten as explicit
fields, and noted in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:07:22 +02:00

47 lines
2.0 KiB
Plaintext

# ---------------------------------------------------------------------------
# Schulcloud instance
# ---------------------------------------------------------------------------
# Base URL of the instance, no trailing slash.
TSC_URL=https://schulcloud-thueringen.de
# The value of the `jwt` cookie from a logged-in browser session.
# Two clocks apply: a 30-day hard expiry, and a 2-hour idle timeout that every
# API call resets. The built-in keepalive handles the second one, so in practice
# this needs replacing monthly. See docs/AUTH.md.
TSC_JWT_COOKIE=
# ---------------------------------------------------------------------------
# This MCP server
# ---------------------------------------------------------------------------
# Shared secret callers must present as `Authorization: Bearer <token>`.
# REQUIRED for the public deployment — without it the endpoint is open to
# anyone who finds the hostname. Generate one with:
# openssl rand -hex 32
MCP_AUTH_TOKEN=
# Listen address inside the container. Leave as-is when running behind Caddy.
PORT=8080
BIND_HOST=0.0.0.0
# ---------------------------------------------------------------------------
# Limits (optional — sensible defaults are built in)
# ---------------------------------------------------------------------------
# Largest file download_file will pull, in bytes. Default 25 MiB.
# Videos in Schulcloud routinely exceed this; they are not extractable anyway.
# MAX_DOWNLOAD_BYTES=26214400
# Characters of extracted text returned before truncation. Default 120000.
# MAX_EXTRACTED_CHARS=120000
# Per-request timeout against the Schulcloud API, in ms. Default 30000.
# REQUEST_TIMEOUT_MS=30000
# How often to ping Schulcloud to hold the session open, in ms. Default 1800000
# (30 min). Must stay well under the instance's JWT_TIMEOUT_SECONDS — 7200s
# here, readable from GET /api/v3/config/public. Set to 0 to disable, which
# will let the token die after two hours of inactivity.
# KEEPALIVE_INTERVAL_MS=1800000