Skip to content

feat(auth): silent re-login on 90-day refresh-token expiry + expiry Repairs warning - #321

Open
sslivins wants to merge 2 commits into
binarydev:mainfrom
sslivins:feat/auto-relogin
Open

sslivins wants to merge 2 commits into
binarydev:mainfrom
sslivins:feat/auto-relogin

Conversation

@sslivins

Copy link
Copy Markdown

Problem

Generac's Auth0 tenant (auth.ecobee.com) gives this client's refresh tokens an absolute lifetime of ~90 days. Using the token doesn't extend it; only a fresh login does. As a result every user gets dropped into reauth every 90 days, and all entities go unavailable until they notice.

Observed on a live install. The entry was signed in at 2026-07-29 18:15 UTC and refreshed every 2 h (expires_in=7200) without trouble:

2026-10-27 10:00:50 PDT  Token refresh OK: expires_in=7200 ...
2026-10-27 12:02:21 PDT  Generac auth rejected, requesting reauth: Unknown or invalid refresh token.

The 90-day mark (2026-10-27 18:15 UTC = 11:15 PDT) falls exactly between the last successful refresh and the first failure. The auth.py docstring claiming the token is "reusable indefinitely" was wrong.

Fix

  1. Opt-in "Remember password" checkbox on setup, reauth and reconfigure (default off).
    • When invalid_grant is returned, GeneracAuth runs a full login with the stored password. It then switches to the new refresh token and DPoP key in place and saves them to entry.data.
    • The access-token call that hit the error returns a fresh token, so no poll fails.
    • If the login needs MFA (the paused flow is closed) or the password is rejected, it raises InvalidGrantError and the normal reauth flow takes over.
    • Network or unexpected errors are passed through unchanged, so the coordinator retries on the next poll instead of asking for reauth.
    • MFA accounts never store the password, because an unattended login would stop at the same challenge.
    • Unticking the box on reauth or reconfigure deletes the stored password.
  2. Expiry warning in Repairs.
    • Every login records token_issued_at.
    • When no password is stored, a WARNING Repairs issue appears 10 days before the expected expiry, telling the user to Reconfigure.
    • The issue clears on the first successful poll after signing in again, and is deleted with the entry.
    • Entries created before this change have no timestamp, so they get no warning until their next login.

Both values are in const.py (REFRESH_TOKEN_LIFETIME_DAYS = 90, REFRESH_TOKEN_WARN_DAYS = 10).

Tests

  • New tests cover: re-login with the stored password (new credentials adopted and saved), no password (still raises for reauth), MFA during re-login (flow closed, falls back to reauth), wrong password (falls back to reauth), the config flow saving the password and timestamp, unticking the box on reauth (password removed), and when the Repairs issue is created and cleared.
  • pytest: 83 passed on Python 3.14. pre-commit run --all-files is clean.

sslivins and others added 2 commits October 30, 2026 13:01
…warning

Generac's Auth0 tenant gives refresh tokens a ~90-day absolute lifetime
(observed in production: invalid_grant exactly 90 days after login while
refreshing every 2 h). Refreshing doesn't extend it, so every user had to
reauth manually every 90 days.

- Opt-in "Remember password" on setup / reauth / reconfigure. When set,
  an invalid_grant triggers a full login with the stored password; the new
  refresh token + DPoP key are adopted in place and persisted, so polling
  continues without a failed update. MFA challenges or a rejected password
  fall back to the normal reauth flow. Unticking the box removes the stored
  password. MFA accounts never store it.
- Record token_issued_at at every login; when no password is stored, raise
  a Repairs warning 10 days before the expected expiry. Cleared on the next
  successful poll after re-signing in.
- Correct the auth.py docstring that claimed refresh tokens are reusable
  indefinitely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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