Skip to content

Session is not reused across invocations — every ct call re-runs the login handshake and trips CT's 429 rate limit #145

Description

@2000game

What happens

Every ct invocation runs the full login handshake against ChurchTools. A short
series of read-only calls therefore trips CT's login rate limit and every
subsequent command fails before it reaches its actual endpoint:

$ ct get raw "/persons/3033/groups?limit=200" --env prod
✗ Login failed (whoami) (HTTP 429)
{ "message": "Too many requests", ... }

Three ct get raw calls in a row were enough to trigger it, and it was still
tripped after a 45s pause. The 429 comes from the handshake, not from the
resource endpoint — the request never gets that far.

Why

CtApiClient holds the session cookie and CSRF token in instance fields
(src/api/ctClient.ts:120-145, authenticate() → captureCookie +
refreshCsrfToken). The CLI is one-shot, so the instance dies with the process
and the next invocation redoes GET /api/whoami?login_token=… +
GET /api/csrftoken. What is persisted per host is the login token (Keychain),
which is a credential, not a session — so nothing carries the handshake across
processes.

This is invisible for the primary workflow (plan / apply — one process, one
login) and only bites on the read path, where using the CLI as intended means
many short invocations: ct get, ct get raw, ct adopt, ct state, scripted
loops.

Suggested fix

Cache the session cookie + CSRF token on disk next to the stored credential,
keyed by host, and reuse it when still valid:

  • On authenticate(), write { cookie, csrfToken, obtainedAt } for that host
    (mode 0600, same store/dir as the existing per-host credential — the cookie is
    as sensitive as the token, so it must not land in a world-readable file or in
    the repo).
  • On startup, load the cached session and skip the handshake; on the first 401
    from a real request, drop the cache, re-authenticate once, and retry — so an
    expired or server-invalidated session self-heals rather than erroring out.
  • Keep the token↔host binding from bug(security): stored token sent to CT_HOST-overridden host — token↔host binding not enforced #30: the cache is per host, and a cached
    session must never be sent to a host other than the one it was captured
    against.
  • ct auth logout must delete the cached session along with the token.

Two smaller things worth deciding alongside it:

  • Backoff on 429 from the handshake. fetchWithRetry treats the whoami call
    as idempotent; if it does not already honour Retry-After for 429, doing so
    would turn a hard failure into a wait.
  • A clearer error. ✗ Login failed (whoami) (HTTP 429) reads as "your
    credentials are wrong". For a 429 specifically it should say the login rate
    limit was hit and roughly how long to wait.

Repro

for id in 3033 92 27813; do ct get raw "/persons/$id/groups?limit=200" --env prod; done

Observed on ct 0.1.0 (local build), host eqrm.church.tools, 2026-08-24.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttriageUnsorted intake — decide in the weekly sweep

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions