sysnet-auth 0.2.3__tar.gz → 0.2.4__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.
- {sysnet_auth-0.2.3/sysnet_auth.egg-info → sysnet_auth-0.2.4}/PKG-INFO +12 -12
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/__init__.py +2 -1
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/config.py +11 -1
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/dependencies.py +28 -16
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/observability.py +3 -3
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/pyproject.toml +24 -14
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4/sysnet_auth.egg-info}/PKG-INFO +12 -12
- sysnet_auth-0.2.4/sysnet_auth.egg-info/requires.txt +14 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_edges.py +0 -3
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_features.py +11 -10
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_jwt.py +12 -7
- sysnet_auth-0.2.3/sysnet_auth.egg-info/requires.txt +0 -14
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/LICENSE +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/README.md +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/exceptions.py +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/jwks.py +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/models.py +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/py.typed +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/roles.py +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/setup.cfg +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/sysnet_auth.egg-info/SOURCES.txt +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/sysnet_auth.egg-info/dependency_links.txt +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/sysnet_auth.egg-info/top_level.txt +0 -0
- {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_roles.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sysnet-auth
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
|
|
5
5
|
Author: SYSNET s.r.o.
|
|
6
6
|
License: GNU Affero General Public License v3
|
|
@@ -21,19 +21,19 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: fastapi
|
|
25
|
-
Requires-Dist: pydantic
|
|
26
|
-
Requires-Dist: pydantic-settings
|
|
27
|
-
Requires-Dist: pyjwt[crypto]
|
|
28
|
-
Requires-Dist: httpx
|
|
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
29
|
Requires-Dist: sysnet-pyutils>=0.1
|
|
30
30
|
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: pytest
|
|
32
|
-
Requires-Dist: pytest-asyncio
|
|
33
|
-
Requires-Dist: pytest-cov
|
|
34
|
-
Requires-Dist: cryptography
|
|
35
|
-
Requires-Dist: mypy
|
|
36
|
-
Requires-Dist: ruff
|
|
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
37
|
Dynamic: license-file
|
|
38
38
|
|
|
39
39
|
# sysnet-auth (import path: `auth_lib`)
|
|
@@ -9,7 +9,8 @@ Verzovani:
|
|
|
9
9
|
se cte pres ``importlib.metadata``, aby nedochazelo k drift mezi nimi.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from importlib.metadata import PackageNotFoundError
|
|
12
|
+
from importlib.metadata import PackageNotFoundError
|
|
13
|
+
from importlib.metadata import version as _pkg_version
|
|
13
14
|
|
|
14
15
|
from auth_lib.config import AuthSettings, get_settings
|
|
15
16
|
from auth_lib.dependencies import (
|
|
@@ -76,6 +76,15 @@ class AuthSettings(BaseSettings):
|
|
|
76
76
|
),
|
|
77
77
|
)
|
|
78
78
|
|
|
79
|
+
verify_keycloak: bool = Field(
|
|
80
|
+
default=True,
|
|
81
|
+
description=(
|
|
82
|
+
"Pokud False, preskoci se overovani podpisu JWT vuci Keycloak JWKS. "
|
|
83
|
+
"Vhodne pro lokalni vyvoj a testovani bez beziciho Keycloaku. "
|
|
84
|
+
"NIKDY nepouzivat v produkci."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
79
88
|
@field_validator("keycloak_url")
|
|
80
89
|
@classmethod
|
|
81
90
|
def _strip_trailing_slash(cls, v: str) -> str:
|
|
@@ -89,6 +98,7 @@ class AuthSettings(BaseSettings):
|
|
|
89
98
|
s = v.strip()
|
|
90
99
|
if s.startswith("["):
|
|
91
100
|
import json
|
|
101
|
+
|
|
92
102
|
try:
|
|
93
103
|
parsed = json.loads(s)
|
|
94
104
|
if isinstance(parsed, list):
|
|
@@ -111,4 +121,4 @@ class AuthSettings(BaseSettings):
|
|
|
111
121
|
@lru_cache(maxsize=1)
|
|
112
122
|
def get_settings() -> AuthSettings:
|
|
113
123
|
"""Vrati cachovanou instanci settings. Reset pres get_settings.cache_clear()."""
|
|
114
|
-
return AuthSettings()
|
|
124
|
+
return AuthSettings()
|
|
@@ -61,33 +61,48 @@ async def _decode_token(
|
|
|
61
61
|
raise InvalidTokenError(f"Malformed token header: {exc}") from exc
|
|
62
62
|
|
|
63
63
|
kid = header.get("kid")
|
|
64
|
-
if not kid:
|
|
64
|
+
if settings.verify_keycloak and not kid:
|
|
65
65
|
raise InvalidTokenError("Token header missing 'kid'")
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
public_key = None
|
|
68
|
+
if settings.verify_keycloak:
|
|
69
|
+
jwk = await jwks_client.get_key(kid)
|
|
70
|
+
public_key = _jwk_to_public_key(jwk)
|
|
69
71
|
|
|
70
72
|
try:
|
|
73
|
+
# Robustní audience: PyJWT neumí nativně list v aud, pokud hledáme string.
|
|
74
|
+
# Vypneme nativní kontrolu a uděláme ji ručně níže.
|
|
75
|
+
decode_options = {
|
|
76
|
+
"require": ["exp", "iat", "iss", "aud", "sub"],
|
|
77
|
+
"verify_signature": settings.verify_keycloak,
|
|
78
|
+
"verify_exp": True,
|
|
79
|
+
"verify_iat": True,
|
|
80
|
+
"verify_iss": True,
|
|
81
|
+
"verify_aud": False, # Manuálně
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
claims = jwt.decode(
|
|
72
85
|
token,
|
|
73
86
|
key=public_key,
|
|
74
87
|
algorithms=settings.algorithms,
|
|
75
|
-
audience=settings.audience,
|
|
76
88
|
issuer=settings.issuer,
|
|
77
89
|
leeway=settings.leeway_seconds,
|
|
78
|
-
options=
|
|
79
|
-
"require": ["exp", "iat", "iss", "aud", "sub"],
|
|
80
|
-
"verify_signature": True,
|
|
81
|
-
"verify_exp": True,
|
|
82
|
-
"verify_iat": True,
|
|
83
|
-
"verify_iss": True,
|
|
84
|
-
"verify_aud": True,
|
|
85
|
-
},
|
|
90
|
+
options=decode_options,
|
|
86
91
|
)
|
|
92
|
+
|
|
93
|
+
# Manuální kontrola audience (podpora pro list i string)
|
|
94
|
+
token_aud = claims.get("aud")
|
|
95
|
+
target_aud = settings.audience
|
|
96
|
+
if isinstance(token_aud, list):
|
|
97
|
+
if target_aud not in token_aud:
|
|
98
|
+
raise jwt.InvalidAudienceError(f"Audience {target_aud!r} not in token {token_aud!r}")
|
|
99
|
+
elif token_aud != target_aud:
|
|
100
|
+
raise jwt.InvalidAudienceError(f"Audience mismatch: expected {target_aud!r}, got {token_aud!r}")
|
|
101
|
+
|
|
87
102
|
except jwt.ExpiredSignatureError as exc:
|
|
88
103
|
raise InvalidTokenError("Token has expired") from exc
|
|
89
104
|
except jwt.InvalidAudienceError as exc:
|
|
90
|
-
raise InvalidTokenError(
|
|
105
|
+
raise InvalidTokenError(str(exc)) from exc
|
|
91
106
|
except jwt.InvalidIssuerError as exc:
|
|
92
107
|
raise InvalidTokenError("Invalid token issuer") from exc
|
|
93
108
|
except jwt.InvalidSignatureError as exc:
|
|
@@ -101,7 +116,6 @@ async def _decode_token(
|
|
|
101
116
|
|
|
102
117
|
|
|
103
118
|
async def get_current_user(
|
|
104
|
-
request: Request,
|
|
105
119
|
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
|
|
106
120
|
settings: AuthSettings = Depends(get_settings),
|
|
107
121
|
) -> AuthenticatedUser:
|
|
@@ -114,8 +128,6 @@ async def get_current_user(
|
|
|
114
128
|
Pokud settings.resource_client je nastaveny, role se mergujou z:
|
|
115
129
|
realm_access.roles + resource_access.<resource_client>.roles
|
|
116
130
|
"""
|
|
117
|
-
_ = request
|
|
118
|
-
|
|
119
131
|
if credentials is None or not credentials.credentials:
|
|
120
132
|
exc = InvalidTokenError("Missing Authorization bearer token")
|
|
121
133
|
_emit_rejected(exc)
|
|
@@ -56,7 +56,7 @@ def remove_listener(fn: Callable[..., None]) -> None:
|
|
|
56
56
|
"""Odebere callback ze vsech registru."""
|
|
57
57
|
for registry in (_on_validated, _on_rejected, _on_jwks_refresh):
|
|
58
58
|
if fn in registry:
|
|
59
|
-
registry.remove(fn)
|
|
59
|
+
registry.remove(fn)
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def clear_listeners() -> None:
|
|
@@ -82,7 +82,7 @@ def install_sysnet_logging() -> None:
|
|
|
82
82
|
|
|
83
83
|
logger = Log().logger
|
|
84
84
|
|
|
85
|
-
def _on_ok(user:
|
|
85
|
+
def _on_ok(user: AuthenticatedUser) -> None:
|
|
86
86
|
logger.info("auth_lib: token validated (sub=%s, roles=%d)", user.sub, len(user.roles))
|
|
87
87
|
|
|
88
88
|
def _on_err(exc: BaseException) -> None:
|
|
@@ -104,7 +104,7 @@ def install_sysnet_logging() -> None:
|
|
|
104
104
|
# ----- interni emitery (vola knihovna) --------------------------------
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
def _emit_validated(user:
|
|
107
|
+
def _emit_validated(user: AuthenticatedUser) -> None:
|
|
108
108
|
for h in list(_on_validated):
|
|
109
109
|
try:
|
|
110
110
|
h(user)
|
|
@@ -3,9 +3,9 @@ requires = ["setuptools>=68", "wheel"]
|
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
|
-
# Distribucni nazev na internim PyPi. Import path zustava
|
|
6
|
+
# Distribucni nazev na internim PyPi. Import path zustava \`\`auth_lib\`\`.
|
|
7
7
|
name = "sysnet-auth"
|
|
8
|
-
version = "0.2.
|
|
8
|
+
version = "0.2.4"
|
|
9
9
|
description = "Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT)."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -27,22 +27,22 @@ classifiers = [
|
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
dependencies = [
|
|
30
|
-
"fastapi>=0.115",
|
|
31
|
-
"pydantic>=2.9",
|
|
32
|
-
"pydantic-settings>=2.3",
|
|
33
|
-
"pyjwt[crypto]>=2.9",
|
|
34
|
-
"httpx>=0.27",
|
|
30
|
+
"fastapi>=0.115,<1",
|
|
31
|
+
"pydantic>=2.9,<3",
|
|
32
|
+
"pydantic-settings>=2.3,<3",
|
|
33
|
+
"pyjwt[crypto]>=2.9,<3",
|
|
34
|
+
"httpx>=0.27,<1",
|
|
35
35
|
"sysnet-pyutils>=0.1",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
dev = [
|
|
40
|
-
"pytest>=8.0",
|
|
41
|
-
"pytest-asyncio>=0.23",
|
|
42
|
-
"pytest-cov>=5.0",
|
|
43
|
-
"cryptography>=42.0",
|
|
44
|
-
"mypy>=1.11",
|
|
45
|
-
"ruff>=0.6",
|
|
40
|
+
"pytest>=8.0,<10",
|
|
41
|
+
"pytest-asyncio>=0.23,<1",
|
|
42
|
+
"pytest-cov>=5.0,<8",
|
|
43
|
+
"cryptography>=42.0,<48",
|
|
44
|
+
"mypy>=1.11,<2",
|
|
45
|
+
"ruff>=0.6,<1",
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[project.urls]
|
|
@@ -60,7 +60,9 @@ exclude = ["tests*"]
|
|
|
60
60
|
"auth_lib" = ["py.typed"]
|
|
61
61
|
|
|
62
62
|
[tool.pytest.ini_options]
|
|
63
|
+
# Explicitne vyzadujeme asyncio pro testy
|
|
63
64
|
asyncio_mode = "auto"
|
|
65
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
64
66
|
testpaths = ["tests"]
|
|
65
67
|
addopts = "-ra --strict-markers"
|
|
66
68
|
|
|
@@ -70,7 +72,7 @@ target-version = "py311"
|
|
|
70
72
|
|
|
71
73
|
[tool.ruff.lint]
|
|
72
74
|
select = ["E", "F", "W", "I", "B", "UP", "ASYNC", "S", "SIM"]
|
|
73
|
-
ignore = ["S101"]
|
|
75
|
+
ignore = ["S101", "B008"]
|
|
74
76
|
|
|
75
77
|
[tool.ruff.lint.per-file-ignores]
|
|
76
78
|
"tests/*" = ["S", "B"]
|
|
@@ -84,6 +86,10 @@ show_error_codes = true
|
|
|
84
86
|
disallow_any_generics = true
|
|
85
87
|
plugins = ["pydantic.mypy"]
|
|
86
88
|
|
|
89
|
+
[[tool.mypy.overrides]]
|
|
90
|
+
module = ["tests.*", "scripts.*"]
|
|
91
|
+
ignore_errors = true
|
|
92
|
+
|
|
87
93
|
[[tool.mypy.overrides]]
|
|
88
94
|
module = "jwt.*"
|
|
89
95
|
ignore_missing_imports = true
|
|
@@ -91,3 +97,7 @@ ignore_missing_imports = true
|
|
|
91
97
|
[[tool.mypy.overrides]]
|
|
92
98
|
module = "sysnet_pyutils.*"
|
|
93
99
|
ignore_missing_imports = true
|
|
100
|
+
|
|
101
|
+
[[tool.mypy.overrides]]
|
|
102
|
+
module = "tomli.*"
|
|
103
|
+
ignore_missing_imports = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sysnet-auth
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
|
|
5
5
|
Author: SYSNET s.r.o.
|
|
6
6
|
License: GNU Affero General Public License v3
|
|
@@ -21,19 +21,19 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: fastapi
|
|
25
|
-
Requires-Dist: pydantic
|
|
26
|
-
Requires-Dist: pydantic-settings
|
|
27
|
-
Requires-Dist: pyjwt[crypto]
|
|
28
|
-
Requires-Dist: httpx
|
|
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
29
|
Requires-Dist: sysnet-pyutils>=0.1
|
|
30
30
|
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: pytest
|
|
32
|
-
Requires-Dist: pytest-asyncio
|
|
33
|
-
Requires-Dist: pytest-cov
|
|
34
|
-
Requires-Dist: cryptography
|
|
35
|
-
Requires-Dist: mypy
|
|
36
|
-
Requires-Dist: ruff
|
|
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
37
|
Dynamic: license-file
|
|
38
38
|
|
|
39
39
|
# sysnet-auth (import path: `auth_lib`)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fastapi<1,>=0.115
|
|
2
|
+
pydantic<3,>=2.9
|
|
3
|
+
pydantic-settings<3,>=2.3
|
|
4
|
+
pyjwt[crypto]<3,>=2.9
|
|
5
|
+
httpx<1,>=0.27
|
|
6
|
+
sysnet-pyutils>=0.1
|
|
7
|
+
|
|
8
|
+
[dev]
|
|
9
|
+
pytest<10,>=8.0
|
|
10
|
+
pytest-asyncio<1,>=0.23
|
|
11
|
+
pytest-cov<8,>=5.0
|
|
12
|
+
cryptography<48,>=42.0
|
|
13
|
+
mypy<2,>=1.11
|
|
14
|
+
ruff<1,>=0.6
|
|
@@ -13,8 +13,6 @@ Pokryva:
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
-
import types
|
|
17
|
-
from typing import Any
|
|
18
16
|
|
|
19
17
|
import httpx
|
|
20
18
|
import pytest
|
|
@@ -26,7 +24,6 @@ from auth_lib.config import AuthSettings
|
|
|
26
24
|
from auth_lib.dependencies import _jwk_to_public_key, install_exception_handlers
|
|
27
25
|
from auth_lib.jwks import JWKSClient, reset_jwks_client
|
|
28
26
|
|
|
29
|
-
|
|
30
27
|
# ----- _fetch happy path + malformed --------------------------------
|
|
31
28
|
|
|
32
29
|
|
|
@@ -13,7 +13,6 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import time
|
|
15
15
|
import types
|
|
16
|
-
from typing import Any
|
|
17
16
|
|
|
18
17
|
import httpx
|
|
19
18
|
import pytest
|
|
@@ -35,7 +34,6 @@ from auth_lib import (
|
|
|
35
34
|
)
|
|
36
35
|
from auth_lib import observability as obs
|
|
37
36
|
|
|
38
|
-
|
|
39
37
|
# ---------- Observability hooks ----------
|
|
40
38
|
|
|
41
39
|
|
|
@@ -115,6 +113,7 @@ def test_install_sysnet_logging_is_idempotent():
|
|
|
115
113
|
def test_jwks_refresh_hook_fires(test_settings, public_jwk):
|
|
116
114
|
"""Fire kdyz _fetch uspesne dobehne."""
|
|
117
115
|
import json
|
|
116
|
+
|
|
118
117
|
clear_listeners()
|
|
119
118
|
captured: list[int] = []
|
|
120
119
|
|
|
@@ -132,6 +131,7 @@ def test_jwks_refresh_hook_fires(test_settings, public_jwk):
|
|
|
132
131
|
http = httpx.AsyncClient(transport=_T())
|
|
133
132
|
|
|
134
133
|
import asyncio
|
|
134
|
+
|
|
135
135
|
async def run():
|
|
136
136
|
c = JWKSClient(test_settings, http_client=http)
|
|
137
137
|
await c.refresh()
|
|
@@ -194,9 +194,10 @@ def test_install_exception_handlers_returns_error_model_shape():
|
|
|
194
194
|
def test_resource_client_merges_client_roles(public_jwk, private_pem):
|
|
195
195
|
"""get_current_user pri AUTH_RESOURCE_CLIENT=my-api mergne client role."""
|
|
196
196
|
import jwt as pyjwt
|
|
197
|
+
|
|
198
|
+
from auth_lib import jwks as jwks_mod
|
|
197
199
|
from auth_lib.config import AuthSettings
|
|
198
200
|
from auth_lib.dependencies import get_current_user
|
|
199
|
-
from auth_lib import jwks as jwks_mod
|
|
200
201
|
from auth_lib.jwks import JWKSClient
|
|
201
202
|
|
|
202
203
|
settings = AuthSettings(
|
|
@@ -238,6 +239,7 @@ def test_resource_client_merges_client_roles(public_jwk, private_pem):
|
|
|
238
239
|
# novy prepsal po ni. Obnovim do puvodniho pri exitu.
|
|
239
240
|
try:
|
|
240
241
|
from auth_lib.config import get_settings
|
|
242
|
+
|
|
241
243
|
app.dependency_overrides[get_settings] = lambda: settings
|
|
242
244
|
c = TestClient(app)
|
|
243
245
|
r = c.get("/who", headers={"Authorization": f"Bearer {token}"})
|
|
@@ -265,7 +267,7 @@ async def test_kid_miss_cooldown_refreshes_when_enabled(public_jwk):
|
|
|
265
267
|
# Fresh cache ale bez hledaneho kidu
|
|
266
268
|
c._keys_by_kid = {"old-kid": {"kid": "old-kid"}}
|
|
267
269
|
c._expires_at = time.monotonic() + 3600
|
|
268
|
-
c._last_refresh_at =
|
|
270
|
+
c._last_refresh_at = -9999.0 # cooldown uplynul (zarucene na jakemkoliv systemu)
|
|
269
271
|
|
|
270
272
|
fetch_calls: list[int] = [0]
|
|
271
273
|
new_kid = public_jwk["kid"]
|
|
@@ -343,6 +345,7 @@ def test_emit_rejected_hook_error_is_swallowed(client, token_factory):
|
|
|
343
345
|
def test_emit_jwks_refresh_hook_error_is_swallowed(test_settings, public_jwk):
|
|
344
346
|
import asyncio
|
|
345
347
|
import json
|
|
348
|
+
|
|
346
349
|
clear_listeners()
|
|
347
350
|
|
|
348
351
|
@on_jwks_refresh
|
|
@@ -354,6 +357,7 @@ def test_emit_jwks_refresh_hook_error_is_swallowed(test_settings, public_jwk):
|
|
|
354
357
|
return httpx.Response(200, content=json.dumps({"keys": [public_jwk]}).encode())
|
|
355
358
|
|
|
356
359
|
from auth_lib.jwks import JWKSClient
|
|
360
|
+
|
|
357
361
|
http = httpx.AsyncClient(transport=_T())
|
|
358
362
|
|
|
359
363
|
async def run():
|
|
@@ -371,9 +375,9 @@ async def test_get_jwks_client_singleton_single_flight():
|
|
|
371
375
|
"""Overime, ze paralelni volani vrati tentyz singleton."""
|
|
372
376
|
import asyncio
|
|
373
377
|
|
|
374
|
-
from auth_lib.jwks import get_jwks_client, reset_jwks_client
|
|
375
378
|
from auth_lib import jwks as jwks_mod
|
|
376
|
-
from auth_lib.config import
|
|
379
|
+
from auth_lib.config import AuthSettings, get_settings
|
|
380
|
+
from auth_lib.jwks import get_jwks_client, reset_jwks_client
|
|
377
381
|
|
|
378
382
|
# Nastavime settings pro proces
|
|
379
383
|
get_settings.cache_clear()
|
|
@@ -382,15 +386,12 @@ async def test_get_jwks_client_singleton_single_flight():
|
|
|
382
386
|
|
|
383
387
|
# Potrebujeme env pro AuthSettings() - prepiseme cache direct
|
|
384
388
|
settings = AuthSettings(keycloak_url="https://kc", realm="r", audience="a")
|
|
385
|
-
from functools import lru_cache
|
|
386
389
|
original = get_settings
|
|
387
390
|
try:
|
|
388
391
|
# Prepiseme lru_cache - vratime stejny settings
|
|
389
392
|
jwks_mod.get_settings = lambda: settings
|
|
390
393
|
|
|
391
|
-
results = await asyncio.gather(
|
|
392
|
-
get_jwks_client(), get_jwks_client(), get_jwks_client()
|
|
393
|
-
)
|
|
394
|
+
results = await asyncio.gather(get_jwks_client(), get_jwks_client(), get_jwks_client())
|
|
394
395
|
assert all(r is results[0] for r in results)
|
|
395
396
|
finally:
|
|
396
397
|
jwks_mod.get_settings = original
|
|
@@ -86,7 +86,10 @@ def test_malformed_token_returns_401(client: TestClient) -> None:
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
@pytest.mark.asyncio
|
|
89
|
-
async def test_jwks_cache_fetches_once_under_concurrency(
|
|
89
|
+
async def test_jwks_cache_fetches_once_under_concurrency(
|
|
90
|
+
mocked_jwks_client,
|
|
91
|
+
public_jwk: dict,
|
|
92
|
+
) -> None:
|
|
90
93
|
"""Single-flight chovani JWKS cache - 50 soubeznych volani = 1 fetch."""
|
|
91
94
|
import asyncio
|
|
92
95
|
import time as _time
|
|
@@ -114,10 +117,11 @@ async def test_jwks_cache_fetches_once_under_concurrency(mocked_jwks_client, pub
|
|
|
114
117
|
@pytest.mark.asyncio
|
|
115
118
|
async def test_jwks_kid_missing_after_refresh_is_invalid_token(test_settings, public_jwk) -> None:
|
|
116
119
|
"""Stale cache, refresh doplni cache, ale bez hledaneho kidu -> InvalidTokenError."""
|
|
117
|
-
import types
|
|
118
120
|
import time as _time
|
|
119
|
-
|
|
121
|
+
import types
|
|
122
|
+
|
|
120
123
|
from auth_lib.exceptions import InvalidTokenError
|
|
124
|
+
from auth_lib.jwks import JWKSClient
|
|
121
125
|
|
|
122
126
|
client = JWKSClient(test_settings)
|
|
123
127
|
|
|
@@ -136,11 +140,11 @@ async def test_jwks_kid_missing_after_refresh_is_invalid_token(test_settings, pu
|
|
|
136
140
|
@pytest.mark.asyncio
|
|
137
141
|
async def test_jwks_refresh_network_error_maps_to_config_error(test_settings) -> None:
|
|
138
142
|
"""Fetch selhe -> AuthConfigurationError (500)."""
|
|
139
|
-
import types
|
|
140
|
-
from auth_lib.jwks import JWKSClient
|
|
141
|
-
from auth_lib.exceptions import AuthConfigurationError
|
|
142
143
|
import httpx
|
|
143
144
|
|
|
145
|
+
from auth_lib.exceptions import AuthConfigurationError
|
|
146
|
+
from auth_lib.jwks import JWKSClient
|
|
147
|
+
|
|
144
148
|
client = JWKSClient(test_settings)
|
|
145
149
|
|
|
146
150
|
async def broken_get(url):
|
|
@@ -157,8 +161,9 @@ async def test_jwks_refresh_network_error_maps_to_config_error(test_settings) ->
|
|
|
157
161
|
@pytest.mark.asyncio
|
|
158
162
|
async def test_jwks_refresh_invalidates_and_reloads(mocked_jwks_client, public_jwk: dict) -> None:
|
|
159
163
|
"""invalidate() + refresh() -> nova data v cache."""
|
|
160
|
-
import types
|
|
161
164
|
import time as _time
|
|
165
|
+
import types
|
|
166
|
+
|
|
162
167
|
from tests.conftest import TEST_KID
|
|
163
168
|
|
|
164
169
|
async def fake_fetch(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|