|
| 1 | +# Authentication Artifacts Design (Password-Free Runtime) |
| 2 | + |
| 3 | +This document proposes and specifies an artifact-based authentication flow for `fmd_api` that avoids storing the raw user password in long-running integrations (e.g., Home Assistant), while preserving the ability to decrypt data and reauthenticate when tokens expire. |
| 4 | + |
| 5 | +## Goals |
| 6 | + |
| 7 | +- Do not retain the user's raw password in memory/storage after onboarding. |
| 8 | +- Support seamless reauthentication (401 → new token) without prompting the user again. |
| 9 | +- Keep the local RSA private key as a long-lived client secret to avoid re-fetching/decrypting each session. |
| 10 | +- Provide clear import/export and resume flows for integrations. |
| 11 | + |
| 12 | +## Terms |
| 13 | + |
| 14 | +- `fmd_id`: The FMD identity (username-like identifier). |
| 15 | +- `password_hash`: The full Argon2id string expected by the server when calling `/api/v1/requestAccess` (includes salt and parameters). |
| 16 | +- `access_token`: The current session token used in API requests; expires after the requested duration. |
| 17 | +- `private_key`: The RSA private key used to decrypt location/picture blobs and sign commands. Long-lived, stored client-side. |
| 18 | +- `session_duration`: Seconds requested when creating tokens (client default: 3600). |
| 19 | +- `token_issued_at`: Local timestamp to optionally preempt expiry. |
| 20 | + |
| 21 | +## Overview |
| 22 | + |
| 23 | +Two operating modes: |
| 24 | + |
| 25 | +1. Password mode (existing): Onboard with raw password; derive `password_hash`, request `access_token`, download and decrypt `private_key`. After success, the client may optionally discard the raw password. |
| 26 | +2. Artifact mode (new): Resume using stored artifacts (no raw password). On 401 Unauthorized, the client uses `password_hash` to request a fresh `access_token`. The `private_key` is already local. |
| 27 | + |
| 28 | +## API Additions |
| 29 | + |
| 30 | +### Constructor/Factory |
| 31 | + |
| 32 | +- `@classmethod async def resume(cls, base_url: str, fmd_id: str, access_token: str, private_key_bytes: bytes | str, *, password_hash: str | None = None, session_duration: int = 3600, **opts) -> FmdClient` |
| 33 | + - Loads the provided private key (PEM/DER) and sets runtime fields. |
| 34 | + - If a 401 occurs and `password_hash` is provided, requests a new token with `/api/v1/requestAccess`. |
| 35 | + - If `password_hash` is not provided, 401 bubbles as an error (caller can re-onboard or supply a callback). |
| 36 | + |
| 37 | +- `@classmethod async def from_auth_artifacts(cls, artifacts: dict, **opts) -> FmdClient` |
| 38 | + - Convenience around `resume()`. Expects keys: `base_url`, `fmd_id`, `access_token`, `private_key` (PEM or base64 DER), optional `password_hash`, `session_duration`. |
| 39 | + |
| 40 | +- `async def export_auth_artifacts(self) -> dict` |
| 41 | + - Returns a serializable dict containing: `base_url`, `fmd_id`, `access_token`, `private_key` (PEM), `password_hash` (if available), `session_duration`, `token_issued_at`. |
| 42 | + |
| 43 | +- `async def drop_password(self) -> None` |
| 44 | + - Immediately discards any stored raw password. Recommended once artifacts have been persisted by the caller. |
| 45 | + |
| 46 | +- `@classmethod async def create(..., drop_password: bool = False)` |
| 47 | + - After successful onboarding, if `drop_password=True`, clears the in-memory `_password` attribute. |
| 48 | + |
| 49 | +### Internal Helpers |
| 50 | + |
| 51 | +- `async def _reauth_with_hash(self) -> None` |
| 52 | + - Calls `/api/v1/requestAccess` with stored `password_hash` and `session_duration`. Updates `access_token` on success. |
| 53 | + |
| 54 | +- `_make_api_request` changes |
| 55 | + - On 401: if `_password` is present, behave as today (reauth using raw password). |
| 56 | + - Otherwise, if `password_hash` is present, call `_reauth_with_hash()` once and retry. |
| 57 | + - Else: raise. |
| 58 | + |
| 59 | +## Data Handling |
| 60 | + |
| 61 | +- `private_key` must be loadable from PEM or DER. `export_auth_artifacts()` will prefer PEM for portability. |
| 62 | +- `password_hash` is an online-equivalent secret for token requests. It is preferable to raw password, but should still be stored carefully (consider HA secrets storage if available). |
| 63 | +- No raw password is stored or exported by default. |
| 64 | + |
| 65 | +## Failure Modes |
| 66 | + |
| 67 | +- User changes password or server salt/params: stored `password_hash` becomes invalid. Reauth fails; caller should prompt the user once, produce a new `password_hash`, and update artifacts. |
| 68 | +- Server caps or rejects long `session_duration`: token would expire earlier than requested; client handles 401 via reauth. |
| 69 | +- Private key rotation: if the server issues a new private key (unlikely in normal flow), onboarding should refresh artifacts. |
| 70 | + |
| 71 | +## Example Flows |
| 72 | + |
| 73 | +### Onboarding (password mode) |
| 74 | + |
| 75 | +```python |
| 76 | +client = await FmdClient.create(base_url, fmd_id, password, session_duration=3600) |
| 77 | +artifacts = await client.export_auth_artifacts() |
| 78 | +await client.drop_password() # optional hardening |
| 79 | +# Persist artifacts in HA storage |
| 80 | +``` |
| 81 | + |
| 82 | +### Resume (artifact mode) |
| 83 | + |
| 84 | +```python |
| 85 | +client = await FmdClient.from_auth_artifacts(artifacts) |
| 86 | +# Use client normally; on 401 it will reauth using password_hash if present |
| 87 | +``` |
| 88 | + |
| 89 | +## Backward Compatibility |
| 90 | + |
| 91 | +- Existing behavior is preserved. |
| 92 | +- New APIs are additive. |
| 93 | +- Deprecation of retaining raw `_password` by default is not proposed; instead provide `drop_password=True` knob and a `drop_password()` method. |
| 94 | + |
| 95 | +## Security Considerations |
| 96 | + |
| 97 | +- Storing `password_hash` is strictly better than storing the raw password, but still sensitive. |
| 98 | +- If the host supports keyrings or encrypted secret storage, prefer it for both `password_hash` and `private_key`. |
| 99 | +- Consider file permissions and in-memory zeroization when feasible. |
| 100 | + |
| 101 | +## Open Questions |
| 102 | + |
| 103 | +- Should `drop_password=True` become the default in a future major version? |
| 104 | +- Should we provide a pluggable secret provider interface for HA to implement platform-specific secure storage? |
0 commit comments