diff --git a/docs/cli/auth.md b/docs/cli/auth.md index d5380c7c..9fe06bc0 100644 --- a/docs/cli/auth.md +++ b/docs/cli/auth.md @@ -139,7 +139,7 @@ The command answers four diagnostic questions: am I signed in, as whom, via whic "issuer": "https://signin.pipefy.com/realms/pipefy", "state": "active", // or "refresh-expired" | "needs-login" | "n/a" "access_expires_at": "2026-05-20T22:14:03Z", // ISO 8601, null for static-bearer - "refresh_expires_at": "2026-06-19T18:02:00Z", // ISO 8601, null when not stored-session + "refresh_expires_at": "2026-06-19T18:02:00Z", // ISO 8601; null when not a stored session, or when the IdP advertises no refresh-token expiry (refresh_expires_in: 0) "token_rejected": false, // true only when the identity `me` query returned 401 "keychain_backend": "Keyring", // null for non-stored-session sources "masking_env_vars": [] // env vars masking a stored session, if any diff --git a/packages/cli/src/pipefy_cli/commands/auth.py b/packages/cli/src/pipefy_cli/commands/auth.py index 8dccf341..8cfa1757 100644 --- a/packages/cli/src/pipefy_cli/commands/auth.py +++ b/packages/cli/src/pipefy_cli/commands/auth.py @@ -358,8 +358,10 @@ def _populate_stored_session(report: AuthStatusReport, oidc: OidcClient) -> None report.access_expires_at = _iso_expiry( stale.obtained_at, stale.token.expires_in ) + # refresh_expires_in: 0 is Keycloak's "no advertised expiry" sentinel, + # not a 0-second TTL — treat as no expiry (else it renders "expired"). report.refresh_expires_at = _iso_expiry( - stale.obtained_at, stale.token.refresh_expires_in + stale.obtained_at, stale.token.refresh_expires_in or None ) raise _StatusExit( report=report, @@ -382,7 +384,7 @@ def _populate_stored_session(report: AuthStatusReport, oidc: OidcClient) -> None fresh_session.obtained_at, fresh_session.token.expires_in ) report.refresh_expires_at = _iso_expiry( - fresh_session.obtained_at, fresh_session.token.refresh_expires_in + fresh_session.obtained_at, fresh_session.token.refresh_expires_in or None ) diff --git a/packages/cli/tests/test_auth_status.py b/packages/cli/tests/test_auth_status.py index 9a8869b9..807bb89f 100644 --- a/packages/cli/tests/test_auth_status.py +++ b/packages/cli/tests/test_auth_status.py @@ -121,6 +121,24 @@ def test_status_stored_session_text_output( assert "Expires:" in result.stdout +def test_status_non_expiring_refresh_token_not_reported_expired( + clean_pipefy_env, saved_cwd, monkeypatch, runner, fake_keyring +): + """Keycloak advertises a non-expiring refresh token as ``refresh_expires_in: 0``. + That must not be read as a 0-second TTL and rendered as already ``expired``.""" + _set_auth_env(monkeypatch) + _seed_session(monkeypatch, refresh_expires_in=0) + session = storage.load_session(issuer=_ISSUER, client_id=_CLIENT_ID) + client = _mock_client_with_me() + with _patch_fresh_session(session), _patch_command_client(client): + json_result = _invoke_status(runner, ["--json"]) + text_result = _invoke_status(runner) + + assert json_result.exit_code == 0, json_result.stdout + assert json.loads(json_result.stdout)["refresh_expires_at"] is None + assert "Refresh token: expired" not in text_result.stdout + + # --------------------------------------------------------------------------- # # Scenario 2: stored-session, eager refresh runs (we mock the result as fresh) # # --------------------------------------------------------------------------- #