sysnet-auth 0.2.9__tar.gz → 0.2.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. sysnet_auth-0.2.10/PKG-INFO +289 -0
  2. sysnet_auth-0.2.10/README.md +251 -0
  3. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/pyproject.toml +1 -1
  4. sysnet_auth-0.2.10/sysnet_auth.egg-info/PKG-INFO +289 -0
  5. sysnet_auth-0.2.9/PKG-INFO +0 -289
  6. sysnet_auth-0.2.9/README.md +0 -251
  7. sysnet_auth-0.2.9/sysnet_auth.egg-info/PKG-INFO +0 -289
  8. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/LICENSE +0 -0
  9. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/__init__.py +0 -0
  10. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/config.py +0 -0
  11. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/dependencies.py +0 -0
  12. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/exceptions.py +0 -0
  13. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/jwks.py +0 -0
  14. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/models.py +0 -0
  15. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/observability.py +0 -0
  16. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/py.typed +0 -0
  17. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/auth_lib/roles.py +0 -0
  18. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/setup.cfg +0 -0
  19. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/sysnet_auth.egg-info/SOURCES.txt +0 -0
  20. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/sysnet_auth.egg-info/dependency_links.txt +0 -0
  21. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/sysnet_auth.egg-info/requires.txt +0 -0
  22. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/sysnet_auth.egg-info/top_level.txt +0 -0
  23. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/tests/test_edges.py +0 -0
  24. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/tests/test_features.py +0 -0
  25. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/tests/test_jwt.py +0 -0
  26. {sysnet_auth-0.2.9 → sysnet_auth-0.2.10}/tests/test_roles.py +0 -0
