Metadata-Version: 2.4
Name: sysnet-auth
Version: 0.2.0
Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
Author: SYSNET s.r.o.
License: Proprietary
Project-URL: Homepage, https://sysnet.cz
Keywords: fastapi,keycloak,jwt,oidc,auth,sysnet
Classifier: Framework :: FastAPI
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.115
Requires-Dist: pydantic>=2.9
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pyjwt[crypto]>=2.9
Requires-Dist: httpx>=0.27
Requires-Dist: sysnet-pyutils>=0.1
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: cryptography>=42.0; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"

# sysnet-auth (import path: `auth_lib`)

Sdílená autentizační knihovna pro FastAPI mikroslužby, které ověřují identitu uživatelů přes **Keycloak** (OIDC / JWT). Součást SYSNET ekosystému.

Knihovna je záměrně úzká: neobsahuje business logiku, neukládá stav a nepřeposílá tokeny. Jediná zodpovědnost je **validace JWT** a vystavení objektu `AuthenticatedUser` dependency injection mechanismem FastAPI.

---

## Proč existuje

V architektuře desítek mikroslužeb nechceme, aby každá z nich měla vlastní implementaci validace JWT. Rozkol v drobných detailech (kontrola audience, leeway, cachování JWKS) je snadný způsob, jak vyrobit bezpečnostní díru. `auth_lib` je **jediné místo, kde se validuje identita uživatele** — a všechny služby ji používají stejně.

---

## Instalace

Balíček je publikovaný jako **`sysnet-auth`**, importuje se jako **`auth_lib`**
(běžný pattern: distribuční jméno ≠ import jméno).

```bash
pip install sysnet-auth
```

```python
from auth_lib import get_current_user, require_role
```

### Python

Vyžaduje **Python ≥ 3.11**, plně testováno proti **3.11 – 3.14**.

### Runtime závislosti

| Balíček             | Role                                           |
|---------------------|------------------------------------------------|
| `fastapi`           | DI / exception handlery                        |
| `pydantic` v2       | modely, validace                               |
| `pydantic-settings` | konfigurace přes env                           |
| `pyjwt[crypto]`     | dekódování + validace JWT                      |
| `httpx`             | async HTTP klient pro JWKS endpoint            |
| `sysnet-pyutils`    | sdílené SYSNET modely (`UserType`, `ErrorModel`, `Log`) |

> **Volba JWT knihovny.** `python-jose` je od 2021 neudržovaný. Používáme **PyJWT** — aktivně udržovaný de facto standard pro JWT v Pythonu.

---

## Konfigurace

Všechno přes environment proměnné s prefixem `AUTH_`. Pydantic-settings načte settings při prvním volání `get_settings()` a cachuje je na proces.

| Proměnná                            | Default      | Popis |
|-------------------------------------|--------------|-------|
| `AUTH_KEYCLOAK_URL`                 | —            | Základní URL Keycloaku (`https://kc.example.cz`) |
| `AUTH_REALM`                        | —            | Název Keycloak realm |
| `AUTH_AUDIENCE`                     | —            | Očekávaný `aud` claim (typicky `client_id` API) |
| `AUTH_ALGORITHMS`                   | `["RS256"]`  | Povolené podpisové algoritmy (CSV nebo JSON) |
| `AUTH_JWKS_CACHE_SECONDS`           | `300`        | TTL JWKS cache |
| `AUTH_JWKS_HTTP_TIMEOUT_SECONDS`    | `5.0`        | HTTP timeout pro fetch JWKS |
| `AUTH_LEEWAY_SECONDS`               | `0`          | Tolerance hodinového rozdílu (clock skew) |
| `AUTH_RESOURCE_CLIENT`              | `None`       | Pokud nastaveno, mergne i `resource_access.<klient>.roles` |
| `AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS` | `0`     | Opt-in: refresh při neznámém `kid` 1× za N sekund |

### Odvozené hodnoty

- **Issuer:** `{AUTH_KEYCLOAK_URL}/realms/{AUTH_REALM}`
- **JWKS URL:** `{issuer}/protocol/openid-connect/certs`

---

## Použití ve FastAPI

### Ověřený uživatel

```python
from fastapi import Depends, FastAPI
from auth_lib import AuthenticatedUser, get_current_user, install_exception_handlers

app = FastAPI()
install_exception_handlers(app)

@app.get("/me")
async def me(user: AuthenticatedUser = Depends(get_current_user)) -> AuthenticatedUser:
    return user
```

### Role guard

```python
from auth_lib import require_role, require_any_role, require_all_roles

@app.delete("/users/{id}")
async def delete_user(
    id: str,
    user: AuthenticatedUser = Depends(require_role("admin")),
):
    ...

@app.get("/content")
async def list_content(
    user: AuthenticatedUser = Depends(require_any_role(["editor", "viewer"])),
):
    ...
```

