hardbyte / hardbyte/python-evnex

Refactor Cognito auth into an async, persistence-aware token lifecycle

Aberta
#113 0 comentários 0 reações 0 responsáveis Ver no GitHub
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

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.