@@ -0,0 +1,289 @@
1
+ Metadata-Version: 2.4
2
+ Name: sysnet-auth
3
+ Version: 0.2.10
4
+ Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
5
+ Author: SYSNET s.r.o.
6
+ License: GNU Affero General Public License v3
7
+ Project-URL: Homepage, https://sysnet.cz
8
+ Project-URL: Repository, https://github.com/SYSNET-CZ/auth-lib
9
+ Project-URL: Documentation, https://github.com/SYSNET-CZ/auth-lib#readme
10
+ Project-URL: Bug Tracker, https://github.com/SYSNET-CZ/auth-lib/issues
11
+ Keywords: fastapi,keycloak,jwt,oidc,auth,sysnet
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
18
+ Classifier: Topic :: Security
19
+ Classifier: Typing :: Typed
20
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: fastapi<1,>=0.115
25
+ Requires-Dist: pydantic<3,>=2.9
26
+ Requires-Dist: pydantic-settings<3,>=2.3
27
+ Requires-Dist: pyjwt[crypto]<3,>=2.9
28
+ Requires-Dist: httpx<1,>=0.27
29
+ Requires-Dist: sysnet-pyutils>=0.1
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest<10,>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio<1,>=0.23; extra == "dev"
33
+ Requires-Dist: pytest-cov<8,>=5.0; extra == "dev"
34
+ Requires-Dist: cryptography<48,>=42.0; extra == "dev"
35
+ Requires-Dist: mypy<2,>=1.11; extra == "dev"
36
+ Requires-Dist: ruff<1,>=0.6; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # sysnet-auth (import path: `auth_lib`)
40
+
41
+ [![GitHub](https://img.shields.io/badge/GitHub-SYSNET--CZ%2Fauth--lib-181717?style=flat&logo=github)](https://github.com/SYSNET-CZ/auth-lib)
42
+ [![PyPI - Version](https://img.shields.io/pypi/v/sysnet-auth)](https://pypi.org/project/sysnet-auth/)
43
+ [![Python Version](https://img.shields.io/pypi/pyversions/sysnet-auth)](https://pypi.org/project/sysnet-auth/)
44
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.115%2B-009688?style=flat&logo=fastapi)](https://fastapi.tiangolo.com)
45
+ [![Tests](https://img.shields.io/badge/tests-58%20passed-brightgreen)](https://github.com/SYSNET-CZ/auth-lib)
46
+ [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen)](https://github.com/SYSNET-CZ/auth-lib)
47
+ [![License](https://img.shields.io/badge/license-AGPL--3-blue)](https://github.com/SYSNET-CZ/auth-lib/blob/main/LICENSE)
48
+
49
+ 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.
50
+
51
+ 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.
52
+
53
+ ---
54
+
55
+ ## Proč existuje
56
+
57
+ 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ě.
58
+
59
+ ---
60
+
61
+ ## Instalace
62
+
63
+ Balíček je publikovaný jako **`sysnet-auth`**, importuje se jako **`auth_lib`**
64
+ (běžný pattern: distribuční jméno ≠ import jméno).
65
+
66
+ ```bash
67
+ pip install sysnet-auth
68
+ ```
69
+
70
+ ```python
71
+ from auth_lib import get_current_user, require_role
72
+ ```
73
+
74
+ ### Python
75
+
76
+ Vyžaduje **Python ≥ 3.11**, plně testováno proti **3.11 – 3.14**.
77
+
78
+ ### Runtime závislosti
79
+
80
+ | Balíček | Role |
81
+ |---------------------|------------------------------------------------|
82
+ | `fastapi` | DI / exception handlery |
83
+ | `pydantic` v2 | modely, validace |
84
+ | `pydantic-settings` | konfigurace přes env |
85
+ | `pyjwt[crypto]` | dekódování + validace JWT |
86
+ | `httpx` | async HTTP klient pro JWKS endpoint |
87
+ | `sysnet-pyutils` | sdílené SYSNET modely (`UserType`, `ErrorModel`, `Log`) |
88
+
89
+ > **Volba JWT knihovny.** `python-jose` je od 2021 neudržovaný. Používáme **PyJWT** — aktivně udržovaný de facto standard pro JWT v Pythonu.
90
+
91
+ ---
92
+
93
+ ## Konfigurace
94
+
95
+ 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.
96
+
97
+ | Proměnná | Default | Popis |
98
+ |-------------------------------------|--------------|-------|
99
+ | `AUTH_KEYCLOAK_URL` | — | Základní URL Keycloaku (`https://kc.example.cz`) |
100
+ | `AUTH_REALM` | — | Název Keycloak realm |
101
+ | `AUTH_AUDIENCE` | — | Očekávaný `aud` claim (typicky `client_id` API) |
102
+ | `AUTH_ALGORITHMS` | `["RS256"]` | Povolené podpisové algoritmy (CSV nebo JSON) |
103
+ | `AUTH_JWKS_CACHE_SECONDS` | `300` | TTL JWKS cache |
104
+ | `AUTH_JWKS_HTTP_TIMEOUT_SECONDS` | `5.0` | HTTP timeout pro fetch JWKS |
105
+ | `AUTH_LEEWAY_SECONDS` | `0` | Tolerance hodinového rozdílu (clock skew) |
106
+ | `AUTH_RESOURCE_CLIENT` | `None` | Pokud nastaveno, mergne i `resource_access.<klient>.roles` |
107
+ | `AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS` | `0` | Opt-in: refresh při neznámém `kid` 1× za N sekund |
108
+
109
+ ### Odvozené hodnoty
110
+
111
+ - **Issuer:** `{AUTH_KEYCLOAK_URL}/realms/{AUTH_REALM}`
112
+ - **JWKS URL:** `{issuer}/protocol/openid-connect/certs`
113
+
114
+ ---
115
+
116
+ ## Použití ve FastAPI
117
+
118
+ ### Ověřený uživatel
119
+
120
+ ```python
121
+ from fastapi import Depends, FastAPI
122
+ from auth_lib import AuthenticatedUser, get_current_user, install_exception_handlers
123
+
124
+ app = FastAPI()
125
+ install_exception_handlers(app)
126
+
127
+ @app.get("/me")
128
+ async def me(user: AuthenticatedUser = Depends(get_current_user)) -> AuthenticatedUser:
129
+ return user
130
+ ```
131
+
132
+ ### Role guard
133
+
134
+ ```python
135
+ from auth_lib import require_role, require_any_role, require_all_roles
136
+
137
+ @app.delete("/users/{id}")
138
+ async def delete_user(
139
+ id: str,
140
+ user: AuthenticatedUser = Depends(require_role("admin")),
141
+ ):
142
+ ...
143
+
144
+ @app.get("/content")
145
+ async def list_content(
146
+ user: AuthenticatedUser = Depends(require_any_role(["editor", "viewer"])),
147
+ ):
148
+ ...
149
+ ```
150
+
151
+ ### Konverze do SYSNET `UserType`
152
+
153
+ ```python
154
+ from auth_lib import get_current_user
155
+
156
+ @app.get("/user-profile")
157
+ async def profile(user = Depends(get_current_user)):
158
+ sysnet_user = user.to_user_type() # sysnet_pyutils.UserType
159
+ return sysnet_user
160
+ ```
161
+
162
+ Mapping: `sub → identifier`, `preferred_username → name`, `email → email`, `given_name → name_first`, `family_name → name_last`, `name → name_full`.
163
+
164
+ ---
165
+
166
+ ## Observability
167
+
168
+ Knihovna neví, co je Prometheus / OpenTelemetry / strukturovaný log. Místo toho exponuje **pub-sub hooky**, které si konzument zaregistruje:
169
+
170
+ ```python
171
+ from auth_lib import on_token_validated, on_token_rejected, on_jwks_refresh
172
+
173
+ @on_token_validated
174
+ def _ok(user):
175
+ metrics.incr("auth.ok", tags={"sub": user.sub})
176
+
177
+ @on_token_rejected
178
+ def _err(exc):
179
+ metrics.incr("auth.err", tags={"type": type(exc).__name__})
180
+
181
+ @on_jwks_refresh
182
+ def _jwks(n):
183
+ metrics.gauge("auth.jwks.keys", n)
184
+ ```
185
+
186
+ Výjimka v hooku **nikdy neshodí validaci** (hooky jsou best-effort).
187
+
188
+ ### Rychlé zapnutí přes SYSNET logger
189
+
190
+ ```python
191
+ from auth_lib import install_sysnet_logging
192
+ install_sysnet_logging() # zapíše INFO/WARNING přes sysnet_pyutils.Log
193
+ ```
194
+
195
+ Idempotentní — druhé volání už hooky znovu neregistruje.
196
+
197
+ ---
198
+
199
+ ## Výjimky
200
+
201
+ | Výjimka | HTTP | Kdy |
202
+ |--------------------------|------|---------------------------------------------------------|
203
+ | `InvalidTokenError` | 401 | špatný podpis, expirace, `iss`, `aud`, neznámý `kid`, … |
204
+ | `MissingRoleError` | 403 | uživatel je ověřen, ale chybí mu role |
205
+ | `AuthConfigurationError` | 500 | nedostupný JWKS, špatné URL, malformed response |
206
+
207
+ Všechny dědí z `AuthError`.
208
+
209
+ `install_exception_handlers(app)` registruje handler, který vrací **`sysnet_pyutils.ErrorModel`**:
210
+
211
+ ```json
212
+ {"code": 401, "message": "Token has expired"}
213
+ ```
214
+
215
+ Jednotný formát chyb napříč SYSNET službami.
216
+
217
+ ---
218
+
219
+ ## JWKS caching
220
+
221
+ - In-memory TTL cache (default 300 s).
222
+ - Lazy fetch při prvním volání.
223
+ - **Single-flight:** souběh N requestů při cold/expired cache spustí právě jeden fetch.
224
+ - **Fresh cache je autoritativní** — kid miss = `InvalidTokenError`. Brání DoS přes náhodné kidy.
225
+ - **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íčů.
226
+ - Lazy init `asyncio.Lock` — kompatibilní s Pythonem 3.14 (neváže lock na event loop z doby importu).
227
+ - Žádný background task, žádný disk.
228
+
229
+ ---
230
+
231
+ ## Bezpečnostní poznámky
232
+
233
+ - Ověřujeme vždy **podpis, issuer i audience**.
234
+ - Výchozí `leeway=0`. Zapnout jen při doloženém clock skew.
235
+ - Pouze `RS256` výchozí, `none` ani HS256 nepovolujeme bez dobrého důvodu.
236
+ - Čteme **pouze `Authorization: Bearer`** (žádné cookies, žádné `X-*` hlavičky).
237
+ - Tokeny se nelogují. Diagnostika přes `sub` / `jti`.
238
+ - JWKS fetch má timeout → nedostupný Keycloak vrátí 500, ne čekání donekonečna.
239
+
240
+ ---
241
+
242
+ ## Struktura projektu
243
+
244
+ ```
245
+ auth_lib/
246
+ ├── auth_lib/
247
+ │ ├── __init__.py # veřejné re-exporty (__all__)
248
+ │ ├── config.py # AuthSettings
249
+ │ ├── exceptions.py # AuthError + to_error_model()
250
+ │ ├── models.py # AuthenticatedUser + to_user_type()
251
+ │ ├── jwks.py # async JWKS cache + cooldown
252
+ │ ├── dependencies.py # get_current_user, install_exception_handlers
253
+ │ ├── roles.py # require_role / require_any_role / require_all_roles
254
+ │ ├── observability.py # hook registry + install_sysnet_logging()
255
+ │ └── py.typed # PEP 561 marker
256
+ ├── tests/
257
+ ├── pyproject.toml # sysnet-auth, Python 3.11-3.14
258
+ ├── CHANGELOG.md
259
+ ├── README.md
260
+ └── .gitignore
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Testování
266
+
267
+ ```bash
268
+ pip install -e ".[dev]"
269
+ pytest --cov=auth_lib --cov-report=term-missing
270
+ ```
271
+
272
+ 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í.**
273
+
274
+ ---
275
+
276
+ ## Architektonický princip
277
+
278
+ > **Tato knihovna je jediným místem, kde se řeší validace identity uživatele.**
279
+
280
+ 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.
281
+
282
+ ## Verze 0.2.4 — novinky
283
+
284
+ ### Offline režim (bez Keycloaku)
285
+ Nastav `AUTH_VERIFY_KEYCLOAK=False` pro přeskočení ověřování podpisu. Vhodné pro lokální vývoj, nikdy ne v produkci.
286
+
287
+ ### Podpora Keycloak audience listu
288
+ Keycloak standardně vrací `aud` jako seznam (`["client-id", "account"]`). Knihovna to od 0.2.4 zpracuje správně.
289
+
@@ -0,0 +1,251 @@
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
+
244
+ ## Verze 0.2.4 — novinky
245
+
246
+ ### Offline režim (bez Keycloaku)
247
+ Nastav `AUTH_VERIFY_KEYCLOAK=False` pro přeskočení ověřování podpisu. Vhodné pro lokální vývoj, nikdy ne v produkci.
248
+
249
+ ### Podpora Keycloak audience listu
250
+ Keycloak standardně vrací `aud` jako seznam (`["client-id", "account"]`). Knihovna to od 0.2.4 zpracuje správně.
251
+
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  # Distribucni nazev na internim PyPi. Import path zustava \`\`auth_lib\`\`.
7
7
  name = "sysnet-auth"
8
- version = "0.2.9"
8
+ version = "0.2.10"
9
9
  description = "Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT)."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"