oidcauthlib 3.0.2__tar.gz → 3.0.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.
- {oidcauthlib-3.0.2/oidcauthlib.egg-info → oidcauthlib-3.0.4}/PKG-INFO +1 -1
- oidcauthlib-3.0.4/VERSION +1 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/auth_manager.py +42 -17
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/config/auth_config.py +14 -5
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/config/auth_config_reader.py +60 -16
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/dcr_client.py +10 -1
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/dcr_manager.py +6 -2
- oidcauthlib-3.0.4/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +18 -0
- oidcauthlib-3.0.4/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +112 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +4 -3
- oidcauthlib-3.0.4/oidcauthlib/utilities/url_validator.py +112 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4/oidcauthlib.egg-info}/PKG-INFO +1 -1
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/SOURCES.txt +7 -3
- oidcauthlib-3.0.4/tests/auth/config/test_auth_config.py +72 -0
- oidcauthlib-3.0.4/tests/auth/config/test_auth_config_reader_register.py +200 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/dcr/test_dcr_client.py +39 -3
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/dcr/test_dcr_manager.py +76 -4
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_register_dynamic_provider.py +84 -1
- oidcauthlib-3.0.4/tests/auth/well_known_configuration/test_auth_server_metadata_discovery.py +184 -0
- oidcauthlib-3.0.4/tests/utilities/test_url_validator.py +155 -0
- oidcauthlib-3.0.2/VERSION +0 -1
- oidcauthlib-3.0.2/tests/auth/config/test_auth_config.py +0 -32
- oidcauthlib-3.0.2/tests/auth/dcr/conftest.py +0 -14
- oidcauthlib-3.0.2/tests/auth/test_auth_config_explicit_endpoints.py +0 -79
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/LICENSE +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/MANIFEST.in +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/Makefile +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/README.md +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/auth_helper.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/config/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/middleware/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/auth.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/base_db_model.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/cache_item.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/client_key_set.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/token.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/base_repository.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/repository_factory.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/routers/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/routers/auth_router.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/token_reader.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/container/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/open_telemetry/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/open_telemetry/span_names.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/py.typed +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/memory_storage_factory.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/storage_factory.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/storage_factory_creator.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/cached.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/environment/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/environment/oidc_environment_variables.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/log_levels.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/logging_response.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/dependency_links.txt +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/not-zip-safe +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/requires.txt +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/top_level.txt +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/setup.cfg +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/setup.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/cache/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/cache/test_oauth_memory_cache.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/config/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/config/test_auth_config_reader.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/config/test_auth_config_reader_thread_safety.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/conftest.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/dcr/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/middleware/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/middleware/test_request_scope_middleware.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/middleware/test_token_reader_middleware.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_auth.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_base_db_model.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_cache_item.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_token.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/memory/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/memory/test_memory_repository.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/mongo/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/mongo/test_mongo_repository.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/mongo/test_mongo_repository_real.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_auth_helper.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_auth_helper2.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_auth_manager.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/well_known_configuration/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/well_known_configuration/test_well_known_configuration_cache.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/well_known_configuration/test_well_known_configuration_manager.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/conftest.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_mongo_gridfs_db.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_mongo_gridfs_race_setup.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_mongo_store_factory.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_storage_factory_security.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/test_mongo_url_utils.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/test_simple.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/utilities/__init__.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/utilities/test_cached.py +0 -0
- {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/utilities/test_mongo_url_utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.0.4
|
|
@@ -18,6 +18,7 @@ from oidcauthlib.auth.config.auth_config import AuthConfig
|
|
|
18
18
|
from oidcauthlib.auth.config.auth_config_reader import (
|
|
19
19
|
AuthConfigReader,
|
|
20
20
|
)
|
|
21
|
+
from oidcauthlib.auth.dcr.dcr_manager import DcrManager
|
|
21
22
|
from oidcauthlib.auth.exceptions.authorization_needed_exception import (
|
|
22
23
|
AuthorizationNeededException,
|
|
23
24
|
)
|
|
@@ -52,23 +53,17 @@ class AuthManager:
|
|
|
52
53
|
auth_config_reader: AuthConfigReader,
|
|
53
54
|
token_reader: TokenReader,
|
|
54
55
|
well_known_configuration_manager: WellKnownConfigurationManager,
|
|
56
|
+
dcr_manager: DcrManager | None = None,
|
|
55
57
|
) -> None:
|
|
56
58
|
"""
|
|
57
59
|
Initialize the AuthManager with the necessary configuration for OIDC PKCE.
|
|
58
|
-
It sets up the OAuth cache, reads environment variables for the OIDC provider,
|
|
59
|
-
and configures the OAuth client.
|
|
60
|
-
The environment variables required are:
|
|
61
|
-
- MONGO_URL: The connection string for the MongoDB database.
|
|
62
|
-
- MONGO_DB_NAME: The name of the MongoDB database.
|
|
63
|
-
- MONGO_DB_TOKEN_COLLECTION_NAME: The name of the MongoDB collection for tokens.
|
|
64
|
-
It also initializes the OAuth cache based on the OAUTH_CACHE environment variable,
|
|
65
|
-
which can be set to "memory" for in-memory caching or "mongo" for MongoDB caching.
|
|
66
|
-
If the OAUTH_CACHE environment variable is not set, it defaults to "memory".
|
|
67
60
|
|
|
68
61
|
Args:
|
|
69
|
-
environment_variables
|
|
70
|
-
auth_config_reader
|
|
71
|
-
token_reader
|
|
62
|
+
environment_variables: The environment variables for the application.
|
|
63
|
+
auth_config_reader: The reader for authentication configurations.
|
|
64
|
+
token_reader: The reader for tokens.
|
|
65
|
+
well_known_configuration_manager: Manager for well-known OIDC discovery.
|
|
66
|
+
dcr_manager: Optional RFC 7591 Dynamic Client Registration manager.
|
|
72
67
|
"""
|
|
73
68
|
self.environment_variables: AbstractEnvironmentVariables = environment_variables
|
|
74
69
|
if self.environment_variables is None:
|
|
@@ -103,6 +98,9 @@ class AuthManager:
|
|
|
103
98
|
raise TypeError(
|
|
104
99
|
"well_known_configuration_manager must be an instance of WellKnownConfigurationManager"
|
|
105
100
|
)
|
|
101
|
+
|
|
102
|
+
self._dcr_manager: DcrManager | None = dcr_manager
|
|
103
|
+
|
|
106
104
|
oauth_cache_type = environment_variables.oauth_cache
|
|
107
105
|
self.cache: OAuthCache = (
|
|
108
106
|
OAuthMemoryCache()
|
|
@@ -120,7 +118,6 @@ class AuthManager:
|
|
|
120
118
|
# https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients
|
|
121
119
|
self._oauth: OAuth = OAuth(cache=self.cache)
|
|
122
120
|
self._registered_dynamic_providers: set[str] = set()
|
|
123
|
-
# read AUTH_PROVIDERS comma separated list from the environment variable and register the OIDC provider for each provider
|
|
124
121
|
self.auth_configs: List[AuthConfig] = (
|
|
125
122
|
self.auth_config_reader.get_auth_configs_for_all_auth_providers()
|
|
126
123
|
)
|
|
@@ -141,13 +138,41 @@ class AuthManager:
|
|
|
141
138
|
) -> None:
|
|
142
139
|
"""Register an OAuth provider dynamically at runtime.
|
|
143
140
|
|
|
144
|
-
Handles explicit endpoints, discovery, configurable PKCE,
|
|
141
|
+
Handles DCR (RFC 7591), explicit endpoints, discovery, configurable PKCE,
|
|
142
|
+
and deduplication.
|
|
145
143
|
"""
|
|
146
144
|
provider_name = auth_config.auth_provider.lower()
|
|
147
145
|
|
|
148
146
|
if provider_name in self._registered_dynamic_providers:
|
|
149
147
|
return
|
|
150
148
|
|
|
149
|
+
# --- DCR: resolve client_id if not provided ---
|
|
150
|
+
client_id = auth_config.client_id
|
|
151
|
+
client_secret = auth_config.client_secret
|
|
152
|
+
|
|
153
|
+
if not client_id:
|
|
154
|
+
if not self._dcr_manager:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"AuthConfig for '{auth_config.auth_provider}' has no client_id "
|
|
157
|
+
f"and no DcrManager is configured to perform DCR"
|
|
158
|
+
)
|
|
159
|
+
dcr_result = await self._dcr_manager.resolve_dcr_credentials(
|
|
160
|
+
auth_provider=provider_name,
|
|
161
|
+
registration_url=auth_config.registration_url,
|
|
162
|
+
)
|
|
163
|
+
if dcr_result is None or not dcr_result.client_id:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"DCR failed to obtain client_id for '{auth_config.auth_provider}'"
|
|
166
|
+
)
|
|
167
|
+
client_id = dcr_result.client_id
|
|
168
|
+
client_secret = dcr_result.client_secret
|
|
169
|
+
logger.info(
|
|
170
|
+
"DCR resolved client_id=%s for provider '%s'",
|
|
171
|
+
client_id,
|
|
172
|
+
auth_config.auth_provider,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# --- Build client kwargs ---
|
|
151
176
|
client_kwargs: dict[str, Any] = {
|
|
152
177
|
"scope": auth_config.scope,
|
|
153
178
|
"transport": LoggingTransport(httpx.AsyncHTTPTransport()),
|
|
@@ -168,8 +193,8 @@ class AuthManager:
|
|
|
168
193
|
|
|
169
194
|
register_kwargs: dict[str, Any] = {
|
|
170
195
|
"name": provider_name,
|
|
171
|
-
"client_id":
|
|
172
|
-
"client_secret":
|
|
196
|
+
"client_id": client_id,
|
|
197
|
+
"client_secret": client_secret,
|
|
173
198
|
"client_kwargs": client_kwargs,
|
|
174
199
|
}
|
|
175
200
|
|
|
@@ -194,7 +219,7 @@ class AuthManager:
|
|
|
194
219
|
"Dynamically registered OAuth provider '%s' "
|
|
195
220
|
"(client_id=%s, well_known=%s, authorize=%s, token=%s, pkce=%s/%s)",
|
|
196
221
|
auth_config.auth_provider,
|
|
197
|
-
|
|
222
|
+
client_id,
|
|
198
223
|
auth_config.well_known_uri,
|
|
199
224
|
auth_config.authorization_endpoint,
|
|
200
225
|
auth_config.token_endpoint,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
-
from typing import Optional, Any, Literal
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
2
|
+
from typing import Optional, Any, Literal, Self
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class AuthConfig(BaseModel):
|
|
@@ -25,9 +25,9 @@ class AuthConfig(BaseModel):
|
|
|
25
25
|
default=None,
|
|
26
26
|
description="The issuer of the token, typically the URL of the auth provider.",
|
|
27
27
|
)
|
|
28
|
-
client_id: str = Field(
|
|
29
|
-
|
|
30
|
-
description="The client ID for the auth provider
|
|
28
|
+
client_id: Optional[str] = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description="The client ID for the auth provider. Optional when using DCR (registration_url).",
|
|
31
31
|
)
|
|
32
32
|
client_secret: Optional[str] = Field(
|
|
33
33
|
default=None,
|
|
@@ -72,3 +72,12 @@ class AuthConfig(BaseModel):
|
|
|
72
72
|
default=None,
|
|
73
73
|
description="RFC 7591 Dynamic Client Registration endpoint URL.",
|
|
74
74
|
)
|
|
75
|
+
|
|
76
|
+
@model_validator(mode="after")
|
|
77
|
+
def _require_client_id_or_registration_url(self) -> Self:
|
|
78
|
+
if not self.client_id and not self.registration_url:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"AuthConfig for '{self.auth_provider}' must have either "
|
|
81
|
+
f"client_id or registration_url (for DCR)"
|
|
82
|
+
)
|
|
83
|
+
return self
|
|
@@ -69,8 +69,11 @@ class AuthConfigReader:
|
|
|
69
69
|
return self._auth_configs
|
|
70
70
|
auth_providers: list[str] | None = self.environment_variables.auth_providers
|
|
71
71
|
if auth_providers is None:
|
|
72
|
-
logger.
|
|
73
|
-
|
|
72
|
+
logger.info(
|
|
73
|
+
"AUTH_PROVIDERS environment variable is not set. "
|
|
74
|
+
"Starting with empty provider list (can be populated via register_auth_configs)."
|
|
75
|
+
)
|
|
76
|
+
auth_providers = []
|
|
74
77
|
logger.info(f"Loading auth configs for providers: {auth_providers}")
|
|
75
78
|
auth_configs: list[AuthConfig] = []
|
|
76
79
|
for auth_provider in auth_providers:
|
|
@@ -110,6 +113,48 @@ class AuthConfigReader:
|
|
|
110
113
|
logger.warning(f"No config found for auth provider: {auth_provider}")
|
|
111
114
|
return None
|
|
112
115
|
|
|
116
|
+
def register_auth_configs(self, *, configs: list[AuthConfig]) -> None:
|
|
117
|
+
"""Register auth configs from an external source (e.g. .mcp.json).
|
|
118
|
+
|
|
119
|
+
Merges with any configs already loaded from environment variables.
|
|
120
|
+
Duplicate auth_provider names (case-insensitive) are skipped.
|
|
121
|
+
Thread-safe.
|
|
122
|
+
"""
|
|
123
|
+
if not configs:
|
|
124
|
+
return
|
|
125
|
+
with self._lock:
|
|
126
|
+
if self._auth_configs is None:
|
|
127
|
+
auth_providers: list[str] | None = (
|
|
128
|
+
self.environment_variables.auth_providers
|
|
129
|
+
)
|
|
130
|
+
if auth_providers is None:
|
|
131
|
+
auth_providers = []
|
|
132
|
+
env_configs: list[AuthConfig] = []
|
|
133
|
+
for auth_provider in auth_providers:
|
|
134
|
+
auth_config = self.read_config_for_auth_provider(
|
|
135
|
+
auth_provider=auth_provider
|
|
136
|
+
)
|
|
137
|
+
if auth_config is not None:
|
|
138
|
+
env_configs.append(auth_config)
|
|
139
|
+
self._auth_configs = env_configs
|
|
140
|
+
|
|
141
|
+
existing_names: set[str] = {
|
|
142
|
+
c.auth_provider.lower() for c in self._auth_configs
|
|
143
|
+
}
|
|
144
|
+
for config in configs:
|
|
145
|
+
if config.auth_provider.lower() not in existing_names:
|
|
146
|
+
self._auth_configs.append(config)
|
|
147
|
+
existing_names.add(config.auth_provider.lower())
|
|
148
|
+
logger.info(
|
|
149
|
+
"Registered external auth provider: %s",
|
|
150
|
+
config.auth_provider,
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
logger.debug(
|
|
154
|
+
"Skipping duplicate auth provider: %s",
|
|
155
|
+
config.auth_provider,
|
|
156
|
+
)
|
|
157
|
+
|
|
113
158
|
# noinspection PyMethodMayBeStatic
|
|
114
159
|
def read_config_for_auth_provider(self, *, auth_provider: str) -> AuthConfig | None:
|
|
115
160
|
"""
|
|
@@ -130,13 +175,6 @@ class AuthConfigReader:
|
|
|
130
175
|
logger.debug(f"Standardized auth provider name to: {auth_provider_upper}")
|
|
131
176
|
# read client_id and client_secret from the environment variables
|
|
132
177
|
auth_client_id: str | None = os.getenv(f"AUTH_CLIENT_ID_{auth_provider_upper}")
|
|
133
|
-
if auth_client_id is None:
|
|
134
|
-
logger.error(
|
|
135
|
-
f"AUTH_CLIENT_ID_{auth_provider_upper} environment variable is not set"
|
|
136
|
-
)
|
|
137
|
-
raise ValueError(
|
|
138
|
-
f"AUTH_CLIENT_ID_{auth_provider_upper} environment variable must be set"
|
|
139
|
-
)
|
|
140
178
|
auth_client_secret: str | None = os.getenv(
|
|
141
179
|
f"AUTH_CLIENT_SECRET_{auth_provider_upper}"
|
|
142
180
|
)
|
|
@@ -147,13 +185,6 @@ class AuthConfigReader:
|
|
|
147
185
|
auth_well_known_uri: str | None = os.getenv(
|
|
148
186
|
f"AUTH_WELL_KNOWN_URI_{auth_provider_upper}"
|
|
149
187
|
)
|
|
150
|
-
if auth_well_known_uri is None:
|
|
151
|
-
logger.error(
|
|
152
|
-
f"AUTH_WELL_KNOWN_URI_{auth_provider_upper} environment variable is not set"
|
|
153
|
-
)
|
|
154
|
-
raise ValueError(
|
|
155
|
-
f"AUTH_WELL_KNOWN_URI_{auth_provider_upper} environment variable must be set"
|
|
156
|
-
)
|
|
157
188
|
issuer: str | None = os.getenv(f"AUTH_ISSUER_{auth_provider_upper}")
|
|
158
189
|
logger.debug(
|
|
159
190
|
f"Issuer for {auth_provider_upper}: {issuer if issuer else 'not set'}"
|
|
@@ -201,6 +232,16 @@ class AuthConfigReader:
|
|
|
201
232
|
extra_info_dict = {str(key): value for key, value in extra_info_raw.items()}
|
|
202
233
|
else:
|
|
203
234
|
extra_info_dict = None
|
|
235
|
+
authorization_endpoint: str | None = os.getenv(
|
|
236
|
+
f"AUTH_AUTHORIZATION_ENDPOINT_{auth_provider_upper}"
|
|
237
|
+
)
|
|
238
|
+
token_endpoint: str | None = os.getenv(
|
|
239
|
+
f"AUTH_TOKEN_ENDPOINT_{auth_provider_upper}"
|
|
240
|
+
)
|
|
241
|
+
registration_url: str | None = os.getenv(
|
|
242
|
+
f"AUTH_REGISTRATION_URL_{auth_provider_upper}"
|
|
243
|
+
)
|
|
244
|
+
|
|
204
245
|
return AuthConfig(
|
|
205
246
|
auth_provider=auth_provider,
|
|
206
247
|
friendly_name=friendly_name,
|
|
@@ -211,6 +252,9 @@ class AuthConfigReader:
|
|
|
211
252
|
well_known_uri=auth_well_known_uri,
|
|
212
253
|
scope=scope,
|
|
213
254
|
extra_info=extra_info_dict,
|
|
255
|
+
authorization_endpoint=authorization_endpoint,
|
|
256
|
+
token_endpoint=token_endpoint,
|
|
257
|
+
registration_url=registration_url,
|
|
214
258
|
)
|
|
215
259
|
|
|
216
260
|
def get_audience_for_provider(self, *, auth_provider: str) -> str:
|
|
@@ -4,14 +4,21 @@ from typing import Any
|
|
|
4
4
|
import httpx
|
|
5
5
|
|
|
6
6
|
from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
7
|
+
from oidcauthlib.utilities.url_validator import validate_url
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
_DEFAULT_TIMEOUT_SECONDS: int = 10
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
class DcrClient:
|
|
13
17
|
"""RFC 7591 Dynamic Client Registration HTTP client."""
|
|
14
18
|
|
|
19
|
+
def __init__(self, *, timeout_seconds: int = _DEFAULT_TIMEOUT_SECONDS) -> None:
|
|
20
|
+
self._timeout_seconds = timeout_seconds
|
|
21
|
+
|
|
15
22
|
async def register(
|
|
16
23
|
self,
|
|
17
24
|
*,
|
|
@@ -22,6 +29,8 @@ class DcrClient:
|
|
|
22
29
|
logo_uri: str | None = None,
|
|
23
30
|
contacts: list[str] | None = None,
|
|
24
31
|
) -> dict[str, Any]:
|
|
32
|
+
validate_url(registration_url)
|
|
33
|
+
|
|
25
34
|
payload: dict[str, Any] = {
|
|
26
35
|
"redirect_uris": [redirect_uri],
|
|
27
36
|
"grant_types": ["authorization_code", "refresh_token"],
|
|
@@ -39,7 +48,7 @@ class DcrClient:
|
|
|
39
48
|
|
|
40
49
|
logger.info("Performing DCR at '%s'", registration_url)
|
|
41
50
|
|
|
42
|
-
async with httpx.AsyncClient(timeout=
|
|
51
|
+
async with httpx.AsyncClient(timeout=self._timeout_seconds) as client:
|
|
43
52
|
response = await client.post(
|
|
44
53
|
registration_url,
|
|
45
54
|
json=payload,
|
|
@@ -13,6 +13,7 @@ from oidcauthlib.utilities.environment.abstract_environment_variables import (
|
|
|
13
13
|
AbstractEnvironmentVariables,
|
|
14
14
|
)
|
|
15
15
|
from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
16
|
+
from oidcauthlib.utilities.url_validator import validate_url
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
@@ -59,6 +60,8 @@ class DcrManager:
|
|
|
59
60
|
f"provided (auth_provider='{auth_provider}')"
|
|
60
61
|
)
|
|
61
62
|
|
|
63
|
+
validate_url(registration_url)
|
|
64
|
+
|
|
62
65
|
cached = await self._find_cached(
|
|
63
66
|
auth_provider=auth_provider,
|
|
64
67
|
registration_url=registration_url,
|
|
@@ -136,7 +139,7 @@ class DcrManager:
|
|
|
136
139
|
item.updated = now
|
|
137
140
|
return item
|
|
138
141
|
|
|
139
|
-
await self._repository.insert_or_update(
|
|
142
|
+
persisted_id = await self._repository.insert_or_update(
|
|
140
143
|
collection_name=self._collection_name,
|
|
141
144
|
item=registration,
|
|
142
145
|
keys={
|
|
@@ -148,4 +151,5 @@ class DcrManager:
|
|
|
148
151
|
on_update=on_update,
|
|
149
152
|
)
|
|
150
153
|
|
|
151
|
-
|
|
154
|
+
# Return with the actual persisted ID (may differ on update)
|
|
155
|
+
return registration.model_copy(update={"id": persisted_id})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class AuthServerMetadata:
|
|
6
|
+
"""Parsed OAuth 2.0 Authorization Server Metadata (RFC 8414) or
|
|
7
|
+
OpenID Connect Discovery metadata.
|
|
8
|
+
|
|
9
|
+
Contains the subset of fields needed to configure an OAuth client:
|
|
10
|
+
authorization and token endpoints (required), plus optional registration
|
|
11
|
+
endpoint, issuer, and supported scopes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
authorization_endpoint: str
|
|
15
|
+
token_endpoint: str
|
|
16
|
+
registration_endpoint: str | None = None
|
|
17
|
+
issuer: str | None = None
|
|
18
|
+
scopes_supported: list[str] | None = None
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Protocol, Any
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from oidcauthlib.auth.well_known_configuration.auth_server_metadata import (
|
|
8
|
+
AuthServerMetadata,
|
|
9
|
+
)
|
|
10
|
+
from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
14
|
+
|
|
15
|
+
_DISCOVERY_TIMEOUT = httpx.Timeout(10.0)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthServerMetadataDiscoveryProtocol(Protocol):
|
|
19
|
+
"""Protocol for discovering OAuth authorization server metadata from a resource URL."""
|
|
20
|
+
|
|
21
|
+
async def discover(self, *, resource_url: str) -> AuthServerMetadata | None: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthServerMetadataDiscovery:
|
|
25
|
+
"""Discovers OAuth authorization server metadata from a resource server URL.
|
|
26
|
+
|
|
27
|
+
Implements RFC 8414 (OAuth 2.0 Authorization Server Metadata) with a
|
|
28
|
+
fallback to OpenID Connect Discovery. Given a resource URL, extracts
|
|
29
|
+
the origin (scheme + host + port) and attempts to fetch:
|
|
30
|
+
|
|
31
|
+
1. ``{origin}/.well-known/oauth-authorization-server`` (RFC 8414)
|
|
32
|
+
2. ``{origin}/.well-known/openid-configuration`` (OIDC Discovery)
|
|
33
|
+
|
|
34
|
+
Returns an ``AuthServerMetadata`` with the discovered endpoints, or
|
|
35
|
+
``None`` if neither well-known URL returns valid metadata.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _extract_base_url(url: str) -> str:
|
|
40
|
+
parsed = urlparse(url)
|
|
41
|
+
port_suffix = f":{parsed.port}" if parsed.port else ""
|
|
42
|
+
return f"{parsed.scheme}://{parsed.hostname}{port_suffix}"
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _parse_metadata(metadata: dict[str, Any]) -> AuthServerMetadata | None:
|
|
46
|
+
authorization_endpoint = metadata.get("authorization_endpoint")
|
|
47
|
+
token_endpoint = metadata.get("token_endpoint")
|
|
48
|
+
if not authorization_endpoint or not token_endpoint:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"Discovered metadata missing required endpoints "
|
|
51
|
+
"(authorization_endpoint=%s, token_endpoint=%s)",
|
|
52
|
+
authorization_endpoint,
|
|
53
|
+
token_endpoint,
|
|
54
|
+
)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
scopes: list[str] | None = None
|
|
58
|
+
scopes_supported = metadata.get("scopes_supported")
|
|
59
|
+
if isinstance(scopes_supported, list):
|
|
60
|
+
scopes = [s for s in scopes_supported if isinstance(s, str)]
|
|
61
|
+
|
|
62
|
+
return AuthServerMetadata(
|
|
63
|
+
authorization_endpoint=authorization_endpoint,
|
|
64
|
+
token_endpoint=token_endpoint,
|
|
65
|
+
registration_endpoint=metadata.get("registration_endpoint"),
|
|
66
|
+
issuer=metadata.get("issuer"),
|
|
67
|
+
scopes_supported=scopes,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def discover(self, *, resource_url: str) -> AuthServerMetadata | None:
|
|
71
|
+
"""Discover OAuth authorization server metadata for a resource URL.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
resource_url: The URL of the resource server (e.g. an MCP server).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
AuthServerMetadata if discovery succeeds, None otherwise.
|
|
78
|
+
"""
|
|
79
|
+
base_url = self._extract_base_url(resource_url)
|
|
80
|
+
|
|
81
|
+
well_known_urls = [
|
|
82
|
+
f"{base_url}/.well-known/oauth-authorization-server",
|
|
83
|
+
f"{base_url}/.well-known/openid-configuration",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
async with httpx.AsyncClient(timeout=_DISCOVERY_TIMEOUT) as client:
|
|
87
|
+
for url in well_known_urls:
|
|
88
|
+
try:
|
|
89
|
+
response = await client.get(url)
|
|
90
|
+
if response.status_code != 200:
|
|
91
|
+
logger.debug(
|
|
92
|
+
"Discovery fetch %s returned status %s, skipping",
|
|
93
|
+
url,
|
|
94
|
+
response.status_code,
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
metadata = response.json()
|
|
98
|
+
result = self._parse_metadata(metadata)
|
|
99
|
+
if result is not None:
|
|
100
|
+
logger.info(
|
|
101
|
+
"Discovered auth server metadata from %s for resource %s",
|
|
102
|
+
url,
|
|
103
|
+
resource_url,
|
|
104
|
+
)
|
|
105
|
+
return result
|
|
106
|
+
except httpx.TimeoutException:
|
|
107
|
+
logger.debug("Discovery fetch %s timed out", url)
|
|
108
|
+
except (httpx.HTTPError, ValueError) as e:
|
|
109
|
+
logger.debug("Discovery fetch %s failed: %s", url, e)
|
|
110
|
+
|
|
111
|
+
logger.info("No auth server metadata discovered for resource %s", resource_url)
|
|
112
|
+
return None
|
|
@@ -92,10 +92,11 @@ class WellKnownConfigurationCache:
|
|
|
92
92
|
async def read_list_async(self, *, auth_configs: list[AuthConfig]) -> None:
|
|
93
93
|
"""Fetch and cache discovery documents for multiple auth configs.
|
|
94
94
|
|
|
95
|
+
Configs without a ``well_known_uri`` (e.g. explicit-endpoint configs)
|
|
96
|
+
are silently skipped.
|
|
97
|
+
|
|
95
98
|
Args:
|
|
96
|
-
auth_configs: List of OIDC authorization configurations
|
|
97
|
-
Returns:
|
|
98
|
-
A list of WellKnownConfigurationCacheResult for successfully fetched configs.
|
|
99
|
+
auth_configs: List of OIDC authorization configurations.
|
|
99
100
|
Notes:
|
|
100
101
|
- Populates the in-memory cache and optional backing store.
|
|
101
102
|
- Aggregates JWKS into the class-level jwks KeySet.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""SSRF-safe URL validation for outbound HTTP requests.
|
|
2
|
+
|
|
3
|
+
Validates URLs before making HTTP requests to prevent Server-Side Request
|
|
4
|
+
Forgery (SSRF) attacks. Enforces HTTPS-only, rejects private/reserved IP
|
|
5
|
+
ranges, and resolves hostnames to verify the target is not internal.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ipaddress
|
|
9
|
+
import logging
|
|
10
|
+
import socket
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
17
|
+
|
|
18
|
+
_BLOCKED_NETWORKS = [
|
|
19
|
+
ipaddress.ip_network("127.0.0.0/8"), # loopback
|
|
20
|
+
ipaddress.ip_network("10.0.0.0/8"), # RFC 1918
|
|
21
|
+
ipaddress.ip_network("172.16.0.0/12"), # RFC 1918
|
|
22
|
+
ipaddress.ip_network("192.168.0.0/16"), # RFC 1918
|
|
23
|
+
ipaddress.ip_network("169.254.0.0/16"), # link-local
|
|
24
|
+
ipaddress.ip_network("0.0.0.0/8"), # "this" network
|
|
25
|
+
ipaddress.ip_network("100.64.0.0/10"), # carrier-grade NAT
|
|
26
|
+
ipaddress.ip_network("192.0.0.0/24"), # IETF protocol assignments
|
|
27
|
+
ipaddress.ip_network("198.18.0.0/15"), # benchmark testing
|
|
28
|
+
ipaddress.ip_network("::1/128"), # IPv6 loopback
|
|
29
|
+
ipaddress.ip_network("fc00::/7"), # IPv6 unique-local
|
|
30
|
+
ipaddress.ip_network("fe80::/10"), # IPv6 link-local
|
|
31
|
+
ipaddress.ip_network("::ffff:0:0/96"), # IPv4-mapped IPv6
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
_BLOCKED_HOSTNAMES = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"localhost",
|
|
37
|
+
"localhost.localdomain",
|
|
38
|
+
"metadata.google.internal", # GCP metadata
|
|
39
|
+
"169.254.169.254", # AWS/Azure/GCP metadata endpoint
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_ip_address(value: str) -> bool:
|
|
45
|
+
"""Return True if the string is a valid IPv4 or IPv6 address."""
|
|
46
|
+
try:
|
|
47
|
+
ipaddress.ip_address(value)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return False
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_private_ip(addr: str) -> bool:
|
|
54
|
+
"""Check whether an IP address falls in a blocked network range."""
|
|
55
|
+
try:
|
|
56
|
+
ip = ipaddress.ip_address(addr)
|
|
57
|
+
except ValueError:
|
|
58
|
+
return True # unparseable → reject
|
|
59
|
+
return any(ip in network for network in _BLOCKED_NETWORKS)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_url(url: str, *, allow_http: bool = False) -> str:
|
|
63
|
+
"""Validate a URL for safe outbound HTTP requests.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
url: The URL to validate.
|
|
67
|
+
allow_http: If True, permit ``http://`` in addition to ``https://``.
|
|
68
|
+
Defaults to False (HTTPS only).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The validated URL (unchanged).
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If the URL fails any validation check.
|
|
75
|
+
"""
|
|
76
|
+
parsed = urlparse(url)
|
|
77
|
+
|
|
78
|
+
# --- scheme ---
|
|
79
|
+
allowed_schemes = {"https"} if not allow_http else {"https", "http"}
|
|
80
|
+
if parsed.scheme not in allowed_schemes:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"URL scheme must be {' or '.join(sorted(allowed_schemes))}, "
|
|
83
|
+
f"got '{parsed.scheme}' in '{url}'"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# --- hostname ---
|
|
87
|
+
hostname = parsed.hostname
|
|
88
|
+
if not hostname:
|
|
89
|
+
raise ValueError(f"URL is missing a hostname: '{url}'")
|
|
90
|
+
|
|
91
|
+
if hostname.lower() in _BLOCKED_HOSTNAMES:
|
|
92
|
+
raise ValueError(f"URL hostname '{hostname}' is blocked")
|
|
93
|
+
|
|
94
|
+
# --- reject raw IP addresses as hostnames ---
|
|
95
|
+
# SSL/TLS certificates are issued for domain names, not IPs.
|
|
96
|
+
# A raw IP bypasses proper certificate validation.
|
|
97
|
+
if _is_ip_address(hostname):
|
|
98
|
+
raise ValueError(f"URL must use a hostname, not a raw IP address: '{hostname}'")
|
|
99
|
+
|
|
100
|
+
# --- resolve DNS and check all IPs ---
|
|
101
|
+
try:
|
|
102
|
+
addr_infos = socket.getaddrinfo(hostname, parsed.port or 443)
|
|
103
|
+
except socket.gaierror as exc:
|
|
104
|
+
raise ValueError(f"Cannot resolve hostname '{hostname}': {exc}") from exc
|
|
105
|
+
|
|
106
|
+
for _family, _type, _proto, _canonname, sockaddr in addr_infos:
|
|
107
|
+
ip_str = str(sockaddr[0])
|
|
108
|
+
if _is_private_ip(ip_str):
|
|
109
|
+
raise ValueError(f"URL '{url}' resolves to blocked IP {ip_str}")
|
|
110
|
+
|
|
111
|
+
logger.debug("URL validated: %s", url)
|
|
112
|
+
return url
|
|
@@ -53,6 +53,8 @@ oidcauthlib/auth/repository/mongo/mongo_repository.py
|
|
|
53
53
|
oidcauthlib/auth/routers/__init__.py
|
|
54
54
|
oidcauthlib/auth/routers/auth_router.py
|
|
55
55
|
oidcauthlib/auth/well_known_configuration/__init__.py
|
|
56
|
+
oidcauthlib/auth/well_known_configuration/auth_server_metadata.py
|
|
57
|
+
oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py
|
|
56
58
|
oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py
|
|
57
59
|
oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py
|
|
58
60
|
oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py
|
|
@@ -72,6 +74,7 @@ oidcauthlib/storage/storage_factory_creator.py
|
|
|
72
74
|
oidcauthlib/utilities/__init__.py
|
|
73
75
|
oidcauthlib/utilities/cached.py
|
|
74
76
|
oidcauthlib/utilities/mongo_url_utils.py
|
|
77
|
+
oidcauthlib/utilities/url_validator.py
|
|
75
78
|
oidcauthlib/utilities/environment/__init__.py
|
|
76
79
|
oidcauthlib/utilities/environment/abstract_environment_variables.py
|
|
77
80
|
oidcauthlib/utilities/environment/oidc_environment_variables.py
|
|
@@ -85,7 +88,6 @@ tests/test_mongo_url_utils.py
|
|
|
85
88
|
tests/test_simple.py
|
|
86
89
|
tests/auth/__init__.py
|
|
87
90
|
tests/auth/conftest.py
|
|
88
|
-
tests/auth/test_auth_config_explicit_endpoints.py
|
|
89
91
|
tests/auth/test_auth_helper.py
|
|
90
92
|
tests/auth/test_auth_helper2.py
|
|
91
93
|
tests/auth/test_auth_manager.py
|
|
@@ -95,9 +97,9 @@ tests/auth/cache/test_oauth_memory_cache.py
|
|
|
95
97
|
tests/auth/config/__init__.py
|
|
96
98
|
tests/auth/config/test_auth_config.py
|
|
97
99
|
tests/auth/config/test_auth_config_reader.py
|
|
100
|
+
tests/auth/config/test_auth_config_reader_register.py
|
|
98
101
|
tests/auth/config/test_auth_config_reader_thread_safety.py
|
|
99
102
|
tests/auth/dcr/__init__.py
|
|
100
|
-
tests/auth/dcr/conftest.py
|
|
101
103
|
tests/auth/dcr/test_dcr_client.py
|
|
102
104
|
tests/auth/dcr/test_dcr_manager.py
|
|
103
105
|
tests/auth/middleware/__init__.py
|
|
@@ -115,6 +117,7 @@ tests/auth/repository/mongo/__init__.py
|
|
|
115
117
|
tests/auth/repository/mongo/test_mongo_repository.py
|
|
116
118
|
tests/auth/repository/mongo/test_mongo_repository_real.py
|
|
117
119
|
tests/auth/well_known_configuration/__init__.py
|
|
120
|
+
tests/auth/well_known_configuration/test_auth_server_metadata_discovery.py
|
|
118
121
|
tests/auth/well_known_configuration/test_well_known_configuration_cache.py
|
|
119
122
|
tests/auth/well_known_configuration/test_well_known_configuration_manager.py
|
|
120
123
|
tests/storage/__init__.py
|
|
@@ -124,4 +127,5 @@ tests/storage/test_mongo_store_factory.py
|
|
|
124
127
|
tests/storage/test_storage_factory_security.py
|
|
125
128
|
tests/utilities/__init__.py
|
|
126
129
|
tests/utilities/test_cached.py
|
|
127
|
-
tests/utilities/test_mongo_url_utils.py
|
|
130
|
+
tests/utilities/test_mongo_url_utils.py
|
|
131
|
+
tests/utilities/test_url_validator.py
|