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:
ThalabusInterfaces/Data/Security/PythonApiTokenClaims.csPythonServer/src/api/auth/jwt_claims.py
Dependencies
The C# issuer uses:
Microsoft.IdentityModel.TokensSystem.IdentityModel.Tokens.Jwt
The Python validator uses:
PyJWT[crypto]
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:
- Generate a new keypair outside the repository.
- Deploy the new public key to PythonServer as
THALABUS_PYTHON_API_JWT_PUBLIC_KEY_PATH. - Deploy the matching private key to ThalabusCore as
THALABUS_PYTHON_API_JWT_PRIVATE_KEY_PATH. - Restart or reload services in a coordinated release window.
- Keep the previous public key available only if Phase 3 validation is extended to support multiple verification keys during overlap.
- 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:
PUBLIC_ROUTESonly for intentionally unauthenticated health/static routes.USER_SESSION_ROUTESfor routes that act on behalf of an authenticated user/session.SERVICE_ROUTESfor internal ThalabusCore/PythonServer maintenance and infrastructure routes.
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:
- Allows only the explicit public route allowlist without a token.
- Requires
Authorization: Bearer <jwt>for every other route. - Rejects missing, malformed, expired, wrongly issued, wrongly targeted, invalid-signature, and
alg=nonetokens with a generic401response. - Validates
iss,aud,exp,iat,jti, and the configured algorithm allowlist. - Populates request-local Flask context (
g.user_uuid,g.session_id,g.tenant_id,g.token_scope) only from validated claims.
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:
- During Flask requests,
get_user_uuid(),get_user_id(), andget_session_id()read validated JWT values fromflask.g. - Outside Flask requests, explicit background/test context can still be set with
set_user_uuid(),set_user_id(), andset_session_id(). - The service no longer reads or writes
USER_UUID,USER_ID, orSESSION_IDinos.environ.
/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:
- Reject authenticated requests to non-public routes that are missing from both route classification tables.
- Redact
Authorizationand other secret-bearing headers from PythonServer request logs.
Rollout
For zero-downtime rollout, deploy in two stages:
- Deploy Phase 3 middleware in log-only mode: validate incoming bearer tokens and log missing/invalid tokens without rejecting requests.
- Confirm Phase 2 propagation from ThalabusCore is live for every protected PythonServer route classified in
jwt_middleware.py. - Flip PythonServer middleware to enforcing mode.
- 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.