oidcauthlib 3.0.3__tar.gz → 3.0.5__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 (133) hide show
  1. {oidcauthlib-3.0.3/oidcauthlib.egg-info → oidcauthlib-3.0.5}/PKG-INFO +1 -1
  2. oidcauthlib-3.0.5/VERSION +1 -0
  3. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/dcr/dcr_client.py +30 -2
  4. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/dcr/dcr_manager.py +65 -3
  5. oidcauthlib-3.0.5/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +18 -0
  6. oidcauthlib-3.0.5/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +112 -0
  7. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5/oidcauthlib.egg-info}/PKG-INFO +1 -1
  8. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib.egg-info/SOURCES.txt +3 -0
  9. oidcauthlib-3.0.5/tests/auth/well_known_configuration/test_auth_server_metadata_discovery.py +184 -0
  10. oidcauthlib-3.0.3/VERSION +0 -1
  11. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/LICENSE +0 -0
  12. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/MANIFEST.in +0 -0
  13. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/Makefile +0 -0
  14. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/README.md +0 -0
  15. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/__init__.py +0 -0
  16. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/__init__.py +0 -0
  17. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/auth_helper.py +0 -0
  18. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/auth_manager.py +0 -0
  19. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/cache/__init__.py +0 -0
  20. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
  21. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
  22. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
  23. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/config/__init__.py +0 -0
  24. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/config/auth_config.py +0 -0
  25. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/config/auth_config_reader.py +0 -0
  26. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/dcr/__init__.py +0 -0
  27. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
  28. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/exceptions/__init__.py +0 -0
  29. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
  30. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
  31. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
  32. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
  33. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
  34. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/middleware/__init__.py +0 -0
  35. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
  36. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
  37. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/models/__init__.py +0 -0
  38. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/models/auth.py +0 -0
  39. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/models/base_db_model.py +0 -0
  40. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/models/cache_item.py +0 -0
  41. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/models/client_key_set.py +0 -0
  42. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/models/token.py +0 -0
  43. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/__init__.py +0 -0
  44. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/base_repository.py +0 -0
  45. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
  46. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
  47. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
  48. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
  49. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/repository/repository_factory.py +0 -0
  50. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/routers/__init__.py +0 -0
  51. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/routers/auth_router.py +0 -0
  52. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/token_reader.py +0 -0
  53. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
  54. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +0 -0
  55. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
  56. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
  57. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/container/__init__.py +0 -0
  58. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
  59. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/open_telemetry/__init__.py +0 -0
  60. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
  61. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/open_telemetry/span_names.py +0 -0
  62. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/py.typed +0 -0
  63. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/__init__.py +0 -0
  64. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
  65. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/memory_storage_factory.py +0 -0
  66. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
  67. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
  68. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
  69. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/storage_factory.py +0 -0
  70. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/storage/storage_factory_creator.py +0 -0
  71. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/__init__.py +0 -0
  72. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/cached.py +0 -0
  73. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/environment/__init__.py +0 -0
  74. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
  75. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/environment/oidc_environment_variables.py +0 -0
  76. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/logger/__init__.py +0 -0
  77. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/logger/log_levels.py +0 -0
  78. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/logger/logging_response.py +0 -0
  79. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
  80. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
  81. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib/utilities/url_validator.py +0 -0
  82. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib.egg-info/dependency_links.txt +0 -0
  83. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib.egg-info/not-zip-safe +0 -0
  84. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib.egg-info/requires.txt +0 -0
  85. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/oidcauthlib.egg-info/top_level.txt +0 -0
  86. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/setup.cfg +0 -0
  87. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/setup.py +0 -0
  88. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/__init__.py +0 -0
  89. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/__init__.py +0 -0
  90. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/cache/__init__.py +0 -0
  91. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/cache/test_oauth_memory_cache.py +0 -0
  92. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/config/__init__.py +0 -0
  93. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/config/test_auth_config.py +0 -0
  94. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/config/test_auth_config_reader.py +0 -0
  95. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/config/test_auth_config_reader_register.py +0 -0
  96. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/config/test_auth_config_reader_thread_safety.py +0 -0
  97. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/conftest.py +0 -0
  98. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/dcr/__init__.py +0 -0
  99. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/dcr/test_dcr_client.py +0 -0
  100. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/dcr/test_dcr_manager.py +0 -0
  101. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/middleware/__init__.py +0 -0
  102. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/middleware/test_request_scope_middleware.py +0 -0
  103. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/middleware/test_token_reader_middleware.py +0 -0
  104. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/models/__init__.py +0 -0
  105. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/models/test_auth.py +0 -0
  106. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/models/test_base_db_model.py +0 -0
  107. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/models/test_cache_item.py +0 -0
  108. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/models/test_token.py +0 -0
  109. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/repository/__init__.py +0 -0
  110. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/repository/memory/__init__.py +0 -0
  111. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/repository/memory/test_memory_repository.py +0 -0
  112. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/repository/mongo/__init__.py +0 -0
  113. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/repository/mongo/test_mongo_repository.py +0 -0
  114. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/repository/mongo/test_mongo_repository_real.py +0 -0
  115. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/test_auth_helper.py +0 -0
  116. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/test_auth_helper2.py +0 -0
  117. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/test_auth_manager.py +0 -0
  118. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/test_register_dynamic_provider.py +0 -0
  119. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/well_known_configuration/__init__.py +0 -0
  120. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/well_known_configuration/test_well_known_configuration_cache.py +0 -0
  121. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/auth/well_known_configuration/test_well_known_configuration_manager.py +0 -0
  122. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/conftest.py +0 -0
  123. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/storage/__init__.py +0 -0
  124. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/storage/test_mongo_gridfs_db.py +0 -0
  125. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/storage/test_mongo_gridfs_race_setup.py +0 -0
  126. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/storage/test_mongo_store_factory.py +0 -0
  127. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/storage/test_storage_factory_security.py +0 -0
  128. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/test_mongo_url_utils.py +0 -0
  129. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/test_simple.py +0 -0
  130. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/utilities/__init__.py +0 -0
  131. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/utilities/test_cached.py +0 -0
  132. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/utilities/test_mongo_url_utils.py +0 -0
  133. {oidcauthlib-3.0.3 → oidcauthlib-3.0.5}/tests/utilities/test_url_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.3