### Konverze do SYSNET `UserType`

```python
from auth_lib import get_current_user

@app.get("/user-profile")
async def profile(user = Depends(get_current_user)):
    sysnet_user = user.to_user_type()   # sysnet_pyutils.UserType
    return sysnet_user
```

Mapping: `sub → identifier`, `preferred_username → name`, `email → email`, `given_name → name_first`, `family_name → name_last`, `name → name_full`.

---

## Observability

Knihovna neví, co je Prometheus / OpenTelemetry / strukturovaný log. Místo toho exponuje **pub-sub hooky**, které si konzument zaregistruje:

```python
from auth_lib import on_token_validated, on_token_rejected, on_jwks_refresh

@on_token_validated
def _ok(user):
    metrics.incr("auth.ok", tags={"sub": user.sub})

@on_token_rejected
def _err(exc):
    metrics.incr("auth.err", tags={"type": type(exc).__name__})

@on_jwks_refresh
def _jwks(n):
    metrics.gauge("auth.jwks.keys", n)
```

Výjimka v hooku **nikdy neshodí validaci** (hooky jsou best-effort).

### Rychlé zapnutí přes SYSNET logger

```python
from auth_lib import install_sysnet_logging
install_sysnet_logging()  # zapíše INFO/WARNING přes sysnet_pyutils.Log
```

Idempotentní — druhé volání už hooky znovu neregistruje.

---

## Výjimky

| Výjimka                  | HTTP | Kdy                                                     |
|--------------------------|------|---------------------------------------------------------|
| `InvalidTokenError`      | 401  | špatný podpis, expirace, `iss`, `aud`, neznámý `kid`, … |
| `MissingRoleError`       | 403  | uživatel je ověřen, ale chybí mu role                   |
| `AuthConfigurationError` | 500  | nedostupný JWKS, špatné URL, malformed response         |

Všechny dědí z `AuthError`.

`install_exception_handlers(app)` registruje handler, který vrací **`sysnet_pyutils.ErrorModel`**:

```json
{"code": 401, "message": "Token has expired"}
```

Jednotný formát chyb napříč SYSNET službami.

---

## JWKS caching

- In-memory TTL cache (default 300 s).
- Lazy fetch při prvním volání.
- **Single-flight:** souběh N requestů při cold/expired cache spustí právě jeden fetch.
- **Fresh cache je autoritativní** — kid miss = `InvalidTokenError`. Brání DoS přes náhodné kidy.
- **Opt-in cooldown refresh** (`AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS > 0`): při kid miss ve fresh cache povolí 1 refresh za N sekund — responzivnější reakce na rotaci klíčů.
- Lazy init `asyncio.Lock` — kompatibilní s Pythonem 3.14 (neváže lock na event loop z doby importu).
- Žádný background task, žádný disk.

---

## Bezpečnostní poznámky

- Ověřujeme vždy **podpis, issuer i audience**.
- Výchozí `leeway=0`. Zapnout jen při doloženém clock skew.
- Pouze `RS256` výchozí, `none` ani HS256 nepovolujeme bez dobrého důvodu.
- Čteme **pouze `Authorization: Bearer`** (žádné cookies, žádné `X-*` hlavičky).
- Tokeny se nelogují. Diagnostika přes `sub` / `jti`.
- JWKS fetch má timeout → nedostupný Keycloak vrátí 500, ne čekání donekonečna.

---

## Struktura projektu

```
auth_lib/
├── auth_lib/
│   ├── __init__.py          # veřejné re-exporty (__all__)
│   ├── config.py            # AuthSettings
│   ├── exceptions.py        # AuthError + to_error_model()
│   ├── models.py            # AuthenticatedUser + to_user_type()
│   ├── jwks.py              # async JWKS cache + cooldown
│   ├── dependencies.py      # get_current_user, install_exception_handlers
│   ├── roles.py             # require_role / require_any_role / require_all_roles
│   ├── observability.py     # hook registry + install_sysnet_logging()
│   └── py.typed             # PEP 561 marker
├── tests/
├── pyproject.toml           # sysnet-auth, Python 3.11-3.14
├── CHANGELOG.md
├── README.md
└── .gitignore
```

---

## Testování

```bash
pip install -e ".[dev]"
pytest --cov=auth_lib --cov-report=term-missing
```

Testy nevolají reálný Keycloak — používají vlastní RSA keypair a JWKS cache předvyplněnou odpovídajícím JWK. **58 testů, 98 % pokrytí.**

---

## Architektonický princip

> **Tato knihovna je jediným místem, kde se řeší validace identity uživatele.**

Všechny FastAPI mikroslužby ji používají jednotným způsobem. Pokud narazíš na potřebu obejít `auth_lib` (vlastní dekódování, custom validace), je to signál k úpravě `auth_lib` — ne k duplikaci logiky.
