pyisyox.auth module¶
Authentication strategies for IoX 6 controllers.
Two modes share a single Auth protocol so the HTTP client can be
auth-agnostic:
LocalAuth— HTTP basic against:8443/rest/*with the local admin account. Offline by construction. Feature-degraded: no/api/triggersAST, no/api/variablesnames; must use the legacy XML/rest/nodesfor structure. Recommended only when the user refuses to use a portal account.PortalAuth— JWT bearer obtained fromPOST :443/api/login. Auto-refreshes viaPOST :443/api/jwt/refresh. Verified offline-safe on 2026-05-07: the eisy validates portal credentials and signs the JWT locally; no my.isy.io round-trip during steady-state. Recommended default — unlocks the modern/api/*JSON surface.
The PortalAuth flow leaks the PG3 MQTT TLS keypair under
data.ssl in the login response. redact_sensitive() (see
pyisyox.redactor) MUST be applied to any debug-level logging of the
response body.
Endpoint discovery and shape verification: POST /api/login body
{"username": "<email>", "password": "<password>"}; response
{"successful": true, "data": {"accessToken": "<es256-jwt>",
"refreshToken": "<es256-jwt>", "ssl": {...}, ...}}. Refresh body
{"refreshToken": "<rt>"} returns the same data shape.
- exception AuthError[source]¶
Bases:
ExceptionAuthentication failure (login rejected, refresh failed, etc.).
- class TokenPair(access_token, refresh_token, access_expires_at=0.0, refresh_expires_at=0.0)[source]¶
Bases:
objectJWT access + refresh tokens with decoded expiry for proactive refresh.
- Variables:
access_token (str) – Short-lived bearer token (default 1 h TTL).
refresh_token (str) – Long-lived token used to mint a new access token (default 30 d TTL).
access_expires_at (float) – Unix timestamp at which
access_tokenexpires. Decoded from the JWTexpclaim.0if the token couldn’t be decoded.refresh_expires_at (float) – Unix timestamp at which
refresh_tokenexpires.0if undecoded.
- Parameters:
- class Auth(*args, **kwargs)[source]¶
Bases:
ProtocolAuth strategy protocol shared by
LocalAuthandPortalAuth.Not
@runtime_checkable—isinstance(x, Auth)against an unrelated class that happens to share these attribute names would pass without verifying coroutine signatures, which masks bugs. Tests construct the concrete classes directly.The HTTP client calls
authenticate()once during connect, thenrequest_kwargs()before every request to obtain the kwargs to splat intosession.get(...)/session.post(...). On a 401 response, the client callshandle_unauthorized(); if it returns True, the client retries the original request once.- async authenticate(session, base_url)[source]¶
Perform any one-time authentication setup (e.g., login POST).
- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
None
- async request_kwargs(session, base_url)[source]¶
Return kwargs for
session.request()(auth, headers, etc.).- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
- async handle_unauthorized(session, base_url)[source]¶
Handle a 401 response. Return True if re-auth succeeded and the original request should be retried; False if the auth state cannot recover (caller should propagate the 401 as a permanent error).
- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
- async close(session, base_url)[source]¶
Release any auth-held resources (e.g., explicit logout).
sessionandbase_urlare passed so implementations can make a final logout call to invalidate server-side state (PortalAuth posts/api/logout); LocalAuth ignores them since basic-auth has no server-side session.- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
None
- class LocalAuth(username, password)[source]¶
Bases:
objectHTTP basic auth against
:8443/rest/*with the local admin account.No login round-trip is needed — credentials are passed on every request. A 401 on this path means the credentials are wrong, so re-auth cannot recover.
- async authenticate(session, base_url)[source]¶
No-op — basic auth attaches per request.
- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
None
- async request_kwargs(session, base_url)[source]¶
Return kwargs that attach HTTP basic auth.
- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
- async handle_unauthorized(session, base_url)[source]¶
Cannot recover from 401 with basic auth — credentials are wrong.
- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
- async close(session, base_url)[source]¶
No-op — basic auth has no server-side session to tear down.
- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
None
- class PortalAuth(email, password)[source]¶
Bases:
objectJWT bearer auth from
POST :443/api/login.Maintains an in-memory
TokenPairwith proactive refresh. On 401, attempts one refresh; if refresh fails (or has expired), falls back to a fresh login.Login URL:
{base_url}/api/login. Refresh URL:{base_url}/api/jwt/refresh. Logout URL (optional, onclose()):{base_url}/api/jwt/logout. Verified against eisy 1.0.3 —POST /api/jwt/logoutreturns200with{"successful": true, "data": null}. (Pre-2026-05-12 versions of this module used/api/logout, which 404s.)- LOGIN_PATH¶
- REFRESH_PATH¶
- LOGOUT_PATH¶
- PROACTIVE_REFRESH_LEEWAY¶
Number of seconds before access-token expiry at which we proactively refresh.
- property tokens: TokenPair | None¶
Currently held tokens, or
Noneif not yet authenticated.Tests use this to assert state without forcing a real network round-trip.
- async authenticate(session, base_url)[source]¶
Perform
POST /api/loginand store the returned token pair.Concurrent calls collapse onto a single login round-trip via the instance lock; the second caller observes the tokens already set and returns without making a network request.
- Raises:
AuthError – When the login response is not
successful: trueor lacks tokens.- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
None
- async request_kwargs(session, base_url)[source]¶
Return
Authorization: Bearer <accessToken>headers.Refreshes the token proactively if it expires within
PROACTIVE_REFRESH_LEEWAYseconds, avoiding the cost of an in-flight 401 + refresh + retry round. Concurrent callers that observe an expiring token both queue on the auth lock; the winner refreshes once, the runners-up re-check and skip.- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
- async handle_unauthorized(session, base_url)[source]¶
Handle 401: try refresh, then re-login.
Concurrent 401s from in-flight requests all enter
_refresh_or_relogin; the first runs the refresh, subsequent callers re-check the cached token (which has just been updated) and skip the network round-trip. Returns True if re-auth succeeded and the caller should retry the original request; False if both refresh and login failed.- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
- async close(session, base_url)[source]¶
Best-effort logout against
POST /api/jwt/logout, then clear the in-memory tokens.If we don’t tell the eisy we’re done, the refresh token stays live for its full 30-day TTL — useful only to attackers who somehow obtain it. The logout call is best-effort: any error (network down, controller already gone, stale token) is logged at debug level and swallowed. The local token state is cleared regardless so the consumer can construct a fresh
PortalAuthand re-authenticate.- Parameters:
session (aiohttp.ClientSession)
base_url (str)
- Return type:
None