PythonServer JWT Authentication

This document captures Phase 0 groundwork for securing the PythonServer REST API with internal JWT bearer tokens issued by ThalabusCore.

Token Contract

ThalabusCore is the only issuer of PythonServer tokens. PythonServer validates tokens locally and derives request identity only from validated claims.

Claim Value
iss thalabus
aud pythonserver
sub Local Thalabus user UUID for user-session tokens, or thalabus-core for service tokens
tenant_id Authenticated tenant identifier
session_id Thalabus session identifier for session-bound routes
username Normalized Thalabus username when required by legacy flows
scope user-session or service-internal
iat Issued-at timestamp
exp Short expiration timestamp
jti Unique token identifier

Shared constants are defined in:

Dependencies

The C# issuer uses:

The Python validator uses:

Configuration

Use a dedicated signing keypair. Do not reuse PLATFORM_API_TOKEN, NEO4J_PASSWORD, or any user password.

Variable Consumer Purpose
THALABUS_PYTHON_API_JWT_ALGORITHM ThalabusCore, PythonServer Signing/validation algorithm. Phase 0 default: RS256
THALABUS_PYTHON_API_JWT_ISSUER ThalabusCore, PythonServer Expected issuer. Phase 0 default: thalabus
THALABUS_PYTHON_API_JWT_AUDIENCE ThalabusCore, PythonServer Expected audience. Phase 0 default: pythonserver
THALABUS_PYTHON_API_JWT_PRIVATE_KEY_PATH ThalabusCore only Path to the private signing key
THALABUS_PYTHON_API_JWT_PUBLIC_KEY_PATH PythonServer only Path to the public verification key
THALABUS_PYTHON_API_JWT_TOKEN_LIFETIME_MINUTES ThalabusCore User-session and service token lifetime in minutes. Default: 10

Issuer Service

Phase 1 adds the ThalabusCore token issuer behind IPythonApiTokenService:

string IssueUserSessionToken(string userUuid, string username, string sessionId, string tenantId);
string IssueServiceToken(string scope);

PythonApiTokenService signs RS256 tokens from THALABUS_PYTHON_API_JWT_PRIVATE_KEY_PATH, applies iss, aud, iat, exp, and jti, and separates user-session tokens from internal service tokens.

Key Generation Procedure

Generate key material outside the repository and store it in the deployment secret store or a mounted secret volume.

PowerShell with OpenSSL:

New-Item -ItemType Directory -Force -Path C:\Secrets\thalabus | Out-Null
openssl genrsa -out C:\Secrets\thalabus\python-api-jwt-private.pem 3072
openssl rsa -in C:\Secrets\thalabus\python-api-jwt-private.pem -pubout -out C:\Secrets\thalabus\python-api-jwt-public.pem

Configure ThalabusCore with only the private key path:

$env:THALABUS_PYTHON_API_JWT_PRIVATE_KEY_PATH = "C:\Secrets\thalabus\python-api-jwt-private.pem"

Configure PythonServer with only the public key path:

$env:THALABUS_PYTHON_API_JWT_PUBLIC_KEY_PATH = "C:\Secrets\thalabus\python-api-jwt-public.pem"

Never commit generated .pem files or print their contents to logs.

Key Rotation

Rotate the PythonServer JWT keypair as an operational secret, not as source code.

Recommended rotation flow:

  1. Generate a new keypair outside the repository.
  2. Deploy the new public key to PythonServer as THALABUS_PYTHON_API_JWT_PUBLIC_KEY_PATH.
  3. Deploy the matching private key to ThalabusCore as THALABUS_PYTHON_API_JWT_PRIVATE_KEY_PATH.
  4. Restart or reload services in a coordinated release window.
  5. Keep the previous public key available only if Phase 3 validation is extended to support multiple verification keys during overlap.
  6. Remove the old private key immediately after cutover and remove the old public key after the maximum token lifetime has elapsed.

Current Phase 3 validation accepts one configured public key path. Multi-key overlap and kid-based key selection are not implemented yet.

Token Lifetime

