Metadata-Version: 2.4
Name: sysnet-auth
Version: 0.2.9
Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
Author: SYSNET s.r.o.
License: GNU Affero General Public License v3
Project-URL: Homepage, https://sysnet.cz
Project-URL: Repository, https://github.com/SYSNET-CZ/auth-lib
Project-URL: Documentation, https://github.com/SYSNET-CZ/auth-lib#readme
Project-URL: Bug Tracker, https://github.com/SYSNET-CZ/auth-lib/issues
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
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi<1,>=0.115
Requires-Dist: pydantic<3,>=2.9
Requires-Dist: pydantic-settings<3,>=2.3
Requires-Dist: pyjwt[crypto]<3,>=2.9
Requires-Dist: httpx<1,>=0.27
Requires-Dist: sysnet-pyutils>=0.1
Provides-Extra: dev
Requires-Dist: pytest<10,>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio<1,>=0.23; extra == "dev"
Requires-Dist: pytest-cov<8,>=5.0; extra == "dev"
Requires-Dist: cryptography<48,>=42.0; extra == "dev"
Requires-Dist: mypy<2,>=1.11; extra == "dev"
Requires-Dist: ruff<1,>=0.6; extra == "dev"
Dynamic: license-file

     1|# sysnet-auth (import path: `auth_lib`)
     2|
     3|[![GitHub](https://img.shields.io/badge/GitHub-SYSNET--CZ%2Fauth--lib-181717?style=flat&logo=github)](https://github.com/SYSNET-CZ/auth-lib)
     4|[![PyPI - Version](https://img.shields.io/pypi/v/sysnet-auth)](https://pypi.org/project/sysnet-auth/)
     5|[![Python Version](https://img.shields.io/pypi/pyversions/sysnet-auth)](https://pypi.org/project/sysnet-auth/)
     6|[![FastAPI](https://img.shields.io/badge/FastAPI-0.115%2B-009688?style=flat&logo=fastapi)](https://fastapi.tiangolo.com)
     7|[![Tests](https://img.shields.io/badge/tests-58%20passed-brightgreen)](https://github.com/SYSNET-CZ/auth-lib)
     8|[![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen)](https://github.com/SYSNET-CZ/auth-lib)
     9|[![License](https://img.shields.io/badge/license-AGPL--3-blue)](https://github.com/SYSNET-CZ/auth-lib/blob/main/LICENSE)
    10|
    11|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.
    12|
    13|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.
    14|
    15|---
    16|
    17|## Proč existuje
    18|
    19|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ě.
    20|
    21|---
    22|
    23|## Instalace
    24|
    25|Balíček je publikovaný jako **`sysnet-auth`**, importuje se jako **`auth_lib`**
    26|(běžný pattern: distribuční jméno ≠ import jméno).
    27|
    28|```bash
    29|pip install sysnet-auth
    30|```
    31|
    32|```python
    33|from auth_lib import get_current_user, require_role
    34|```
    35|
    36|### Python
    37|
    38|Vyžaduje **Python ≥ 3.11**, plně testováno proti **3.11 – 3.14**.
    39|
    40|### Runtime závislosti
    41|
    42|| Balíček             | Role                                           |
    43||---------------------|------------------------------------------------|
    44|| `fastapi`           | DI / exception handlery                        |
    45|| `pydantic` v2       | modely, validace                               |
    46|| `pydantic-settings` | konfigurace přes env                           |
    47|| `pyjwt[crypto]`     | dekódování + validace JWT                      |
    48|| `httpx`             | async HTTP klient pro JWKS endpoint            |
    49|| `sysnet-pyutils`    | sdílené SYSNET modely (`UserType`, `ErrorModel`, `Log`) |
    50|
    51|> **Volba JWT knihovny.** `python-jose` je od 2021 neudržovaný. Používáme **PyJWT** — aktivně udržovaný de facto standard pro JWT v Pythonu.
    52|
    53|---
    54|
    55|## Konfigurace
    56|
    57|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.
    58|
    59|| Proměnná                            | Default      | Popis |
    60||-------------------------------------|--------------|-------|
    61|| `AUTH_KEYCLOAK_URL`                 | —            | Základní URL Keycloaku (`https://kc.example.cz`) |
    62|| `AUTH_REALM`                        | —            | Název Keycloak realm |
    63|| `AUTH_AUDIENCE`                     | —            | Očekávaný `aud` claim (typicky `client_id` API) |
    64|| `AUTH_ALGORITHMS`                   | `["RS256"]`  | Povolené podpisové algoritmy (CSV nebo JSON) |
    65|| `AUTH_JWKS_CACHE_SECONDS`           | `300`        | TTL JWKS cache |
    66|| `AUTH_JWKS_HTTP_TIMEOUT_SECONDS`    | `5.0`        | HTTP timeout pro fetch JWKS |
    67|| `AUTH_LEEWAY_SECONDS`               | `0`          | Tolerance hodinového rozdílu (clock skew) |
    68|| `AUTH_RESOURCE_CLIENT`              | `None`       | Pokud nastaveno, mergne i `resource_access.<klient>.roles` |
    69|| `AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS` | `0`     | Opt-in: refresh při neznámém `kid` 1× za N sekund |
    70|
    71|### Odvozené hodnoty
    72|
    73|- **Issuer:** `{AUTH_KEYCLOAK_URL}/realms/{AUTH_REALM}`
    74|- **JWKS URL:** `{issuer}/protocol/openid-connect/certs`
    75|
    76|---
    77|
    78|## Použití ve FastAPI
    79|
    80|### Ověřený uživatel
    81|
    82|```python
    83|from fastapi import Depends, FastAPI
    84|from auth_lib import AuthenticatedUser, get_current_user, install_exception_handlers
    85|
    86|app = FastAPI()
    87|install_exception_handlers(app)
    88|
    89|@app.get("/me")
    90|async def me(user: AuthenticatedUser = Depends(get_current_user)) -> AuthenticatedUser:
    91|    return user
    92|```
    93|
    94|### Role guard
    95|
    96|```python
    97|from auth_lib import require_role, require_any_role, require_all_roles
    98|
    99|@app.delete("/users/{id}")
   100|async def delete_user(
   101|    id: str,
   102|    user: AuthenticatedUser = Depends(require_role("admin")),
   103|):
   104|    ...
   105|
   106|@app.get("/content")
   107|async def list_content(
   108|    user: AuthenticatedUser = Depends(require_any_role(["editor", "viewer"])),
   109|):
   110|    ...
   111|```
   112|
   113|### Konverze do SYSNET `UserType`
   114|
   115|```python
   116|from auth_lib import get_current_user
   117|
   118|@app.get("/user-profile")
   119|async def profile(user = Depends(get_current_user)):
   120|    sysnet_user = user.to_user_type()   # sysnet_pyutils.UserType
   121|    return sysnet_user
   122|```
   123|
   124|Mapping: `sub → identifier`, `preferred_username → name`, `email → email`, `given_name → name_first`, `family_name → name_last`, `name → name_full`.
   125|
   126|---
   127|
   128|## Observability
   129|
   130|Knihovna neví, co je Prometheus / OpenTelemetry / strukturovaný log. Místo toho exponuje **pub-sub hooky**, které si konzument zaregistruje:
   131|
   132|```python
   133|from auth_lib import on_token_validated, on_token_rejected, on_jwks_refresh
   134|
   135|@on_token_validated
   136|def _ok(user):
   137|    metrics.incr("auth.ok", tags={"sub": user.sub})
   138|
   139|@on_token_rejected
   140|def _err(exc):
   141|    metrics.incr("auth.err", tags={"type": type(exc).__name__})
   142|
   143|@on_jwks_refresh
   144|def _jwks(n):
   145|    metrics.gauge("auth.jwks.keys", n)
   146|```
   147|
   148|Výjimka v hooku **nikdy neshodí validaci** (hooky jsou best-effort).
   149|
   150|### Rychlé zapnutí přes SYSNET logger
   151|
   152|```python
   153|from auth_lib import install_sysnet_logging
   154|install_sysnet_logging()  # zapíše INFO/WARNING přes sysnet_pyutils.Log
   155|```
   156|
   157|Idempotentní — druhé volání už hooky znovu neregistruje.
   158|
   159|---
   160|
   161|## Výjimky
   162|
   163|| Výjimka                  | HTTP | Kdy                                                     |
   164||--------------------------|------|---------------------------------------------------------|
   165|| `InvalidTokenError`      | 401  | špatný podpis, expirace, `iss`, `aud`, neznámý `kid`, … |
   166|| `MissingRoleError`       | 403  | uživatel je ověřen, ale chybí mu role                   |
   167|| `AuthConfigurationError` | 500  | nedostupný JWKS, špatné URL, malformed response         |
   168|
   169|Všechny dědí z `AuthError`.
   170|
   171|`install_exception_handlers(app)` registruje handler, který vrací **`sysnet_pyutils.ErrorModel`**:
   172|
   173|```json
   174|{"code": 401, "message": "Token has expired"}
   175|```
   176|
   177|Jednotný formát chyb napříč SYSNET službami.
   178|
   179|---
   180|
   181|## JWKS caching
   182|
   183|- In-memory TTL cache (default 300 s).
   184|- Lazy fetch při prvním volání.
   185|- **Single-flight:** souběh N requestů při cold/expired cache spustí právě jeden fetch.
   186|- **Fresh cache je autoritativní** — kid miss = `InvalidTokenError`. Brání DoS přes náhodné kidy.
   187|- **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íčů.
   188|- Lazy init `asyncio.Lock` — kompatibilní s Pythonem 3.14 (neváže lock na event loop z doby importu).
   189|- Žádný background task, žádný disk.
   190|
   191|---
   192|
   193|## Bezpečnostní poznámky
   194|
   195|- Ověřujeme vždy **podpis, issuer i audience**.
   196|- Výchozí `leeway=0`. Zapnout jen při doloženém clock skew.
   197|- Pouze `RS256` výchozí, `none` ani HS256 nepovolujeme bez dobrého důvodu.
   198|- Čteme **pouze `Authorization: Bearer`** (žádné cookies, žádné `X-*` hlavičky).
   199|- Tokeny se nelogují. Diagnostika přes `sub` / `jti`.
   200|- JWKS fetch má timeout → nedostupný Keycloak vrátí 500, ne čekání donekonečna.
   201|
   202|---
   203|
   204|## Struktura projektu
   205|
   206|```
   207|auth_lib/
   208|├── auth_lib/
   209|│   ├── __init__.py          # veřejné re-exporty (__all__)
   210|│   ├── config.py            # AuthSettings
   211|│   ├── exceptions.py        # AuthError + to_error_model()
   212|│   ├── models.py            # AuthenticatedUser + to_user_type()
   213|│   ├── jwks.py              # async JWKS cache + cooldown
   214|│   ├── dependencies.py      # get_current_user, install_exception_handlers
   215|│   ├── roles.py             # require_role / require_any_role / require_all_roles
   216|│   ├── observability.py     # hook registry + install_sysnet_logging()
   217|│   └── py.typed             # PEP 561 marker
   218|├── tests/
   219|├── pyproject.toml           # sysnet-auth, Python 3.11-3.14
   220|├── CHANGELOG.md
   221|├── README.md
   222|└── .gitignore
   223|```
   224|
   225|---
   226|
   227|## Testování
   228|
   229|```bash
   230|pip install -e ".[dev]"
   231|pytest --cov=auth_lib --cov-report=term-missing
   232|```
   233|
   234|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í.**
   235|
   236|---
   237|
   238|## Architektonický princip
   239|
   240|> **Tato knihovna je jediným místem, kde se řeší validace identity uživatele.**
   241|
   242|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.
   243|
## Verze 0.2.4 — novinky

### Offline režim (bez Keycloaku)
Nastav `AUTH_VERIFY_KEYCLOAK=False` pro přeskočení ověřování podpisu. Vhodné pro lokální vývoj, nikdy ne v produkci.

### Podpora Keycloak audience listu
Keycloak standardně vrací `aud` jako seznam (`["client-id", "account"]`). Knihovna to od 0.2.4 zpracuje správně.

