oidcauthlib 3.0.4__tar.gz → 3.0.6__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.
- {oidcauthlib-3.0.4/oidcauthlib.egg-info → oidcauthlib-3.0.6}/PKG-INFO +1 -1
- oidcauthlib-3.0.6/VERSION +1 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/auth_manager.py +4 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/dcr/dcr_client.py +30 -2
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/dcr/dcr_manager.py +63 -3
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +3 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/url_validator.py +33 -11
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6/oidcauthlib.egg-info}/PKG-INFO +1 -1
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/test_auth_manager.py +11 -2
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/test_register_dynamic_provider.py +10 -1
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/well_known_configuration/test_well_known_configuration_cache.py +12 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/utilities/test_url_validator.py +27 -0
- oidcauthlib-3.0.4/VERSION +0 -1
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/LICENSE +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/MANIFEST.in +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/Makefile +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/README.md +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/auth_helper.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/cache/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/config/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/config/auth_config.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/config/auth_config_reader.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/dcr/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/exceptions/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/middleware/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/models/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/models/auth.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/models/base_db_model.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/models/cache_item.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/models/client_key_set.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/models/token.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/base_repository.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/repository_factory.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/routers/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/routers/auth_router.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/token_reader.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/container/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/open_telemetry/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/open_telemetry/span_names.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/py.typed +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/memory_storage_factory.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/storage_factory.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/storage/storage_factory_creator.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/cached.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/environment/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/environment/oidc_environment_variables.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/logger/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/logger/log_levels.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/logger/logging_response.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib.egg-info/SOURCES.txt +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib.egg-info/dependency_links.txt +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib.egg-info/not-zip-safe +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib.egg-info/requires.txt +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib.egg-info/top_level.txt +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/setup.cfg +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/setup.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/cache/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/cache/test_oauth_memory_cache.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/test_auth_config.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/test_auth_config_reader.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/test_auth_config_reader_register.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/test_auth_config_reader_thread_safety.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/conftest.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/dcr/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/dcr/test_dcr_client.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/dcr/test_dcr_manager.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/middleware/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/middleware/test_request_scope_middleware.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/middleware/test_token_reader_middleware.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/models/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/models/test_auth.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/models/test_base_db_model.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/models/test_cache_item.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/models/test_token.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/memory/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/memory/test_memory_repository.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/mongo/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/mongo/test_mongo_repository.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/mongo/test_mongo_repository_real.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/test_auth_helper.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/test_auth_helper2.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/well_known_configuration/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/well_known_configuration/test_auth_server_metadata_discovery.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/well_known_configuration/test_well_known_configuration_manager.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/conftest.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/storage/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/storage/test_mongo_gridfs_db.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/storage/test_mongo_gridfs_race_setup.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/storage/test_mongo_store_factory.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/storage/test_storage_factory_security.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/test_mongo_url_utils.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/test_simple.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/utilities/__init__.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/utilities/test_cached.py +0 -0
- {oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/utilities/test_mongo_url_utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.0.6
|
|
@@ -33,6 +33,7 @@ from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
|
33
33
|
from oidcauthlib.utilities.logger.logging_transport import (
|
|
34
34
|
LoggingTransport,
|
|
35
35
|
)
|
|
36
|
+
from oidcauthlib.utilities.url_validator import validate_url
|
|
36
37
|
|
|
37
38
|
logger = logging.getLogger(__name__)
|
|
38
39
|
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
@@ -199,9 +200,12 @@ class AuthManager:
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
if auth_config.authorization_endpoint and auth_config.token_endpoint:
|
|
203
|
+
validate_url(auth_config.authorization_endpoint)
|
|
204
|
+
validate_url(auth_config.token_endpoint)
|
|
202
205
|
register_kwargs["authorize_url"] = auth_config.authorization_endpoint
|
|
203
206
|
register_kwargs["access_token_url"] = auth_config.token_endpoint
|
|
204
207
|
elif auth_config.well_known_uri:
|
|
208
|
+
validate_url(auth_config.well_known_uri)
|
|
205
209
|
register_kwargs["server_metadata_url"] = auth_config.well_known_uri
|
|
206
210
|
else:
|
|
207
211
|
raise ValueError(
|
|
@@ -46,7 +46,16 @@ class DcrClient:
|
|
|
46
46
|
if contacts:
|
|
47
47
|
payload["contacts"] = contacts
|
|
48
48
|
|
|
49
|
-
logger.info(
|
|
49
|
+
logger.info(
|
|
50
|
+
"DCR: Sending registration request to '%s' with payload keys: %s",
|
|
51
|
+
registration_url,
|
|
52
|
+
sorted(payload.keys()),
|
|
53
|
+
)
|
|
54
|
+
logger.debug(
|
|
55
|
+
"DCR: Full registration payload for '%s': %s",
|
|
56
|
+
registration_url,
|
|
57
|
+
payload,
|
|
58
|
+
)
|
|
50
59
|
|
|
51
60
|
async with httpx.AsyncClient(timeout=self._timeout_seconds) as client:
|
|
52
61
|
response = await client.post(
|
|
@@ -57,6 +66,12 @@ class DcrClient:
|
|
|
57
66
|
try:
|
|
58
67
|
response.raise_for_status()
|
|
59
68
|
except httpx.HTTPStatusError as e:
|
|
69
|
+
logger.error(
|
|
70
|
+
"DCR: Registration failed at '%s' — HTTP %s. Response body: %s",
|
|
71
|
+
registration_url,
|
|
72
|
+
e.response.status_code,
|
|
73
|
+
e.response.text[:500],
|
|
74
|
+
)
|
|
60
75
|
raise ValueError(
|
|
61
76
|
f"DCR registration failed at '{registration_url}' "
|
|
62
77
|
f"with status {e.response.status_code}"
|
|
@@ -65,14 +80,27 @@ class DcrClient:
|
|
|
65
80
|
|
|
66
81
|
if "client_id" not in dcr_response:
|
|
67
82
|
response_keys: list[str] = sorted(dcr_response.keys())
|
|
83
|
+
logger.error(
|
|
84
|
+
"DCR: Response from '%s' missing 'client_id'. Keys present: %s",
|
|
85
|
+
registration_url,
|
|
86
|
+
response_keys,
|
|
87
|
+
)
|
|
68
88
|
raise ValueError(
|
|
69
89
|
f"DCR response from '{registration_url}' missing 'client_id'. "
|
|
70
90
|
f"Response keys: {response_keys}"
|
|
71
91
|
)
|
|
72
92
|
|
|
73
93
|
logger.info(
|
|
74
|
-
"DCR successful at '%s'
|
|
94
|
+
"DCR: Registration successful at '%s' — client_id=%s, "
|
|
95
|
+
"has_secret=%s, expires_at=%s",
|
|
75
96
|
registration_url,
|
|
76
97
|
dcr_response["client_id"],
|
|
98
|
+
"client_secret" in dcr_response,
|
|
99
|
+
dcr_response.get("client_secret_expires_at", "not_set"),
|
|
100
|
+
)
|
|
101
|
+
logger.debug(
|
|
102
|
+
"DCR: Full response from '%s': %s",
|
|
103
|
+
registration_url,
|
|
104
|
+
{k: v for k, v in dcr_response.items() if k != "client_secret"},
|
|
77
105
|
)
|
|
78
106
|
return dcr_response
|
|
@@ -52,9 +52,20 @@ class DcrManager:
|
|
|
52
52
|
contacts: list[str] | None = None,
|
|
53
53
|
) -> DcrRegistration | None:
|
|
54
54
|
if client_id:
|
|
55
|
+
logger.info(
|
|
56
|
+
"DCR: Skipping registration for '%s' — client_id already "
|
|
57
|
+
"provided (client_id=%s)",
|
|
58
|
+
auth_provider,
|
|
59
|
+
client_id,
|
|
60
|
+
)
|
|
55
61
|
return None
|
|
56
62
|
|
|
57
63
|
if not registration_url:
|
|
64
|
+
logger.error(
|
|
65
|
+
"DCR: No registration_url and no client_id for '%s' — "
|
|
66
|
+
"cannot resolve credentials",
|
|
67
|
+
auth_provider,
|
|
68
|
+
)
|
|
58
69
|
raise ValueError(
|
|
59
70
|
f"registration_url is required for DCR when client_id is not "
|
|
60
71
|
f"provided (auth_provider='{auth_provider}')"
|
|
@@ -62,19 +73,42 @@ class DcrManager:
|
|
|
62
73
|
|
|
63
74
|
validate_url(registration_url)
|
|
64
75
|
|
|
76
|
+
logger.info(
|
|
77
|
+
"DCR: Resolving credentials for '%s' via registration_url='%s'",
|
|
78
|
+
auth_provider,
|
|
79
|
+
registration_url,
|
|
80
|
+
)
|
|
81
|
+
|
|
65
82
|
cached = await self._find_cached(
|
|
66
83
|
auth_provider=auth_provider,
|
|
67
84
|
registration_url=registration_url,
|
|
68
85
|
)
|
|
69
86
|
|
|
70
87
|
if cached and not self._is_expired(cached):
|
|
71
|
-
logger.
|
|
72
|
-
"Using cached
|
|
88
|
+
logger.info(
|
|
89
|
+
"DCR: Using cached credentials for '%s' — client_id=%s, expires_at=%s",
|
|
73
90
|
auth_provider,
|
|
74
91
|
cached.client_id,
|
|
92
|
+
cached.client_secret_expires_at or "never",
|
|
75
93
|
)
|
|
76
94
|
return cached
|
|
77
95
|
|
|
96
|
+
if cached and self._is_expired(cached):
|
|
97
|
+
logger.info(
|
|
98
|
+
"DCR: Cached credentials for '%s' are expired "
|
|
99
|
+
"(client_id=%s, expired_at=%s) — re-registering",
|
|
100
|
+
auth_provider,
|
|
101
|
+
cached.client_id,
|
|
102
|
+
cached.client_secret_expires_at,
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
logger.info(
|
|
106
|
+
"DCR: No cached credentials found for '%s' — performing "
|
|
107
|
+
"new registration at '%s'",
|
|
108
|
+
auth_provider,
|
|
109
|
+
registration_url,
|
|
110
|
+
)
|
|
111
|
+
|
|
78
112
|
dcr_response = await self._dcr_client.register(
|
|
79
113
|
registration_url=registration_url,
|
|
80
114
|
redirect_uri=self._redirect_uri,
|
|
@@ -84,11 +118,17 @@ class DcrManager:
|
|
|
84
118
|
contacts=contacts,
|
|
85
119
|
)
|
|
86
120
|
|
|
87
|
-
|
|
121
|
+
registration = await self._persist(
|
|
88
122
|
auth_provider=auth_provider,
|
|
89
123
|
registration_url=registration_url,
|
|
90
124
|
dcr_response=dcr_response,
|
|
91
125
|
)
|
|
126
|
+
logger.info(
|
|
127
|
+
"DCR: Persisted credentials for '%s' — client_id=%s",
|
|
128
|
+
auth_provider,
|
|
129
|
+
registration.client_id,
|
|
130
|
+
)
|
|
131
|
+
return registration
|
|
92
132
|
|
|
93
133
|
async def _find_cached(
|
|
94
134
|
self,
|
|
@@ -96,6 +136,13 @@ class DcrManager:
|
|
|
96
136
|
auth_provider: str,
|
|
97
137
|
registration_url: str,
|
|
98
138
|
) -> DcrRegistration | None:
|
|
139
|
+
logger.debug(
|
|
140
|
+
"DCR: Querying cache for auth_provider='%s', "
|
|
141
|
+
"registration_url='%s', collection='%s'",
|
|
142
|
+
auth_provider,
|
|
143
|
+
registration_url,
|
|
144
|
+
self._collection_name,
|
|
145
|
+
)
|
|
99
146
|
results = await self._repository.find_many(
|
|
100
147
|
collection_name=self._collection_name,
|
|
101
148
|
model_class=DcrRegistration,
|
|
@@ -104,6 +151,19 @@ class DcrManager:
|
|
|
104
151
|
"registration_url": registration_url,
|
|
105
152
|
},
|
|
106
153
|
)
|
|
154
|
+
if results:
|
|
155
|
+
logger.debug(
|
|
156
|
+
"DCR: Cache hit for '%s' — found %d result(s), client_id=%s",
|
|
157
|
+
auth_provider,
|
|
158
|
+
len(results),
|
|
159
|
+
results[0].client_id,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
logger.debug(
|
|
163
|
+
"DCR: Cache miss for '%s' at '%s'",
|
|
164
|
+
auth_provider,
|
|
165
|
+
registration_url,
|
|
166
|
+
)
|
|
107
167
|
return results[0] if results else None
|
|
108
168
|
|
|
109
169
|
@staticmethod
|
|
@@ -20,6 +20,7 @@ from oidcauthlib.utilities.environment.oidc_environment_variables import (
|
|
|
20
20
|
)
|
|
21
21
|
from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
22
22
|
from oidcauthlib.open_telemetry.attribute_names import OidcOpenTelemetryAttributeNames
|
|
23
|
+
from oidcauthlib.utilities.url_validator import validate_url
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
@@ -249,6 +250,7 @@ class WellKnownConfigurationCache:
|
|
|
249
250
|
span.set_attribute(
|
|
250
251
|
OidcOpenTelemetryAttributeNames.WELL_KNOWN_URI, well_known_uri
|
|
251
252
|
)
|
|
253
|
+
validate_url(well_known_uri)
|
|
252
254
|
async with httpx.AsyncClient() as client:
|
|
253
255
|
try:
|
|
254
256
|
logger.info(
|
|
@@ -465,6 +467,7 @@ class WellKnownConfigurationCache:
|
|
|
465
467
|
Security:
|
|
466
468
|
- Keys are not logged; only counts are logged to avoid PII/token leakage.
|
|
467
469
|
"""
|
|
470
|
+
validate_url(jwks_uri)
|
|
468
471
|
async with httpx.AsyncClient() as client:
|
|
469
472
|
try:
|
|
470
473
|
logger.info(f"Fetching JWKS from {jwks_uri}")
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
Validates URLs before making HTTP requests to prevent Server-Side Request
|
|
4
4
|
Forgery (SSRF) attacks. Enforces HTTPS-only, rejects private/reserved IP
|
|
5
5
|
ranges, and resolves hostnames to verify the target is not internal.
|
|
6
|
+
|
|
7
|
+
Set the ``AUTH_ALLOW_HTTP_URLS`` environment variable to ``true`` to relax
|
|
8
|
+
HTTPS-only enforcement and private-IP blocking for local development
|
|
9
|
+
(e.g. Docker Compose with ``http://keycloak:8080``).
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
import ipaddress
|
|
9
13
|
import logging
|
|
14
|
+
import os
|
|
10
15
|
import socket
|
|
11
16
|
from urllib.parse import urlparse
|
|
12
17
|
|
|
@@ -59,13 +64,23 @@ def _is_private_ip(addr: str) -> bool:
|
|
|
59
64
|
return any(ip in network for network in _BLOCKED_NETWORKS)
|
|
60
65
|
|
|
61
66
|
|
|
67
|
+
def _env_allow_http() -> bool:
|
|
68
|
+
"""Return True when AUTH_ALLOW_HTTP_URLS is set to a truthy value."""
|
|
69
|
+
return os.environ.get("AUTH_ALLOW_HTTP_URLS", "").lower() in (
|
|
70
|
+
"true",
|
|
71
|
+
"1",
|
|
72
|
+
"yes",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
62
76
|
def validate_url(url: str, *, allow_http: bool = False) -> str:
|
|
63
77
|
"""Validate a URL for safe outbound HTTP requests.
|
|
64
78
|
|
|
65
79
|
Args:
|
|
66
80
|
url: The URL to validate.
|
|
67
81
|
allow_http: If True, permit ``http://`` in addition to ``https://``.
|
|
68
|
-
Defaults to False (HTTPS only).
|
|
82
|
+
Defaults to False (HTTPS only). Also enabled globally by the
|
|
83
|
+
``AUTH_ALLOW_HTTP_URLS`` environment variable.
|
|
69
84
|
|
|
70
85
|
Returns:
|
|
71
86
|
The validated URL (unchanged).
|
|
@@ -73,6 +88,8 @@ def validate_url(url: str, *, allow_http: bool = False) -> str:
|
|
|
73
88
|
Raises:
|
|
74
89
|
ValueError: If the URL fails any validation check.
|
|
75
90
|
"""
|
|
91
|
+
allow_http = allow_http or _env_allow_http()
|
|
92
|
+
|
|
76
93
|
parsed = urlparse(url)
|
|
77
94
|
|
|
78
95
|
# --- scheme ---
|
|
@@ -80,7 +97,8 @@ def validate_url(url: str, *, allow_http: bool = False) -> str:
|
|
|
80
97
|
if parsed.scheme not in allowed_schemes:
|
|
81
98
|
raise ValueError(
|
|
82
99
|
f"URL scheme must be {' or '.join(sorted(allowed_schemes))}, "
|
|
83
|
-
f"got '{parsed.scheme}' in '{url}'"
|
|
100
|
+
f"got '{parsed.scheme}' in '{url}'. "
|
|
101
|
+
f"Set AUTH_ALLOW_HTTP_URLS=true to allow http:// URLs."
|
|
84
102
|
)
|
|
85
103
|
|
|
86
104
|
# --- hostname ---
|
|
@@ -98,15 +116,19 @@ def validate_url(url: str, *, allow_http: bool = False) -> str:
|
|
|
98
116
|
raise ValueError(f"URL must use a hostname, not a raw IP address: '{hostname}'")
|
|
99
117
|
|
|
100
118
|
# --- resolve DNS and check all IPs ---
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
# When allow_http is enabled (local development), skip the private-IP
|
|
120
|
+
# check because Docker-internal hostnames (e.g. keycloak) resolve to
|
|
121
|
+
# RFC 1918 addresses on the container network.
|
|
122
|
+
if not allow_http:
|
|
123
|
+
try:
|
|
124
|
+
addr_infos = socket.getaddrinfo(hostname, parsed.port or 443)
|
|
125
|
+
except socket.gaierror as exc:
|
|
126
|
+
raise ValueError(f"Cannot resolve hostname '{hostname}': {exc}") from exc
|
|
127
|
+
|
|
128
|
+
for _family, _type, _proto, _canonname, sockaddr in addr_infos:
|
|
129
|
+
ip_str = str(sockaddr[0])
|
|
130
|
+
if _is_private_ip(ip_str):
|
|
131
|
+
raise ValueError(f"URL '{url}' resolves to blocked IP {ip_str}")
|
|
110
132
|
|
|
111
133
|
logger.debug("URL validated: %s", url)
|
|
112
134
|
return url
|
|
@@ -4,8 +4,7 @@ import pytest
|
|
|
4
4
|
import httpx
|
|
5
5
|
import respx
|
|
6
6
|
from unittest.mock import patch
|
|
7
|
-
|
|
8
|
-
from typing import Any, Dict, List, Optional, override
|
|
7
|
+
from typing import Any, Dict, Generator, List, Optional, override
|
|
9
8
|
|
|
10
9
|
from oidcauthlib.auth.auth_manager import AuthManager
|
|
11
10
|
from oidcauthlib.auth.auth_helper import AuthHelper
|
|
@@ -188,6 +187,16 @@ def well_known_manager(
|
|
|
188
187
|
)
|
|
189
188
|
|
|
190
189
|
|
|
190
|
+
_PATCH_VALIDATE = "oidcauthlib.auth.auth_manager.validate_url"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.fixture(autouse=True)
|
|
194
|
+
def _bypass_url_validation() -> Generator[None, None, None]:
|
|
195
|
+
"""Skip DNS resolution in validate_url for unit tests with fake hostnames."""
|
|
196
|
+
with patch(_PATCH_VALIDATE, side_effect=lambda url, **kw: url):
|
|
197
|
+
yield
|
|
198
|
+
|
|
199
|
+
|
|
191
200
|
# ---------------- Tests -----------------
|
|
192
201
|
@pytest.mark.asyncio
|
|
193
202
|
async def test_init_memory_cache(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import Any, AsyncGenerator
|
|
2
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
from simple_container.container.interfaces import IContainer
|
|
@@ -10,6 +10,15 @@ from oidcauthlib.auth.dcr.dcr_registration import DcrRegistration
|
|
|
10
10
|
|
|
11
11
|
pytestmark = pytest.mark.asyncio
|
|
12
12
|
|
|
13
|
+
_PATCH_VALIDATE = "oidcauthlib.auth.auth_manager.validate_url"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def _bypass_url_validation() -> Any:
|
|
18
|
+
"""Skip DNS resolution in validate_url for unit tests with fake hostnames."""
|
|
19
|
+
with patch(_PATCH_VALIDATE, side_effect=lambda url, **kw: url):
|
|
20
|
+
yield
|
|
21
|
+
|
|
13
22
|
|
|
14
23
|
@pytest.fixture(scope="function")
|
|
15
24
|
async def test_container() -> AsyncGenerator[None, None]:
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from typing import Any
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
2
5
|
import respx
|
|
3
6
|
import httpx
|
|
4
7
|
import pytest
|
|
@@ -16,6 +19,15 @@ from oidcauthlib.utilities.environment.oidc_environment_variables import (
|
|
|
16
19
|
OidcEnvironmentVariables,
|
|
17
20
|
)
|
|
18
21
|
|
|
22
|
+
_PATCH_VALIDATE = "oidcauthlib.auth.well_known_configuration.well_known_configuration_cache.validate_url"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(autouse=True)
|
|
26
|
+
def _bypass_url_validation() -> Any:
|
|
27
|
+
"""Skip DNS resolution in validate_url for unit tests with fake hostnames."""
|
|
28
|
+
with patch(_PATCH_VALIDATE, side_effect=lambda url, **kw: url):
|
|
29
|
+
yield
|
|
30
|
+
|
|
19
31
|
|
|
20
32
|
@pytest.mark.asyncio
|
|
21
33
|
async def test_get_async_caches_on_first_call(test_container: IContainer) -> None:
|
|
@@ -126,6 +126,33 @@ class TestDNSRebindingProtection:
|
|
|
126
126
|
validate_url("https://some-external-host.com/register")
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
class TestEnvVarAllowHttp:
|
|
130
|
+
"""AUTH_ALLOW_HTTP_URLS env var globally enables allow_http."""
|
|
131
|
+
|
|
132
|
+
def test_env_var_allows_http(self) -> None:
|
|
133
|
+
with patch.dict("os.environ", {"AUTH_ALLOW_HTTP_URLS": "true"}):
|
|
134
|
+
assert (
|
|
135
|
+
validate_url(
|
|
136
|
+
"http://keycloak:8080/realms/test/.well-known/openid-configuration"
|
|
137
|
+
)
|
|
138
|
+
== "http://keycloak:8080/realms/test/.well-known/openid-configuration"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def test_env_var_skips_private_ip_check(self) -> None:
|
|
142
|
+
with patch.dict("os.environ", {"AUTH_ALLOW_HTTP_URLS": "true"}):
|
|
143
|
+
with patch(
|
|
144
|
+
"oidcauthlib.utilities.url_validator.socket.getaddrinfo"
|
|
145
|
+
) as mock_gai:
|
|
146
|
+
mock_gai.return_value = [(2, 1, 6, "", ("172.18.0.5", 8080))]
|
|
147
|
+
validate_url("http://keycloak:8080/realms/test")
|
|
148
|
+
mock_gai.assert_not_called()
|
|
149
|
+
|
|
150
|
+
def test_env_var_false_keeps_https_only(self) -> None:
|
|
151
|
+
with patch.dict("os.environ", {"AUTH_ALLOW_HTTP_URLS": "false"}):
|
|
152
|
+
with pytest.raises(ValueError, match="AUTH_ALLOW_HTTP_URLS"):
|
|
153
|
+
validate_url("http://example.com/register")
|
|
154
|
+
|
|
155
|
+
|
|
129
156
|
class TestPublicHostnamesAccepted:
|
|
130
157
|
@pytest.mark.parametrize(
|
|
131
158
|
"ip",
|
oidcauthlib-3.0.4/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.0.4
|
|
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
|
|
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
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/middleware/request_scope_middleware.py
RENAMED
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/middleware/token_reader_middleware.py
RENAMED
|
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
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/memory/memory_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/repository/mongo/mongo_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/auth/well_known_configuration/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/oidcauthlib/container/oidc_authlib_container_factory.py
RENAMED
|
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
|
|
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
|
|
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
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/test_auth_config_reader_register.py
RENAMED
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/config/test_auth_config_reader_thread_safety.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/middleware/test_request_scope_middleware.py
RENAMED
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/middleware/test_token_reader_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/memory/test_memory_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/mongo/test_mongo_repository.py
RENAMED
|
File without changes
|
{oidcauthlib-3.0.4 → oidcauthlib-3.0.6}/tests/auth/repository/mongo/test_mongo_repository_real.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|