THALABUS_PYTHON_API_JWT_TOKEN_LIFETIME_MINUTES controls the lifetime of both user-session and service-internal tokens issued by ThalabusCore.

The default is 10 minutes. Keep this value short because PythonServer validates JWTs locally and does not perform token introspection against ThalabusWeb on every request.

If emergency revocation is required before expiration, either rotate the signing key or add a future jti denylist/revocation store.

Token Systems

Thalabus has two distinct bearer-token systems:

Token Issuer Consumer Purpose
/settingsapi session token (Session.ApiToken) ThalabusWeb session UI / C# session creation API External C# REST clients Existing opaque token for v1/sessions/... REST compatibility
PythonServer internal JWT ThalabusCore PythonApiTokenService PythonServer Flask API Internal service-to-service authentication and request identity for PythonServer

Do not use Session.ApiToken as a PythonServer identity token. Do not use PLATFORM_API_TOKEN as a PythonServer user-session token.

Route Classification

Every new PythonServer Flask route must be added to exactly one route classification list in PythonServer/src/api/auth/jwt_middleware.py:

The middleware is deny-by-default: any non-public route missing from both USER_SESSION_ROUTES and SERVICE_ROUTES is rejected with 403 Forbidden, even when the caller sends a valid JWT.

The authoritative route classification is the code in PythonServer/src/api/auth/jwt_middleware.py. Do not duplicate the route list in documentation; update the middleware constants directly when adding or changing Flask routes.

Phase 1 Handoff

Phase 1 should implement token issuance in ThalabusCore using the constants and configuration above. Phase 3 should implement PythonServer middleware using the same constants and the route classification constants in jwt_middleware.py as the explicit allowlist/scope map.

Outbound Request Propagation

Phase 2 adds PythonApiHttpClientFactory for ThalabusCore-to-PythonServer calls. Use this helper instead of constructing unauthenticated HttpClient instances in container code.

Helper Token type Intended routes
CreateUserSessionClient(aiProgram, timeout) scope=user-session Session-bound routes such as skill start, input, progress, logs, document load/unload, dossier, pause, llmconfig, flavour, reasoning, and RAG settings
CreateServiceClient(timeout) scope=service-internal Internal routes such as warm-up imports, utility maintenance, plan configuration, source-file reads, health pings, and container stop

Session.ApiToken remains the external C# REST session token used by /settingsapi; it is separate from the internal JWT used between ThalabusCore and PythonServer.

Browser-loaded skill widgets do not receive PythonServer JWTs. Their iframe, parameter, and skill-execution requests remain same-origin under the ThalabusWeb /proxy routes. ProxyController authenticates the internal hop to PythonServer with a service token for skill HTML and parameter routes, and a user-session token for skill execution routes. Do not place internal JWTs in iframe URLs, query strings, or browser JavaScript.

PythonServer Middleware

Phase 3 adds PythonServer/src/api/auth/jwt_middleware.py and registers it from PythonServer/src/api/app.py with @app.before_request.

The middleware:

Phase 4 removes trust in body-supplied user_id and user_uuid from /session/<session_id> and /main_session/start/<session_id>.

Python identity resolution now uses UserService as a request-aware facade:

/session/<session_id> requires a validated user-session token and rejects requests whose path session ID does not match the token's session_id claim with 403.

Route Scope Enforcement

Phase 5 introduces middleware route classification for public, user-session, and service-internal routes.

Known follow-up hardening before production closure of SEC-01/SEC-02:

Rollout

For zero-downtime rollout, deploy in two stages:

  1. Deploy Phase 3 middleware in log-only mode: validate incoming bearer tokens and log missing/invalid tokens without rejecting requests.
  2. Confirm Phase 2 propagation from ThalabusCore is live for every protected PythonServer route classified in jwt_middleware.py.
  3. Flip PythonServer middleware to enforcing mode.
  4. Keep token lifetime short during the first rollout window to limit replay risk.

If zero-downtime rollout is not required, ship the coordinated ThalabusCore and PythonServer changes together because both services are versioned in the same deployment stack. Phase 6 port hardening should be part of the production release: the default Docker Compose configuration should not expose PythonServer port 5100 publicly.