Skip to main content

sysnet-auth (import path: auth_lib)

GitHub PyPI - Version Python Version FastAPI Tests Coverage License

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. Bez tokenu nebo při vypnuté autentizaci vrací anonymní identitu ANONYMOUS.


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).

pip install sysnet-auth
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.

Povinná konfigurace při zapnuté autentizaci

AUTH_KEYCLOAK_URL, AUTH_REALM a AUTH_AUDIENCE jsou povinné jen pokud AUTH_ENABLED=True (nebo 1). Při výchozím AUTH_ENABLED=False není potřeba konfigurace Keycloaku a všichni uživatelé jsou ANONYMOUS. Při zapnuté autentizaci chybějící hodnoty způsobí validační chybu konfigurace, nikoli tichý přechod na anonymní identitu. To platí i pro offline režim AUTH_VERIFY_KEYCLOAK=False.

Proměnná Default Popis
AUTH_KEYCLOAK_URL "" Základní URL Keycloaku (https://kc.example.cz); povinná při AUTH_ENABLED=True
AUTH_REALM "" Název Keycloak realm; povinná při AUTH_ENABLED=True
AUTH_AUDIENCE "" Očekávaný aud claim (typicky client_id API); povinná při AUTH_ENABLED=True

Volitelné proměnné

Proměnná Default Popis
AUTH_ENABLED False Zapíná/vypíná autentizaci: False = všichni ANONYMOUS, True = validace dodaného JWT
AUTH_VERIFY_KEYCLOAK True False = offline vývojový režim bez načítání JWKS a ověření podpisu; NIKDY v produkci
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

Rychlý start — prostředí

Před spuštěním aplikace nastav proměnné prostředí pro svůj Keycloak. Bez AUTH_ENABLED=1 zůstává autentizace vypnutá, i když pošleš Bearer token.

export AUTH_ENABLED=1
export AUTH_KEYCLOAK_URL=https://kc.example.cz
export AUTH_REALM=myrealm
export AUTH_AUDIENCE=myapi
export AUTH_VERIFY_KEYCLOAK=True

Ověřený uživatel

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

S platným Bearer tokenem a zapnutou autentizací vrací endpoint ověřeného uživatele. Samotné Depends(get_current_user) však přihlášení nevynucuje — bez tokenu vrací anonymní identitu.

Anonymní uživatel a vypnutá autentizace

Pokud token chybí (nebo je prázdný) NEBO AUTH_ENABLED=False, get_current_user vrací singleton ANONYMOUS: sub='anonymous', is_anonymous=True, roles=[]. Při vypnuté autentizaci se dodaný token ignoruje a JWT se nevaliduje.

from fastapi import Depends, FastAPI
from auth_lib import ANONYMOUS, ANONYMOUS_SUB, AuthenticatedUser, get_current_user

app = FastAPI()

assert ANONYMOUS.sub == ANONYMOUS_SUB == "anonymous"
assert ANONYMOUS.is_anonymous is True

@app.get("/resource")
async def resource(user: AuthenticatedUser = Depends(get_current_user)):
    if user.is_anonymous:
        return {"access": "public only", "sub": ANONYMOUS_SUB}
    return {"access": "full", "sub": user.sub}
  • Při AUTH_ENABLED=True neplatný nebo expirovaný token stále vrací 401 — není nahrazen anonymní identitou.
  • Anonymní uživatel nemá role; role guard proto vrací 403.
  • Endpointy vyžadující přihlášení musí explicitně odmítnout user.is_anonymous nebo použít odpovídající role guard.

Hotový /whoami endpoint

Není potřeba psát vlastní endpoint pro zjištění identity. Knihovna exportuje volitelný auth_router:

from fastapi import FastAPI
from auth_lib import auth_router, install_exception_handlers

app = FastAPI()
install_exception_handlers(app)
app.include_router(auth_router, prefix='/auth', tags=['auth'])
# GET /auth/whoami → AuthenticatedUser nebo ANONYMOUS

GET /auth/whoami vrací JSON modelu AuthenticatedUser, včetně pole is_anonymous. Bez tokenu nebo při vypnuté autentizaci vrací ANONYMOUS (sub="anonymous", is_anonymous=true). Prefix i tagy můžeš upravit podle své aplikace.

Role guard

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

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:

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

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 chybí požadovaná role, včetně anonymního uživatele
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:

{"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í vyžadujícím ověření podpisu (AUTH_ENABLED=True, dodaný token, AUTH_VERIFY_KEYCLOAK=True).
  • 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

  • Při AUTH_ENABLED=True a AUTH_VERIFY_KEYCLOAK=True ověřujeme podpis, issuer i audience. Audience může být řetězec nebo seznam obsahující očekávané AUTH_AUDIENCE.
  • Pro produkční autentizaci nastav AUTH_ENABLED=1 a ponech AUTH_VERIFY_KEYCLOAK=True. Offline režim vypíná ověření podpisu a není bezpečný pro produkci.
  • Výchozí AUTH_ENABLED=False znamená anonymní identitu pro všechny požadavky, nikoli ochranu endpointů. Přístup řiď explicitní kontrolou identity nebo rolí.
  • 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 <token> (žá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, AUTH_ENABLED, AUTH_VERIFY_KEYCLOAK
│   ├── exceptions.py        # AuthError + to_error_model()
│   ├── models.py            # AuthenticatedUser, is_anonymous, ANONYMOUS, ANONYMOUS_SUB
│   ├── jwks.py              # async JWKS cache + cooldown
│   ├── dependencies.py      # get_current_user, install_exception_handlers
│   ├── router.py            # auth_router s GET /whoami
│   ├── 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í

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. 71 testů, 98 % pokrytí. Pokrývají také anonymní identitu, přepínač autentizace, /whoami, offline režim a audience jako seznam.


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.

Verze 0.3.0 — novinky oproti 0.2.0

Anonymní identita a přepínač autentizace

ANONYMOUS singleton a pole is_anonymous sjednocují chování bez tokenu. AUTH_ENABLED má výchozí hodnotu False pro zpětně kompatibilní zapojení do služeb; JWT validaci aktivuje AUTH_ENABLED=1.

Hotový router

auth_router poskytuje GET /whoami pro vrácení aktuální identity bez vlastní implementace endpointu.

Offline režim (bez Keycloaku)

Nastav AUTH_ENABLED=1 a AUTH_VERIFY_KEYCLOAK=False pro dekódování tokenu bez JWKS a ověřování podpisu. Konfigurace URL, realm a audience zůstává povinná; kontroly issueru, audience a časových claimů zůstávají aktivní. Vhodné pouze pro lokální vývoj, NIKDY v produkci.

Podpora Keycloak audience listu

Keycloak standardně vrací aud jako seznam (["client-id", "account"]). Knihovna správně zpracuje jak řetězec, tak seznam; očekávané AUTH_AUDIENCE musí odpovídat řetězci nebo být jednou z položek seznamu.

Metadata

Release files for sysnet-auth 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sysnet-auth 0.3.0
File Size Uploaded
sysnet_auth-0.3.0.tar.gz 43.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sysnet-auth 0.3.0
File Interpreter ABI Platform
sysnet_auth-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 76.2 kB

Release files / sysnet_auth-0.3.0.tar.gz

Download URL sysnet_auth-0.3.0.tar.gz
Size 43.6 kB
Tags Source
SHA-256 checksum
How to use checksums
295e30e5c6f59c0b80f6d94f3d18aa90871e118386369cbde3a8719ce5b751c7
BLAKE2b-256 checksum
How to use checksums
b3464317dde810176ba9c41de42b6109192bf216bf1af05b5b4afdcd5b17c740
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sysnet_auth-0.3.0-py3-none-any.whl

Download URL sysnet_auth-0.3.0-py3-none-any.whl
Size 32.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7d0c0fa94a023f7f9b92a5de9ca5dc36aa9f014983b0b3d1f54c47d02d36ed3
BLAKE2b-256 checksum
How to use checksums
b8b08d0c7cb9fe0aa78a78f10247d093c3a35ea739739fc53cd4aa6ecfa009c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.14

2 release files

0.2.13

2 release files

0.2.12

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page