3
+ Version: 3.0.5
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.5
@@ -46,7 +46,16 @@ class DcrClient:
46
46
  if contacts:
47
47
  payload["contacts"] = contacts
48
48
 
49
- logger.info("Performing DCR at '%s'", registration_url)
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': client_id=%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,43 @@ 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.debug(
72
- "Using cached DCR credentials for '%s' (client_id=%s)",
88
+ logger.info(
89
+ "DCR: Using cached credentials for '%s' — client_id=%s, "
90
+ "expires_at=%s",
73
91
  auth_provider,
74
92
  cached.client_id,
93
+ cached.client_secret_expires_at or "never",
75
94
  )
76
95
  return cached
77
96
 
97
+ if cached and self._is_expired(cached):
98
+ logger.info(
99
+ "DCR: Cached credentials for '%s' are expired "
100
+ "(client_id=%s, expired_at=%s) — re-registering",
101
+ auth_provider,
102
+ cached.client_id,
103
+ cached.client_secret_expires_at,
104
+ )
105
+ else:
106
+ logger.info(
107
+ "DCR: No cached credentials found for '%s' — performing "
108
+ "new registration at '%s'",
109
+ auth_provider,
110
+ registration_url,
111
+ )
112
+
78
113
  dcr_response = await self._dcr_client.register(
79
114
  registration_url=registration_url,
80
115
  redirect_uri=self._redirect_uri,
@@ -84,11 +119,17 @@ class DcrManager:
84
119
  contacts=contacts,
85
120
  )
86
121
 
87
- return await self._persist(
122
+ registration = await self._persist(
88
123
  auth_provider=auth_provider,
89
124
  registration_url=registration_url,
90
125
  dcr_response=dcr_response,
91
126
  )
127
+ logger.info(
128
+ "DCR: Persisted credentials for '%s' — client_id=%s",
129
+ auth_provider,
130
+ registration.client_id,
131
+ )
132
+ return registration
92
133
 
93
134
  async def _find_cached(
94
135
  self,
@@ -96,6 +137,13 @@ class DcrManager:
96
137
  auth_provider: str,
97
138
  registration_url: str,
98
139
  ) -> DcrRegistration | None:
140
+ logger.debug(
141
+ "DCR: Querying cache for auth_provider='%s', "
142
+ "registration_url='%s', collection='%s'",
143
+ auth_provider,
144
+ registration_url,
145
+ self._collection_name,
146
+ )
99
147
  results = await self._repository.find_many(
100
148
  collection_name=self._collection_name,
101
149
  model_class=DcrRegistration,
@@ -104,6 +152,20 @@ class DcrManager:
104
152
  "registration_url": registration_url,
105
153
  },
106
154
  )
155
+ if results:
156
+ logger.debug(
157
+ "DCR: Cache hit for '%s' — found %d result(s), "
158
+ "client_id=%s",
159
+ auth_provider,
160
+ len(results),
161
+ results[0].client_id,
162
+ )
163
+ else:
164
+ logger.debug(
165
+ "DCR: Cache miss for '%s' at '%s'",
166
+ auth_provider,
167
+ registration_url,
168
+ )
107
169
  return results[0] if results else None
108
170
 
109
171
  @staticmethod
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.3
3
+ Version: 3.0.5
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
@@ -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
@@ -0,0 +1,184 @@
1
+ """Tests for AuthServerMetadataDiscovery — RFC 8414 and OIDC Discovery."""
2
+
3
+ import httpx
4
+ import pytest
5
+ import respx
6
+
7
+ from oidcauthlib.auth.well_known_configuration.auth_server_metadata_discovery import (
8
+ AuthServerMetadataDiscovery,
9
+ )
10
+
11
+ _BASE = "https://mcp.example.com"
12
+ _RESOURCE_URL = f"{_BASE}/v1/mcp"
13
+ _RFC8414_URL = f"{_BASE}/.well-known/oauth-authorization-server"
14
+ _OIDC_URL = f"{_BASE}/.well-known/openid-configuration"
15
+
16
+ _VALID_METADATA = {
17
+ "issuer": "https://auth.example.com",
18
+ "authorization_endpoint": "https://auth.example.com/authorize",
19
+ "token_endpoint": "https://auth.example.com/token",
20
+ "registration_endpoint": "https://auth.example.com/register",
21
+ "scopes_supported": ["openid", "profile", "email"],
22
+ }
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ @respx.mock
27
+ async def test_rfc8414_success() -> None:
28
+ """RFC 8414 endpoint returns valid metadata."""
29
+ respx.get(_RFC8414_URL).mock(return_value=httpx.Response(200, json=_VALID_METADATA))
30
+
31
+ discovery = AuthServerMetadataDiscovery()
32
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
33
+
34
+ assert result is not None
35
+ assert result.authorization_endpoint == "https://auth.example.com/authorize"
36
+ assert result.token_endpoint == "https://auth.example.com/token"
37
+ assert result.registration_endpoint == "https://auth.example.com/register"
38
+ assert result.issuer == "https://auth.example.com"
39
+ assert result.scopes_supported == ["openid", "profile", "email"]
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ @respx.mock
44
+ async def test_rfc8414_fails_oidc_succeeds() -> None:
45
+ """RFC 8414 returns 404, falls back to OIDC Discovery."""
46
+ respx.get(_RFC8414_URL).mock(return_value=httpx.Response(404))
47
+ respx.get(_OIDC_URL).mock(return_value=httpx.Response(200, json=_VALID_METADATA))
48
+
49
+ discovery = AuthServerMetadataDiscovery()
50
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
51
+
52
+ assert result is not None
53
+ assert result.authorization_endpoint == "https://auth.example.com/authorize"
54
+ assert result.token_endpoint == "https://auth.example.com/token"
55
+
56
+
57
+ @pytest.mark.asyncio
58
+ @respx.mock
59
+ async def test_both_endpoints_fail() -> None:
60
+ """Both well-known endpoints return errors — returns None."""
61
+ respx.get(_RFC8414_URL).mock(return_value=httpx.Response(404))
62
+ respx.get(_OIDC_URL).mock(return_value=httpx.Response(500))
63
+
64
+ discovery = AuthServerMetadataDiscovery()
65
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
66
+
67
+ assert result is None
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ @respx.mock
72
+ async def test_malformed_json() -> None:
73
+ """Endpoint returns non-JSON — returns None."""
74
+ respx.get(_RFC8414_URL).mock(return_value=httpx.Response(200, text="not json"))
75
+ respx.get(_OIDC_URL).mock(return_value=httpx.Response(200, text="also not json"))
76
+
77
+ discovery = AuthServerMetadataDiscovery()
78
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
79
+
80
+ assert result is None
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ @respx.mock
85
+ async def test_missing_required_endpoints() -> None:
86
+ """Metadata without authorization_endpoint or token_endpoint — returns None."""
87
+ respx.get(_RFC8414_URL).mock(
88
+ return_value=httpx.Response(200, json={"issuer": "https://auth.example.com"})
89
+ )
90
+ respx.get(_OIDC_URL).mock(return_value=httpx.Response(404))
91
+
92
+ discovery = AuthServerMetadataDiscovery()
93
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
94
+
95
+ assert result is None
96
+
97
+
98
+ @pytest.mark.asyncio
99
+ @respx.mock
100
+ async def test_network_timeout() -> None:
101
+ """Network timeout — returns None."""
102
+ respx.get(_RFC8414_URL).mock(side_effect=httpx.ConnectTimeout("timeout"))
103
+ respx.get(_OIDC_URL).mock(side_effect=httpx.ConnectTimeout("timeout"))
104
+
105
+ discovery = AuthServerMetadataDiscovery()
106
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
107
+
108
+ assert result is None
109
+
110
+
111
+ @pytest.mark.asyncio
112
+ @respx.mock
113
+ async def test_no_scopes_in_metadata() -> None:
114
+ """Metadata without scopes_supported — scopes_supported is None."""
115
+ metadata = {
116
+ "authorization_endpoint": "https://auth.example.com/authorize",
117
+ "token_endpoint": "https://auth.example.com/token",
118
+ }
119
+ respx.get(_RFC8414_URL).mock(return_value=httpx.Response(200, json=metadata))
120
+
121
+ discovery = AuthServerMetadataDiscovery()
122
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
123
+
124
+ assert result is not None
125
+ assert result.scopes_supported is None
126
+
127
+
128
+ @pytest.mark.asyncio
129
+ @respx.mock
130
+ async def test_url_with_port() -> None:
131
+ """Resource URL with custom port — base URL preserves port."""
132
+ url = "https://mcp.example.com:8443/v1/mcp"
133
+ respx.get(
134
+ "https://mcp.example.com:8443/.well-known/oauth-authorization-server"
135
+ ).mock(return_value=httpx.Response(200, json=_VALID_METADATA))
136
+
137
+ discovery = AuthServerMetadataDiscovery()
138
+ result = await discovery.discover(resource_url=url)
139
+
140
+ assert result is not None
141
+ assert result.authorization_endpoint == "https://auth.example.com/authorize"
142
+
143
+
144
+ class TestExtractBaseUrl:
145
+ def test_strips_path(self) -> None:
146
+ assert (
147
+ AuthServerMetadataDiscovery._extract_base_url(
148
+ "https://mcp.example.com/v1/mcp"
149
+ )
150
+ == "https://mcp.example.com"
151
+ )
152
+
153
+ def test_preserves_port(self) -> None:
154
+ assert (
155
+ AuthServerMetadataDiscovery._extract_base_url(
156
+ "https://mcp.example.com:8443/v1/mcp"
157
+ )
158
+ == "https://mcp.example.com:8443"
159
+ )
160
+
161
+ def test_no_path(self) -> None:
162
+ assert (
163
+ AuthServerMetadataDiscovery._extract_base_url("https://mcp.example.com")
164
+ == "https://mcp.example.com"
165
+ )
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ @respx.mock
170
+ async def test_no_registration_endpoint() -> None:
171
+ """Metadata without registration_endpoint — registration_endpoint is None."""
172
+ metadata = {
173
+ "authorization_endpoint": "https://auth.example.com/authorize",
174
+ "token_endpoint": "https://auth.example.com/token",
175
+ "issuer": "https://auth.example.com",
176
+ }
177
+ respx.get(_RFC8414_URL).mock(return_value=httpx.Response(200, json=metadata))
178
+
179
+ discovery = AuthServerMetadataDiscovery()
180
+ result = await discovery.discover(resource_url=_RESOURCE_URL)
181
+
182
+ assert result is not None
183
+ assert result.registration_endpoint is None
184
+ assert result.issuer == "https://auth.example.com"
oidcauthlib-3.0.3/VERSION DELETED
@@ -1 +0,0 @@
1
- 3.0.3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes