hardbyte / hardbyte/python-evnex
Refactor Cognito auth into an async, persistence-aware token lifecycle
- Linguagem predominante
- Python
- Estrelas
- 13
- Forks
- 8
- Merge médio
- 5h 58min
- PRs com merge (30d)
- 2
Descrição
## Context
MFA support in #85 / v0.6.0 adds the missing Cognito challenge flow and fixes several immediate reliability problems. The current API now works reasonably for a CLI or a single long-lived `Evnex` object.
It is still the wrong abstraction for a Home Assistant integration or another long-running application that must persist credentials safely across restarts.
## Problems
### 1. Refresh-token resumption still requires and retains the password
`Evnex.__init__()` requires both `username` and `password`, even when a refresh token is supplied:
https://github.com/hardbyte/python-evnex/blob/main/evnex/api.py#L120-L157
The README says "the refresh token alone is enough", but callers still need to provide a real or dummy password:
https://github.com/hardbyte/python-evnex/blob/main/README.md#L104-L113
For Home Assistant, the password should ideally be used only during interactive bootstrap/reauthentication and then discarded. Normal startup should require only the durable refresh-token session.
### 2. Refreshed token state cannot be reliably persisted
`pycognito` mutates `access_token`, `id_token`, and `refresh_token` internally during authentication and refresh. `python-evnex` exposes properties for reading them, but provides no notification or async callback when the token set changes:
https://github.com/hardbyte/python-evnex/blob/main/evnex/api.py#L167-L207
https://github.com/hardbyte/python-evnex/blob/main/evnex/api.py#L275-L285
A caller can manually inspect the properties after a request, but it cannot reliably or atomically persist every replacement token set. This becomes a correctness issue if Cognito refresh-token rotation is enabled, and is awkward even when only access/ID tokens change.
Token persistence should be owned by the application, with the library explicitly publishing each newly issued token set before other requests proceed.
### 3. The public authentication API is synchronous
`authenticate()` and `respond_to_mfa_challenge()` perform network operations synchronously:
https://github.com/hardbyte/python-evnex/blob/main/evnex/api.py#L218-L273
Internal API calls correctly wrap Cognito operations in `asyncio.to_thread()`, but a Home Assistant config flow calling the documented public methods must know to add its own executor/thread boundary. This is easy to get wrong and inconsistent with the otherwise async client.
All public methods that can perform network I/O should be async.
### 4. Public consumers are coupled to `pycognito`
MFA is surfaced through `pycognito` exception classes and `challenge.get_tokens()`:
https://github.com/hardbyte/python-evnex/blob/main/README.md#L62-L102
This leaks the implementation dependency into integrations and makes it harder to change the Cognito implementation later. It also models MFA as a special `mode + code` call rather than an explicit Cognito challenge containing the challenge name, opaque session, and parameters.
Even if Evnex currently uses only SMS and TOTP, `python-evnex` should expose its own typed challenge/result API.
### 5. Authentication retries are coupled to generic request retries
A 401 calls `_ensure_valid_token()`. If `check_token()` considers the JWT unexpired, the request fails without forcing one refresh. Conversely, if renewal keeps reporting success, `TokenRefreshedError` can consume the full five-attempt generic retry budget:
https://github.com/hardbyte/python-evnex/blob/main/evnex/api.py#L302-L310
https://github.com/hardbyte/python-evnex/blob/main/tests/test_auth.py#L162-L197
Authentication recovery should have independent semantics:
1. Send with the current valid token.
2. On 401, force one single-flight refresh.
3. Retry the request once.
4. Surface a reauthentication-required error if it still fails.
It should not share the generic transient-network retry counter, particularly for command endpoints that may not be safe to send repeatedly.
### 6. Authentication is applied separately to every API method
`@refresh_token_if_expired` is repeated across resource methods. This is easy to omit when adding a new endpoint and mixes transport/auth concerns into each operation.
Authentication and one-time 401 recovery should happen in one central request path or an injected `httpx.Auth`-like component.
## Proposed direction
Introduce a separate async authentication component, while retaining a backwards-compatible convenience constructor initially.
For example:
```python
@dataclass(frozen=True)
class TokenSet:
access_token: str
id_token: str | None
refresh_token: str
expires_at: datetime | None
@dataclass(frozen=True)
class AuthChallenge:
name: str
session: str
parameters: Mapping[str, str]
class EvnexAuth:
async def get_access_token(self) -> str: ...
async def force_refresh(self) -> TokenSet: ...
async def start_authentication(self, username: str, password: str) -> TokenSet | AuthChallenge: ...
async def respond_to_challenge(self, challenge: AuthChallenge, response: str) -> TokenSet | AuthChallenge: ...
```
The application should be able to supply an async token-update callback or token store:
```python
async def save_tokens(tokens: TokenSet) -> None: ...
```
`Evnex` would then accept an auth object and an `httpx.AsyncClient`, without needing to retain a password itself.
## Acceptance criteria
- [ ] A client can resume using a refresh token without supplying or retaining a password.
- [ ] Every successful authentication/refresh returns or publishes the complete current token set.
- [ ] Token updates can be persisted atomically by the caller.
- [ ] All public network operations, including initial auth and MFA responses, are async.
- [ ] Public exceptions and challenge objects do not expose `pycognito` types.
- [ ] Concurrent API calls perform at most one refresh.
- [ ] A 401 triggers at most one forced refresh and one request retry.
- [ ] Auth retry behaviour is separate from generic transient HTTP retries.
- [ ] Authentication is enforced through one central request path rather than per-method decorators.
- [ ] Tests cover refresh-token-only startup, token-update persistence, concurrent refresh, MFA challenge round-tripping, and bounded 401 recovery.
This does not need to block the current MFA release. It is a follow-up refactor to make the new support robust for Home Assistant rather than merely functional in a single-process example.
Guia de contribuição
Nenhum guia de contribuição indexado para este repositório
Direção de pesquisa
Comece lendo o fluxo de autenticação em evnex/api.py, especialmente as linhas 120-157, 167-207, 218-273, 275-285 e 302-310; em seguida, revise as linhas 162-197 de tests/test_auth.py e os exemplos de autenticação do README. Considera-se concluído quando os critérios de aceitação listados estiverem cobertos, incluindo autenticação assíncrona, atualizações persistidas do token, recuperação limitada para 401, autenticação centralizada e os novos testes de autenticação.
Escrita pelo modelo de indexação a partir do texto da issue.
Avaliação
- Stack de tecnologia
- python
- Domínio
- authentication, backend-api-design
- Tipo de issue
- Refatoração
- Dificuldade
- 5/5
- Tempo estimado
- Mais de uma semana
- Status de atividade
- Pouca atividade
- Clareza
- Claramente especificada
- Facilidade para iniciantes
- 35/100