Skip to content

fix(cli): don't render a non-expiring refresh token as expired#425

Draft
richard-pipefy wants to merge 1 commit into
pipefy:devfrom
richard-pipefy:fix/auth-status-non-expiring-refresh-token
Draft

fix(cli): don't render a non-expiring refresh token as expired#425
richard-pipefy wants to merge 1 commit into
pipefy:devfrom
richard-pipefy:fix/auth-status-non-expiring-refresh-token

Conversation

@richard-pipefy

Copy link
Copy Markdown

Summary

pipefy auth status always showed Refresh token: expired (and refresh_expires_at as a past timestamp in --json), even on a freshly-authenticated session — misleading users, and agents polling auth during long-running work, into thinking re-login is imminent.

Refresh-token expiry is computed as obtained_at + refresh_expires_in. Keycloak sends refresh_expires_in: 0 to advertise a non-expiring refresh token, but that 0 was read as a 0-second TTL, placing expiry at the login instant. Eager refresh resets obtained_at to now on every rotation, so it sat perpetually at ~now → perpetually "expired".

Fix: normalize the sentinel where the refresh token is read (refresh_expires_in or None), so 0 → no expiry → unknown (text) / null (JSON). _iso_expiry stays generic, so a genuine 0-second access-token TTL still reads as "expired". Purely a status-readout change; the refresh decision (_is_stale) never consults this value — it's only carried forward verbatim on rotation.

Deliberately not rendered as "never": per Keycloak's issue tracker, refresh_expires_in: 0 doesn't guarantee the token never expires — with "Offline Session Max Limited" off but a non-zero "Offline Session Idle", the server still enforces an idle timeout and rejects refreshes with invalid_grant. unknown/null is the honest readout.

Confirmed against a live session: refresh_expires_in = 0, expires_in = 300, recent obtained_at.

Testing performed

  • uv run --package pipefy-cli pytest packages/cli/tests/341 passed, 13 skipped
  • uv run ruff check packages/cli/src/pipefy_cli/commands/auth.pyAll checks passed!
  • Added test_status_non_expiring_refresh_token_not_reported_expired (seeds refresh_expires_in=0; asserts --json refresh_expires_at is None and the text line is not expired).

Docs

  • docs/cli/auth.md: updated the refresh_expires_at JSON-schema note — null now also covers an active stored session whose refresh token advertises no expiry (refresh_expires_in: 0).

`pipefy auth status` computed the refresh-token expiry as
`obtained_at + refresh_expires_in`. Keycloak sends `refresh_expires_in: 0`
to advertise a non-expiring refresh token, but `_iso_expiry` reads `0` as a
0-second TTL and places expiry at the login instant — so the durable
credential was always shown as `expired` (and `refresh_expires_at` was a
past timestamp in `--json`). Eager refresh resets `obtained_at` to now on
every rotation, so it stays perpetually "expired".

Normalize the sentinel where the refresh token is read (`refresh_expires_in
or None`), leaving `_iso_expiry` generic so a genuine 0-second access-token
TTL still reads as expired. Purely a status-readout change; the refresh
decision (`_is_stale`) never consults this value — it's only carried forward
verbatim on rotation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant