oidcauthlib 3.0.17__tar.gz → 3.0.19__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 (89) hide show
  1. {oidcauthlib-3.0.17/oidcauthlib.egg-info → oidcauthlib-3.0.19}/PKG-INFO +1 -1
  2. oidcauthlib-3.0.19/VERSION +1 -0
  3. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/token_reader.py +25 -3
  4. oidcauthlib-3.0.19/oidcauthlib/cache_gate/__init__.py +4 -0
  5. oidcauthlib-3.0.19/oidcauthlib/cache_gate/advisory_lock.py +168 -0
  6. oidcauthlib-3.0.19/oidcauthlib/cache_gate/version_gate.py +103 -0
  7. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/environment/oidc_environment_variables.py +24 -0
  8. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19/oidcauthlib.egg-info}/PKG-INFO +1 -1
  9. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib.egg-info/SOURCES.txt +3 -0
  10. oidcauthlib-3.0.17/VERSION +0 -1
  11. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/LICENSE +0 -0
  12. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/MANIFEST.in +0 -0
  13. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/README.md +0 -0
  14. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/__init__.py +0 -0
  15. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/__init__.py +0 -0
  16. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/auth_helper.py +0 -0
  17. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/auth_manager.py +0 -0
  18. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/cache/__init__.py +0 -0
  19. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
  20. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
  21. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
  22. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/config/__init__.py +0 -0
  23. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/config/auth_config.py +0 -0
  24. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/config/auth_config_reader.py +0 -0
  25. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/dcr/__init__.py +0 -0
  26. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/dcr/dcr_client.py +0 -0
  27. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/dcr/dcr_manager.py +0 -0
  28. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
  29. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/exceptions/__init__.py +0 -0
  30. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
  31. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
  32. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
  33. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
  34. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
  35. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/middleware/__init__.py +0 -0
  36. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
  37. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
  38. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/models/__init__.py +0 -0
  39. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/models/auth.py +0 -0
  40. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/models/base_db_model.py +0 -0
  41. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/models/cache_item.py +0 -0
  42. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/models/client_key_set.py +0 -0
  43. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/models/token.py +0 -0
  44. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/__init__.py +0 -0
  45. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/base_repository.py +0 -0
  46. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
  47. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
  48. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
  49. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
  50. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/repository/repository_factory.py +0 -0
  51. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/routers/__init__.py +0 -0
  52. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/routers/auth_router.py +0 -0
  53. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
  54. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +0 -0
  55. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +0 -0
  56. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +0 -0
  57. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
  58. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
  59. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/container/__init__.py +0 -0
  60. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
  61. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/open_telemetry/__init__.py +0 -0
  62. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
  63. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/open_telemetry/span_names.py +0 -0
  64. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/py.typed +0 -0
  65. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/__init__.py +0 -0
  66. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
  67. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/memory_storage_factory.py +0 -0
  68. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
  69. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
  70. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
  71. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/storage_factory.py +0 -0
  72. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/storage/storage_factory_creator.py +0 -0
  73. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/__init__.py +0 -0
  74. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/cached.py +0 -0
  75. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/environment/__init__.py +0 -0
  76. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
  77. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/logger/__init__.py +0 -0
  78. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/logger/log_levels.py +0 -0
  79. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/logger/logging_response.py +0 -0
  80. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
  81. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
  82. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib/utilities/url_validator.py +0 -0
  83. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib.egg-info/dependency_links.txt +0 -0
  84. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib.egg-info/requires.txt +0 -0
  85. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/oidcauthlib.egg-info/top_level.txt +0 -0
  86. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/pyproject.toml +0 -0
  87. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/setup.cfg +0 -0
  88. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/tests/test_mongo_url_utils.py +0 -0
  89. {oidcauthlib-3.0.17 → oidcauthlib-3.0.19}/tests/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.17
3
+ Version: 3.0.19
4
4
  Summary: oidcauthlib
5
5
  Author-email: Imran Qureshi <imran.qureshi@bwell.com>
6
6
  License: Apache-2.0
@@ -0,0 +1 @@
1
+ 3.0.19
@@ -26,6 +26,9 @@ from oidcauthlib.auth.exceptions.authorization_bearer_token_missing_exception im
26
26
  AuthorizationBearerTokenMissingException,
27
27
  )
28
28
  from oidcauthlib.auth.models.token import Token
29
+ from oidcauthlib.utilities.environment.oidc_environment_variables import (
30
+ OidcEnvironmentVariables,
31
+ )
29
32
  from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
30
33
  from oidcauthlib.auth.well_known_configuration.well_known_configuration_manager import (
31
34
  WellKnownConfigurationManager,
@@ -61,12 +64,19 @@ class TokenReader:
61
64
  - Algorithms used for verification are configurable via the constructor.
62
65
  """
63
66
 
67
+ # Class-level default so test doubles that bypass __init__ (a pattern
68
+ # already used in this test suite, e.g. tests/auth/test_token_reader_audience.py's
69
+ # _TestTokenReader) still find a usable environment_variables via normal
70
+ # attribute lookup, instead of raising AttributeError on first access.
71
+ environment_variables: OidcEnvironmentVariables = OidcEnvironmentVariables()
72
+
64
73
  def __init__(
65
74
  self,
66
75
  *,
67
76
  algorithms: Optional[list[str]] = None,
68
77
  auth_config_reader: AuthConfigReader,
69
78
  well_known_config_manager: WellKnownConfigurationManager,
79
+ environment_variables: Optional[OidcEnvironmentVariables] = None,
70
80
  ):
71
81
  """
72
82
  Initialize TokenReader with dependencies and verification settings.
@@ -75,6 +85,8 @@ class TokenReader:
75
85
  algorithms: Allowed JWT algorithms for signature verification (e.g., ["RS256"]).
76
86
  auth_config_reader: Provider configuration reader used for issuer/audience validation.
77
87
  well_known_config_manager: Manager responsible for well-known configs and JWKS retrieval.
88
+ environment_variables: Source for jwt_clock_skew_leeway_seconds. Defaults to a
89
+ plain OidcEnvironmentVariables() (reads directly from os.environ) if not given.
78
90
  Raises:
79
91
  ValueError: If required dependencies or provider configs are missing.
80
92
  TypeError: If dependency types do not match expected classes.
@@ -99,6 +111,8 @@ class TokenReader:
99
111
  if not isinstance(self._well_known_config_manager, WellKnownConfigurationManager):
100
112
  raise TypeError("well_known_config_manager must be an instance of WellKnownConfigurationManager")
101
113
 
114
+ self.environment_variables: OidcEnvironmentVariables = environment_variables or OidcEnvironmentVariables()
115
+
102
116
  @staticmethod
103
117
  def extract_token(*, authorization_header: str | None) -> Optional[str]:
104
118
  """
@@ -192,6 +206,10 @@ class TokenReader:
192
206
  if not token:
193
207
  raise ValueError("Token must not be empty")
194
208
 
209
+ # Read outside the try/except below so a misconfigured env var (ValueError)
210
+ # surfaces as a config error, not a per-request "invalid token" failure.
211
+ leeway = self.environment_variables.jwt_clock_skew_leeway_seconds
212
+
195
213
  jwks: KeySet = await self._well_known_config_manager.get_jwks_async()
196
214
 
197
215
  # get kids from jwks for logging
@@ -294,8 +312,11 @@ class TokenReader:
294
312
 
295
313
  exp_str = to_eastern_time(exp)
296
314
  now_str = to_eastern_time(now)
297
- # Create claims registry
298
- claims_requests = jwt.JWTClaimsRegistry()
315
+ # Create claims registry. leeway tolerates clock skew between the
316
+ # token issuer and this process (e.g. a container/VM clock
317
+ # drifting from its host) -- without it, validate_iat rejects a
318
+ # token as "issued in the future" on any skew at all.
319
+ claims_requests = jwt.JWTClaimsRegistry(leeway=leeway)
299
320
  claims_requests.validate(verified.claims)
300
321
 
301
322
  logger.debug(f"Successfully verified token: {token}")
@@ -352,7 +373,8 @@ class TokenReader:
352
373
  verified = jwt.decode(access_token, jwks, algorithms=self.algorithms)
353
374
  exp = verified.claims.get("exp")
354
375
  now = time.time()
355
- if exp and exp < now:
376
+ leeway = self.environment_variables.jwt_clock_skew_leeway_seconds
377
+ if exp and exp < now - leeway:
356
378
  logger.warning(f"Token has expired. Exp: {exp}, Now: {now}")
357
379
  return False
358
380
  return True
@@ -0,0 +1,4 @@
1
+ from oidcauthlib.cache_gate.advisory_lock import AdvisoryLock
2
+ from oidcauthlib.cache_gate.version_gate import VersionGate
3
+
4
+ __all__ = ["AdvisoryLock", "VersionGate"]
@@ -0,0 +1,168 @@
1
+ """Staged here pending extraction into a standalone shared-infra package —
2
+ this module has no OIDC-specific logic and doesn't belong in oidc-auth-lib
3
+ long-term; it's here because this is the one package every consumer of
4
+ VersionGate already depends on.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import logging
12
+ import os
13
+ import uuid
14
+ from types import TracebackType
15
+
16
+ from key_value.aio.protocols.key_value import AsyncKeyValueProtocol
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ LOCK_COLLECTION = "advisory_locks"
21
+
22
+
23
+ class AdvisoryLock:
24
+ """Distributed advisory lock backed by any AsyncKeyValueProtocol store.
25
+
26
+ Uses put-with-TTL so that stale locks auto-expire if the holder crashes.
27
+ Acquire semantics rely on get-then-put — not atomic compare-and-swap —
28
+ so there is a small race window. For leader-election among cooperating
29
+ workers at startup this is acceptable; do not use for high-contention
30
+ critical sections.
31
+
32
+ While held, a background task refreshes the lock's TTL every
33
+ `heartbeat_interval_seconds` (default `ttl_seconds / 2`) — so `ttl_seconds`
34
+ only bounds how quickly a *crashed* holder's lock is reclaimed, not how
35
+ long the guarded critical section may run. Without this, a critical
36
+ section that legitimately outlives `ttl_seconds` would have its lock
37
+ entry expire while still running, letting a second holder acquire the
38
+ same lock and re-enter concurrently — exactly what the lock exists to
39
+ prevent. A single missed heartbeat is not fatal: it's logged and retried
40
+ on the next tick, with a full `heartbeat_interval_seconds` of margin
41
+ before the lock would actually expire.
42
+
43
+ Usage:
44
+ async with AdvisoryLock(store, "skill_sync", ttl_seconds=300) as acquired:
45
+ if acquired:
46
+ await do_sync()
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ store: AsyncKeyValueProtocol,
52
+ lock_name: str,
53
+ *,
54
+ ttl_seconds: int = 300,
55
+ collection: str = LOCK_COLLECTION,
56
+ heartbeat_interval_seconds: float | None = None,
57
+ ) -> None:
58
+ self._store = store
59
+ self._lock_name = lock_name
60
+ self._ttl_seconds = ttl_seconds
61
+ self._collection = collection
62
+ self._heartbeat_interval_seconds = heartbeat_interval_seconds or (ttl_seconds / 2)
63
+ self._holder_id = f"{os.getpid()}-{uuid.uuid4().hex[:8]}"
64
+ self._acquired = False
65
+ self._heartbeat_task: asyncio.Task[None] | None = None
66
+
67
+ @property
68
+ def acquired(self) -> bool:
69
+ return self._acquired
70
+
71
+ async def __aenter__(self) -> bool:
72
+ existing = await self._store.get(self._lock_name, collection=self._collection)
73
+
74
+ if existing is not None:
75
+ logger.info(
76
+ "AdvisoryLock: lock '%s' held by '%s' — skipping.",
77
+ self._lock_name,
78
+ existing.get("holder", "unknown"),
79
+ )
80
+ self._acquired = False
81
+ return False
82
+
83
+ await self._store.put(
84
+ self._lock_name,
85
+ {"holder": self._holder_id},
86
+ collection=self._collection,
87
+ ttl=self._ttl_seconds,
88
+ )
89
+
90
+ # Verify we won the race (read-after-write check)
91
+ written = await self._store.get(self._lock_name, collection=self._collection)
92
+ if written is not None and written.get("holder") == self._holder_id:
93
+ self._acquired = True
94
+ logger.info(
95
+ "AdvisoryLock: acquired lock '%s' (holder=%s, ttl=%ds)",
96
+ self._lock_name,
97
+ self._holder_id,
98
+ self._ttl_seconds,
99
+ )
100
+ self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
101
+ else:
102
+ self._acquired = False
103
+ logger.info(
104
+ "AdvisoryLock: lost race for lock '%s' — another holder won.",
105
+ self._lock_name,
106
+ )
107
+
108
+ return self._acquired
109
+
110
+ async def _heartbeat_loop(self) -> None:
111
+ while True:
112
+ await asyncio.sleep(self._heartbeat_interval_seconds)
113
+ try:
114
+ await self._store.put(
115
+ self._lock_name,
116
+ {"holder": self._holder_id},
117
+ collection=self._collection,
118
+ ttl=self._ttl_seconds,
119
+ )
120
+ logger.debug(
121
+ "AdvisoryLock: refreshed lock '%s' (holder=%s, ttl=%ds)",
122
+ self._lock_name,
123
+ self._holder_id,
124
+ self._ttl_seconds,
125
+ )
126
+ except Exception:
127
+ logger.warning(
128
+ "AdvisoryLock: failed to refresh lock '%s' (holder=%s) — will retry "
129
+ "in %.1fs; lock still expires in up to %ds if refreshes keep failing.",
130
+ self._lock_name,
131
+ self._holder_id,
132
+ self._heartbeat_interval_seconds,
133
+ self._ttl_seconds,
134
+ exc_info=True,
135
+ )
136
+
137
+ async def __aexit__(
138
+ self,
139
+ exc_type: type[BaseException] | None,
140
+ exc_val: BaseException | None,
141
+ exc_tb: TracebackType | None,
142
+ ) -> None:
143
+ if self._heartbeat_task is not None:
144
+ self._heartbeat_task.cancel()
145
+ with contextlib.suppress(asyncio.CancelledError):
146
+ await self._heartbeat_task
147
+ self._heartbeat_task = None
148
+
149
+ if self._acquired:
150
+ try:
151
+ await self._store.delete(self._lock_name, collection=self._collection)
152
+ logger.info(
153
+ "AdvisoryLock: released lock '%s' (holder=%s)",
154
+ self._lock_name,
155
+ self._holder_id,
156
+ )
157
+ except Exception:
158
+ # Don't let a failed release mask an exception already propagating
159
+ # from the caller's critical section. The heartbeat has stopped, so
160
+ # the lock still self-clears once its TTL elapses.
161
+ logger.warning(
162
+ "AdvisoryLock: failed to release lock '%s' (holder=%s) — it will "
163
+ "still expire via TTL in up to %ds.",
164
+ self._lock_name,
165
+ self._holder_id,
166
+ self._ttl_seconds,
167
+ exc_info=True,
168
+ )
@@ -0,0 +1,103 @@
1
+ """Staged here pending extraction into a standalone shared-infra package —
2
+ this module has no OIDC-specific logic and doesn't belong in oidc-auth-lib
3
+ long-term; it's here because this is the one package both current consumers
4
+ (baileyai, baileyai-skills-service) already depend on.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from collections.abc import Awaitable, Callable
11
+
12
+ from key_value.aio.protocols.key_value import AsyncKeyValueProtocol
13
+
14
+ from oidcauthlib.cache_gate.advisory_lock import AdvisoryLock
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ VERSION_GATE_COLLECTION = "version_gate"
19
+
20
+
21
+ class VersionGate:
22
+ """Runs an action at most once per distinct version, across a pod fleet.
23
+
24
+ Compares `current_version` against a marker persisted in `store`. If it
25
+ differs, runs `action` and — only once `action` completes without raising
26
+ — persists the new marker. The marker is written *after* the action
27
+ succeeds, never before: writing it first would let a crashed/incomplete
28
+ action look "done" to every other pod, forever, until the version next
29
+ changes.
30
+
31
+ Pass `lock_name` when `action` is not safe to run concurrently from
32
+ multiple pods (e.g. a multi-step load with partial-write visibility).
33
+ Omit it when `action` is idempotent under concurrent execution (e.g. a
34
+ pure cache clear followed by lazy repopulation) — redundant concurrent
35
+ runs are then harmless and a lock only adds latency/contention.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ store: AsyncKeyValueProtocol,
42
+ key: str,
43
+ collection: str = VERSION_GATE_COLLECTION,
44
+ lock_name: str | None = None,
45
+ lock_ttl_seconds: int = 300,
46
+ lock_heartbeat_interval_seconds: float | None = None,
47
+ ) -> None:
48
+ self._store = store
49
+ self._key = key
50
+ self._collection = collection
51
+ self._lock_name = lock_name
52
+ self._lock_ttl_seconds = lock_ttl_seconds
53
+ self._lock_heartbeat_interval_seconds = lock_heartbeat_interval_seconds
54
+
55
+ async def run_if_changed_async(
56
+ self,
57
+ *,
58
+ current_version: str,
59
+ action: Callable[[], Awaitable[None]],
60
+ ) -> bool:
61
+ """Runs `action` and records `current_version` iff it differs from the
62
+ stored marker. Returns True iff `action` ran.
63
+ """
64
+ if await self._is_current(current_version):
65
+ return False
66
+
67
+ if self._lock_name is None:
68
+ await action()
69
+ await self._mark(current_version)
70
+ return True
71
+
72
+ async with AdvisoryLock(
73
+ self._store,
74
+ self._lock_name,
75
+ ttl_seconds=self._lock_ttl_seconds,
76
+ heartbeat_interval_seconds=self._lock_heartbeat_interval_seconds,
77
+ ) as acquired:
78
+ if not acquired:
79
+ logger.info(
80
+ "VersionGate: lock '%s' held by another pod — skipping.",
81
+ self._lock_name,
82
+ )
83
+ return False
84
+
85
+ # Re-check: whoever held the lock before us may have already
86
+ # handled this version change while we were waiting to acquire.
87
+ if await self._is_current(current_version):
88
+ return False
89
+
90
+ await action()
91
+ await self._mark(current_version)
92
+ return True
93
+
94
+ async def _is_current(self, current_version: str) -> bool:
95
+ stored = await self._store.get(self._key, collection=self._collection)
96
+ return stored is not None and stored.get("version") == current_version
97
+
98
+ async def _mark(self, current_version: str) -> None:
99
+ await self._store.put(
100
+ self._key,
101
+ {"version": current_version},
102
+ collection=self._collection,
103
+ )
@@ -234,3 +234,27 @@ class OidcEnvironmentVariables(AbstractEnvironmentVariables):
234
234
  return timeout
235
235
  except ValueError:
236
236
  raise ValueError("WELL_KNOWN_CONFIG_HTTP_TIMEOUT_SECONDS must be a positive integer representing seconds")
237
+
238
+ @property
239
+ def jwt_clock_skew_leeway_seconds(self) -> int:
240
+ """Clock-skew tolerance in seconds for JWT iat/exp/nbf claim validation.
241
+
242
+ Accounts for clock drift between the token issuer and the process
243
+ verifying it (e.g. containers on different hosts, or a local dev VM's
244
+ clock drifting from its host) — applied as joserfc JWTClaimsRegistry's
245
+ `leeway`. Without this, any skew at all — even sub-second — makes
246
+ validate_iat reject a token as "issued in the future".
247
+
248
+ Set via JWT_CLOCK_SKEW_LEEWAY_SECONDS environment variable.
249
+ Defaults to 10 seconds.
250
+
251
+ Returns:
252
+ Leeway in seconds
253
+ """
254
+ try:
255
+ leeway = int(os.environ.get("JWT_CLOCK_SKEW_LEEWAY_SECONDS", "10"))
256
+ except ValueError:
257
+ raise ValueError("JWT_CLOCK_SKEW_LEEWAY_SECONDS must be a non-negative integer representing seconds")
258
+ if leeway < 0:
259
+ raise ValueError("JWT_CLOCK_SKEW_LEEWAY_SECONDS must be a non-negative integer representing seconds")
260
+ return leeway
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.17
3
+ Version: 3.0.19
4
4
  Summary: oidcauthlib
5
5
  Author-email: Imran Qureshi <imran.qureshi@bwell.com>
6
6
  License: Apache-2.0
@@ -55,6 +55,9 @@ oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py
55
55
  oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py
56
56
  oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py
57
57
  oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py
58
+ oidcauthlib/cache_gate/__init__.py
59
+ oidcauthlib/cache_gate/advisory_lock.py
60
+ oidcauthlib/cache_gate/version_gate.py
58
61
  oidcauthlib/container/__init__.py
59
62
  oidcauthlib/container/oidc_authlib_container_factory.py
60
63
  oidcauthlib/open_telemetry/__init__.py
@@ -1 +0,0 @@
1
- 3.0.17
File without changes
File without changes
File without changes
File without changes