Skip to content

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

Merged
mocha06 merged 1 commit into
pipefy:devfrom
richard-pipefy:fix/auth-status-non-expiring-refresh-token
Jul 22, 2026
Merged

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

Conversation

@richard-pipefy

Copy link
Copy Markdown
Contributor

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>

@adriannoes adriannoes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed while this PR is still a draft. The status-readout fix looks correct and ready to land once you mark it ready for review (undraft): Keycloak refresh_expires_in: 0 no longer renders as an expired refresh token, _iso_expiry stays generic for access-token TTLs, and refresh/_is_stale is untouched. Local auth-status tests and CI on tip are green.

Please undraft when you consider it ready for review / merge. This APPROVE is on the current tip (9267fc1) as a signal that the change itself is good; it is not a request to merge while still draft.

What worked well

  • Honest unknown / null instead of claiming "never" (idle timeout can still invalidate refreshes).
  • Docs note for refresh_expires_at updated in the same PR.
  • Regression test fails if the or None sentinel handling is reverted.

Review path

  • Checked out the PR tip, ran CLI auth-status unit tests and ruff; CI lint+test green.
  • Cross-checked sibling #409 (Windows dpapi keychain): independent merge order, non-overlapping hunks, auto-merge clean. No findings to carry from that PR onto this tip.

@Danielmoraisg Danielmoraisg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! It was driving me crazy that the agents were always talking to me about how long we had until the token expiration ❤️

@mocha06

mocha06 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

we'll ship this later on in this week, I'd like to thoroughly test it beforehand. For now, thanks and good job!

@mocha06
mocha06 merged commit 55040f5 into pipefy:dev Jul 22, 2026
2 checks passed
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.

4 participants