feat(auth): support OAuth2 refresh-token login via auth.zaptec.com - #422
feat(auth): support OAuth2 refresh-token login via auth.zaptec.com#422johanneswilm wants to merge 2 commits into
Conversation
Zaptec migrated account login to OIDC (auth.zaptec.com) and retired the legacy password grant at api.zaptec.com/oauth/token. Accounts registered on the new identity provider can no longer authenticate with the current password-based login. Adds an optional OAuth2 path: the config flow accepts an OAuth2 refresh token (obtained from the Zaptec web portal session, client_id defaulting to the portal public client) and mints api.zaptec.com access tokens via the refresh_token grant at https://auth.zaptec.com/oauth2/token. The legacy username/password flow is preserved unchanged (password is now optional). Files: zaptec/const.py (AUTH_URL, OAUTH_SCOPE, OAUTH_CLIENT_ID), const.py (CONF_CLIENT_ID, CONF_REFRESH_TOKEN), zaptec/api.py (client_id/refresh_token args; refresh dispatcher with OAuth + legacy paths; token rotation), config_flow.py (optional client_id/refresh_token in setup and reauth), __init__.py (thread options, redact new secrets). NOTE: interim mechanism relying on the Zaptec web portal public OAuth client; a production-grade solution needs Zaptec to register an OAuth client for Home Assistant.
The Zaptec OIDC provider (Ory) issues single-use rotating refresh tokens. The config flow validated by consuming the supplied token, but the entry was created with the (now spent) original token, so runtime setup re-used it and failed with 'refresh token re-use was detected'. - config_flow: store the rotated token from login() into the new entry - api: expose the current refresh_token via a property - runtime: persist the current token to the config entry after login and after each poll, so restarts keep working without re-auth
Live install notes (validated 2026-08-16)Installed this branch on a Home Assistant Green (HACS custom repo, branch download) and got a real Zaptec charger (GPN032403 "Harpasset2") set up with live entities. Two things a user must know beyond the code:
Config flow inputs: |
|
Thank you for the PR. Since this is new functionality I'm slating this for the 0.9.0 release, as we need to get the 0.8.8 released out first. |
|
Hey @sveinse , |
feat(auth): support OAuth2 refresh-token login via auth.zaptec.com
Zaptec has moved login to the OIDC provider at
auth.zaptec.com(Kratos/Ory).The legacy
grant_type=passwordflow is retired on the new tenant, so thisadds an optional refresh-token login path while keeping the legacy password
flow for existing setups.
Changes
refresh_token(and optionalclient_id, defaulting to Zaptec's publicportal client) to the config schema.
_refresh_token()prefers the OAuth2 refresh-token grant athttps://auth.zaptec.com/oauth2/tokenwhen a refresh token is configured,falling back to the legacy password grant otherwise.
refresh tokens).
Important: two bugs fixed during real-world install
Zaptec's Ory/OAuth2 provider is strict in two ways that the first version of
this PR missed. Both are fixed here:
Single-use rotating refresh tokens. Every refresh consumes the token and
issues a new one. The config flow validated by consuming the supplied token
but created the entry with the original (now spent) token, so runtime setup
failed with
refresh token re-use was detected. Fix:config_flow.pystores the rotated token fromlogin()into the entry.api.pyexposes the current token via arefresh_tokenproperty.__init__.py/coordinator.pypersist the current token back to theconfig entry after login and after each poll, so the integration keeps
working across Home Assistant restarts without re-auth.
Audience. The access tokens must carry the Zaptec API audience, otherwise
api.zaptec.comrejects them with 401 and the integration keeps churningtokens. The portal requests this via an
audiencequery parameter(
extraQueryParams.audience, a space-separated list of the API URLs). Therefresh token must come from a grant that was authorized with that audience,
i.e. captured from the logged-in Zaptec portal (or any flow that passes the
same
audienceparam) — a plainauthorization_codegrant without it yieldsan empty-audience access token.
Verification
npm testfor the base repo tooling; Pythonpy_compileand
ruffclean except the pre-existingCPY001copyright notices).invalid_grant → AuthenticationError. A real charger (GPN032403 "Harpasset2")is reachable through
api.zaptec.com, setup completes, entities are created,and the integration survives a Home Assistant restart.