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.
Files changed (136) hide show
  1. {oidcauthlib-3.0.2/oidcauthlib.egg-info → oidcauthlib-3.0.4}/PKG-INFO +1 -1
  2. oidcauthlib-3.0.4/VERSION +1 -0
  3. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/auth_manager.py +42 -17
  4. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/config/auth_config.py +14 -5
  5. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/config/auth_config_reader.py +60 -16
  6. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/dcr_client.py +10 -1
  7. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/dcr_manager.py +6 -2
  8. oidcauthlib-3.0.4/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +18 -0
  9. oidcauthlib-3.0.4/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +112 -0
  10. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +4 -3
  11. oidcauthlib-3.0.4/oidcauthlib/utilities/url_validator.py +112 -0
  12. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4/oidcauthlib.egg-info}/PKG-INFO +1 -1
  13. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/SOURCES.txt +7 -3
  14. oidcauthlib-3.0.4/tests/auth/config/test_auth_config.py +72 -0
  15. oidcauthlib-3.0.4/tests/auth/config/test_auth_config_reader_register.py +200 -0
  16. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/dcr/test_dcr_client.py +39 -3
  17. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/dcr/test_dcr_manager.py +76 -4
  18. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_register_dynamic_provider.py +84 -1
  19. oidcauthlib-3.0.4/tests/auth/well_known_configuration/test_auth_server_metadata_discovery.py +184 -0
  20. oidcauthlib-3.0.4/tests/utilities/test_url_validator.py +155 -0
  21. oidcauthlib-3.0.2/VERSION +0 -1
  22. oidcauthlib-3.0.2/tests/auth/config/test_auth_config.py +0 -32
  23. oidcauthlib-3.0.2/tests/auth/dcr/conftest.py +0 -14
  24. oidcauthlib-3.0.2/tests/auth/test_auth_config_explicit_endpoints.py +0 -79
  25. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/LICENSE +0 -0
  26. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/MANIFEST.in +0 -0
  27. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/Makefile +0 -0
  28. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/README.md +0 -0
  29. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/__init__.py +0 -0
  30. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/__init__.py +0 -0
  31. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/auth_helper.py +0 -0
  32. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/__init__.py +0 -0
  33. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
  34. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
  35. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
  36. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/config/__init__.py +0 -0
  37. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/__init__.py +0 -0
  38. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
  39. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/__init__.py +0 -0
  40. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
  41. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
  42. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
  43. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
  44. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
  45. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/middleware/__init__.py +0 -0
  46. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
  47. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
  48. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/__init__.py +0 -0
  49. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/auth.py +0 -0
  50. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/base_db_model.py +0 -0
  51. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/cache_item.py +0 -0
  52. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/client_key_set.py +0 -0
  53. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/models/token.py +0 -0
  54. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/__init__.py +0 -0
  55. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/base_repository.py +0 -0
  56. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
  57. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
  58. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
  59. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
  60. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/repository/repository_factory.py +0 -0
  61. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/routers/__init__.py +0 -0
  62. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/routers/auth_router.py +0 -0
  63. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/token_reader.py +0 -0
  64. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
  65. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
  66. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
  67. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/container/__init__.py +0 -0
  68. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
  69. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/open_telemetry/__init__.py +0 -0
  70. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
  71. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/open_telemetry/span_names.py +0 -0
  72. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/py.typed +0 -0
  73. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/__init__.py +0 -0
  74. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
  75. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/memory_storage_factory.py +0 -0
  76. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
  77. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
  78. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
  79. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/storage_factory.py +0 -0
  80. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/storage/storage_factory_creator.py +0 -0
  81. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/__init__.py +0 -0
  82. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/cached.py +0 -0
  83. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/environment/__init__.py +0 -0
  84. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
  85. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/environment/oidc_environment_variables.py +0 -0
  86. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/__init__.py +0 -0
  87. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/log_levels.py +0 -0
  88. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/logging_response.py +0 -0
  89. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
  90. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
  91. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/dependency_links.txt +0 -0
  92. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/not-zip-safe +0 -0
  93. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/requires.txt +0 -0
  94. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/oidcauthlib.egg-info/top_level.txt +0 -0
  95. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/setup.cfg +0 -0
  96. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/setup.py +0 -0
  97. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/__init__.py +0 -0
  98. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/__init__.py +0 -0
  99. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/cache/__init__.py +0 -0
  100. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/cache/test_oauth_memory_cache.py +0 -0
  101. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/config/__init__.py +0 -0
  102. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/config/test_auth_config_reader.py +0 -0
  103. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/config/test_auth_config_reader_thread_safety.py +0 -0
  104. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/conftest.py +0 -0
  105. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/dcr/__init__.py +0 -0
  106. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/middleware/__init__.py +0 -0
  107. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/middleware/test_request_scope_middleware.py +0 -0
  108. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/middleware/test_token_reader_middleware.py +0 -0
  109. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/__init__.py +0 -0
  110. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_auth.py +0 -0
  111. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_base_db_model.py +0 -0
  112. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_cache_item.py +0 -0
  113. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/models/test_token.py +0 -0
  114. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/__init__.py +0 -0
  115. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/memory/__init__.py +0 -0
  116. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/memory/test_memory_repository.py +0 -0
  117. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/mongo/__init__.py +0 -0
  118. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/mongo/test_mongo_repository.py +0 -0
  119. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/repository/mongo/test_mongo_repository_real.py +0 -0
  120. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_auth_helper.py +0 -0
  121. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_auth_helper2.py +0 -0
  122. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/test_auth_manager.py +0 -0
  123. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/well_known_configuration/__init__.py +0 -0
  124. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/well_known_configuration/test_well_known_configuration_cache.py +0 -0
  125. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/auth/well_known_configuration/test_well_known_configuration_manager.py +0 -0
  126. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/conftest.py +0 -0
  127. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/__init__.py +0 -0
  128. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_mongo_gridfs_db.py +0 -0
  129. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_mongo_gridfs_race_setup.py +0 -0
  130. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_mongo_store_factory.py +0 -0
  131. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/storage/test_storage_factory_security.py +0 -0
  132. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/test_mongo_url_utils.py +0 -0
  133. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/test_simple.py +0 -0
  134. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/utilities/__init__.py +0 -0
  135. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/utilities/test_cached.py +0 -0
  136. {oidcauthlib-3.0.2 → oidcauthlib-3.0.4}/tests/utilities/test_mongo_url_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: oidcauthlib
5
5
  Home-page: https://github.com/icanbwell/oidc-auth-lib
6
6
  Author: Imran Qureshi
@@ -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 (AbstractEnvironmentVariables): The environment variables for the application.
70
- auth_config_reader (AuthConfigReader): The reader for authentication configurations.
71
- token_reader (TokenReader): The reader for tokens.
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, and deduplication.
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": auth_config.client_id,
172
- "client_secret": auth_config.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
- auth_config.client_id,
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, used to identify the application making the request.",
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.error("auth_providers environment variable is not set")
73
- raise ValueError("auth_providers environment variable must be set")
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=10) as client:
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
- return registration
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 (must have well_known_uri).
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: oidcauthlib
5
5
  Home-page: https://github.com/icanbwell/oidc-auth-lib
6
6
  Author: Imran Qureshi
@@ -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