oidcauthlib 1.0.8__py3-none-any.whl

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 (95) hide show
  1. oidcauthlib/__init__.py +0 -0
  2. oidcauthlib/auth/__init__.py +0 -0
  3. oidcauthlib/auth/auth_helper.py +66 -0
  4. oidcauthlib/auth/auth_manager.py +348 -0
  5. oidcauthlib/auth/cache/__init__.py +0 -0
  6. oidcauthlib/auth/cache/oauth_cache.py +47 -0
  7. oidcauthlib/auth/cache/oauth_memory_cache.py +53 -0
  8. oidcauthlib/auth/cache/oauth_mongo_cache.py +186 -0
  9. oidcauthlib/auth/config/__init__.py +0 -0
  10. oidcauthlib/auth/config/auth_config.py +44 -0
  11. oidcauthlib/auth/config/auth_config_reader.py +290 -0
  12. oidcauthlib/auth/exceptions/__init__.py +0 -0
  13. oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +39 -0
  14. oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +23 -0
  15. oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +22 -0
  16. oidcauthlib/auth/exceptions/authorization_needed_exception.py +17 -0
  17. oidcauthlib/auth/fastapi_auth_manager.py +267 -0
  18. oidcauthlib/auth/middleware/__init__.py +0 -0
  19. oidcauthlib/auth/middleware/request_scope_middleware.py +84 -0
  20. oidcauthlib/auth/middleware/token_reader_middleware.py +103 -0
  21. oidcauthlib/auth/models/__init__.py +0 -0
  22. oidcauthlib/auth/models/auth.py +40 -0
  23. oidcauthlib/auth/models/base_db_model.py +27 -0
  24. oidcauthlib/auth/models/cache_item.py +29 -0
  25. oidcauthlib/auth/models/client_key_set.py +21 -0
  26. oidcauthlib/auth/models/token.py +214 -0
  27. oidcauthlib/auth/repository/__init__.py +0 -0
  28. oidcauthlib/auth/repository/base_repository.py +94 -0
  29. oidcauthlib/auth/repository/memory/__init__.py +0 -0
  30. oidcauthlib/auth/repository/memory/memory_repository.py +156 -0
  31. oidcauthlib/auth/repository/mongo/__init__.py +0 -0
  32. oidcauthlib/auth/repository/mongo/mongo_repository.py +343 -0
  33. oidcauthlib/auth/repository/repository_factory.py +50 -0
  34. oidcauthlib/auth/routers/__init__.py +0 -0
  35. oidcauthlib/auth/routers/auth_router.py +225 -0
  36. oidcauthlib/auth/token_reader.py +421 -0
  37. oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
  38. oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +237 -0
  39. oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +154 -0
  40. oidcauthlib/container/__init__.py +0 -0
  41. oidcauthlib/container/container_registry.py +62 -0
  42. oidcauthlib/container/inject.py +51 -0
  43. oidcauthlib/container/interfaces.py +39 -0
  44. oidcauthlib/container/oidc_authlib_container_factory.py +87 -0
  45. oidcauthlib/container/simple_container.py +488 -0
  46. oidcauthlib/py.typed +0 -0
  47. oidcauthlib/utilities/__init__.py +0 -0
  48. oidcauthlib/utilities/cached.py +25 -0
  49. oidcauthlib/utilities/environment/__init__.py +0 -0
  50. oidcauthlib/utilities/environment/abstract_environment_variables.py +63 -0
  51. oidcauthlib/utilities/environment/environment_variables.py +71 -0
  52. oidcauthlib/utilities/logger/__init__.py +0 -0
  53. oidcauthlib/utilities/logger/log_levels.py +43 -0
  54. oidcauthlib/utilities/logger/logging_response.py +38 -0
  55. oidcauthlib/utilities/logger/logging_transport.py +58 -0
  56. oidcauthlib/utilities/mongo_url_utils.py +76 -0
  57. oidcauthlib-1.0.8.dist-info/METADATA +33 -0
  58. oidcauthlib-1.0.8.dist-info/RECORD +95 -0
  59. oidcauthlib-1.0.8.dist-info/WHEEL +5 -0
  60. oidcauthlib-1.0.8.dist-info/licenses/LICENSE +201 -0
  61. oidcauthlib-1.0.8.dist-info/top_level.txt +2 -0
  62. tests/__init__.py +0 -0
  63. tests/auth/__init__.py +0 -0
  64. tests/auth/cache/__init__.py +0 -0
  65. tests/auth/cache/test_oauth_memory_cache.py +26 -0
  66. tests/auth/config/__init__.py +0 -0
  67. tests/auth/config/test_auth_config.py +32 -0
  68. tests/auth/config/test_auth_config_reader.py +89 -0
  69. tests/auth/config/test_auth_config_reader_thread_safety.py +125 -0
  70. tests/auth/conftest.py +70 -0
  71. tests/auth/middleware/__init__.py +0 -0
  72. tests/auth/middleware/test_request_scope_middleware.py +181 -0
  73. tests/auth/middleware/test_token_reader_middleware.py +671 -0
  74. tests/auth/models/__init__.py +0 -0
  75. tests/auth/models/test_auth.py +43 -0
  76. tests/auth/models/test_base_db_model.py +68 -0
  77. tests/auth/models/test_cache_item.py +17 -0
  78. tests/auth/models/test_token.py +48 -0
  79. tests/auth/repository/__init__.py +0 -0
  80. tests/auth/repository/memory/__init__.py +0 -0
  81. tests/auth/repository/memory/test_memory_repository.py +25 -0
  82. tests/auth/repository/mongo/__init__.py +0 -0
  83. tests/auth/repository/mongo/test_mongo_repository.py +908 -0
  84. tests/auth/test_auth_helper.py +26 -0
  85. tests/auth/test_auth_helper2.py +33 -0
  86. tests/auth/well_known_configuration/__init__.py +0 -0
  87. tests/auth/well_known_configuration/test_well_known_configuration_cache.py +232 -0
  88. tests/auth/well_known_configuration/test_well_known_configuration_manager_deadlock.py +355 -0
  89. tests/container/__init__.py +0 -0
  90. tests/container/test_simple_container.py +147 -0
  91. tests/test_mongo_url_utils.py +36 -0
  92. tests/test_simple.py +2 -0
  93. tests/utilities/__init__.py +0 -0
  94. tests/utilities/test_cached.py +52 -0
  95. tests/utilities/test_mongo_url_utils.py +30 -0
File without changes
File without changes
@@ -0,0 +1,66 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+
5
+ from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
6
+
7
+ logger = logging.getLogger(__name__)
8
+ logger.setLevel(SRC_LOG_LEVELS["AUTH"])
9
+
10
+
11
+ class AuthHelper:
12
+ @staticmethod
13
+ def encode_state(content: dict[str, str | None]) -> str:
14
+ """
15
+ Encode the state content into a base64url encoded string.
16
+
17
+ Args:
18
+ content: The content to encode, typically a dictionary.
19
+
20
+ Returns:
21
+ A base64url encoded string of the content.
22
+ """
23
+ json_content = json.dumps(content)
24
+ encoded_content = base64.urlsafe_b64encode(json_content.encode("utf-8")).decode(
25
+ "utf-8"
26
+ )
27
+ return encoded_content.rstrip("=")
28
+
29
+ @staticmethod
30
+ def decode_state(encoded_content: str) -> dict[str, str | None]:
31
+ """
32
+ Decode a base64url encoded string back into its original dictionary form.
33
+
34
+ Args:
35
+ encoded_content: The base64url encoded string to decode.
36
+
37
+ Returns:
38
+ The decoded content as a dictionary.
39
+ """
40
+ # Add padding if necessary
41
+ try:
42
+ if not encoded_content or not isinstance(encoded_content, str):
43
+ logger.error("Failed to decode state: Input is empty or not a string")
44
+ raise ValueError("Encoded state is empty or not a string")
45
+ # Fix base64 padding
46
+ padding_needed = (-len(encoded_content)) % 4
47
+ padded_content = encoded_content + ("=" * padding_needed)
48
+ try:
49
+ json_content = base64.urlsafe_b64decode(padded_content).decode("utf-8")
50
+ except Exception as e:
51
+ logger.error(f"Failed to decode state (base64 error): {e}")
52
+ raise ValueError("Invalid base64 encoding in state") from e
53
+ try:
54
+ result = json.loads(json_content)
55
+ except Exception as e:
56
+ logger.error(f"Failed to decode state (JSON error): {e}")
57
+ raise ValueError("Invalid JSON in decoded state") from e
58
+ if not isinstance(result, dict):
59
+ logger.error(
60
+ "Failed to decode state: Decoded state is not a dictionary"
61
+ )
62
+ raise ValueError("Decoded state is not a dictionary")
63
+ return result
64
+ except Exception:
65
+ # Already logged specific error above
66
+ raise
@@ -0,0 +1,348 @@
1
+ import logging
2
+ import os
3
+ import time
4
+ import uuid
5
+ from typing import Any, Dict, cast, List
6
+
7
+ import httpx
8
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
9
+ from authlib.integrations.starlette_client import OAuth, StarletteOAuth2App
10
+
11
+ from oidcauthlib.auth.auth_helper import AuthHelper
12
+ from oidcauthlib.auth.cache.oauth_cache import OAuthCache
13
+ from oidcauthlib.auth.cache.oauth_memory_cache import (
14
+ OAuthMemoryCache,
15
+ )
16
+ from oidcauthlib.auth.cache.oauth_mongo_cache import OAuthMongoCache
17
+ from oidcauthlib.auth.config.auth_config import AuthConfig
18
+ from oidcauthlib.auth.config.auth_config_reader import (
19
+ AuthConfigReader,
20
+ )
21
+ from oidcauthlib.auth.exceptions.authorization_needed_exception import (
22
+ AuthorizationNeededException,
23
+ )
24
+ from oidcauthlib.auth.token_reader import TokenReader
25
+ from oidcauthlib.auth.well_known_configuration.well_known_configuration_manager import (
26
+ WellKnownConfigurationManager,
27
+ )
28
+ from oidcauthlib.utilities.environment.abstract_environment_variables import (
29
+ AbstractEnvironmentVariables,
30
+ )
31
+ from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
32
+ from oidcauthlib.utilities.logger.logging_transport import (
33
+ LoggingTransport,
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+ logger.setLevel(SRC_LOG_LEVELS["AUTH"])
38
+
39
+
40
+ class AuthManager:
41
+ """
42
+ AuthManager is responsible for managing authentication using OIDC PKCE.
43
+
44
+ It initializes the OAuth client with the necessary configuration and provides methods
45
+ to create authorization URLs and handle callback responses.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ *,
51
+ environment_variables: AbstractEnvironmentVariables,
52
+ auth_config_reader: AuthConfigReader,
53
+ token_reader: TokenReader,
54
+ well_known_configuration_manager: WellKnownConfigurationManager,
55
+ ) -> None:
56
+ """
57
+ 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
+
68
+ 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.
72
+ """
73
+ self.environment_variables: AbstractEnvironmentVariables = environment_variables
74
+ if self.environment_variables is None:
75
+ raise ValueError("environment_variables must not be None")
76
+ if not isinstance(self.environment_variables, AbstractEnvironmentVariables):
77
+ raise TypeError(
78
+ "environment_variables must be an instance of EnvironmentVariables"
79
+ )
80
+
81
+ self.auth_config_reader: AuthConfigReader = auth_config_reader
82
+ if self.auth_config_reader is None:
83
+ raise ValueError("auth_config_reader must not be None")
84
+ if not isinstance(self.auth_config_reader, AuthConfigReader):
85
+ raise TypeError(
86
+ "auth_config_reader must be an instance of AuthConfigReader"
87
+ )
88
+
89
+ self.token_reader: TokenReader = token_reader
90
+ if self.token_reader is None:
91
+ raise ValueError("token_reader must not be None")
92
+ if not isinstance(self.token_reader, TokenReader):
93
+ raise TypeError("token_reader must be an instance of TokenReader")
94
+
95
+ self.well_known_configuration_manager: WellKnownConfigurationManager = (
96
+ well_known_configuration_manager
97
+ )
98
+ if self.well_known_configuration_manager is None:
99
+ raise ValueError("well_known_configuration_manager must not be None")
100
+ if not isinstance(
101
+ self.well_known_configuration_manager, WellKnownConfigurationManager
102
+ ):
103
+ raise TypeError(
104
+ "well_known_configuration_manager must be an instance of WellKnownConfigurationManager"
105
+ )
106
+ oauth_cache_type = environment_variables.oauth_cache
107
+ self.cache: OAuthCache = (
108
+ OAuthMemoryCache()
109
+ if oauth_cache_type == "memory"
110
+ else OAuthMongoCache(environment_variables=environment_variables)
111
+ )
112
+
113
+ logger.debug(
114
+ f"Initializing AuthManager with cache type {type(self.cache)} cache id: {self.cache.id}"
115
+ )
116
+ # OIDC PKCE setup
117
+ self.redirect_uri = os.getenv("AUTH_REDIRECT_URI")
118
+ if self.redirect_uri is None:
119
+ raise ValueError("AUTH_REDIRECT_URI environment variable must be set")
120
+ # https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients
121
+ self._oauth: OAuth = OAuth(cache=self.cache) # type: ignore[no-untyped-call]
122
+ # read AUTH_PROVIDERS comma separated list from the environment variable and register the OIDC provider for each provider
123
+ self.auth_configs: List[AuthConfig] = (
124
+ self.auth_config_reader.get_auth_configs_for_all_auth_providers()
125
+ )
126
+
127
+ async def ensure_initialized_async(self) -> None:
128
+ auth_config: AuthConfig
129
+ for auth_config in self.auth_configs:
130
+ server_metadata: (
131
+ dict[str, Any] | None
132
+ ) = await self.well_known_configuration_manager.get_async(
133
+ auth_config=auth_config
134
+ )
135
+ logger.debug(
136
+ f"Registering OAuth client for auth provider {auth_config.auth_provider}"
137
+ + (
138
+ f" with well-known configuration: {server_metadata}"
139
+ if server_metadata is not None
140
+ else f" from {auth_config.well_known_uri}"
141
+ )
142
+ )
143
+ self._oauth.register(
144
+ name=auth_config.auth_provider.lower(),
145
+ client_id=auth_config.client_id,
146
+ client_secret=auth_config.client_secret,
147
+ server_metadata_url=auth_config.well_known_uri,
148
+ # server_metadata_url=auth_config.well_known_uri
149
+ # if server_metadata is None
150
+ # else None,
151
+ # server_metadata=server_metadata,
152
+ client_kwargs={
153
+ "scope": auth_config.scope,
154
+ "code_challenge_method": "S256",
155
+ "transport": LoggingTransport(httpx.AsyncHTTPTransport()),
156
+ },
157
+ )
158
+
159
+ async def create_authorization_url(
160
+ self,
161
+ *,
162
+ auth_provider: str,
163
+ redirect_uri: str,
164
+ audience: str,
165
+ url: str | None,
166
+ referring_email: str | None,
167
+ referring_subject: str | None,
168
+ ) -> str:
169
+ """
170
+ Create the authorization URL for the OIDC provider.
171
+
172
+ This method generates the authorization URL with the necessary parameters,
173
+ including the redirect URI and state. The state is encoded to include the tool name,
174
+ which is used to identify the tool that initiated the authentication process.
175
+ Args:
176
+ auth_provider (str): The name of the OIDC provider.
177
+ redirect_uri (str): The redirect URI to which the OIDC provider will send the user
178
+ after authentication.
179
+ audience (str): The audience we need to get a token for.
180
+ url (str): The URL of the tool that has requested this.
181
+ referring_email (str): The email of the user who initiated the request.
182
+ referring_subject (str): The subject of the user who initiated the request.
183
+ Returns:
184
+ str: The authorization URL to redirect the user to for authentication.
185
+ """
186
+ # default to first audience
187
+ client: StarletteOAuth2App = await self.create_oauth_client(name=auth_provider)
188
+ if client is None:
189
+ raise ValueError(f"Client for audience {audience} not found")
190
+ state_content: Dict[str, str | None] = {
191
+ "auth_provider": auth_provider,
192
+ "referring_email": referring_email,
193
+ "referring_subject": referring_subject,
194
+ "url": url, # the URL of the tool that has requested this
195
+ # include a unique request ID so we don't get cache for another request
196
+ # This will create a unique state for each request
197
+ # the callback will use this state to find the correct token
198
+ "request_id": uuid.uuid4().hex,
199
+ }
200
+ # convert state_content to a string
201
+ state: str = AuthHelper.encode_state(state_content)
202
+
203
+ logger.debug(
204
+ f"Creating authorization URL for audience {audience} with state {state_content} and encoded state {state}"
205
+ )
206
+
207
+ rv: Dict[str, Any] = await client.create_authorization_url( # type: ignore[no-untyped-call]
208
+ redirect_uri=redirect_uri, state=state
209
+ )
210
+ logger.debug(f"Authorization URL created: {rv}")
211
+ # request is only needed if we are using the session to store the state
212
+ await client.save_authorize_data(request=None, redirect_uri=redirect_uri, **rv) # type: ignore[no-untyped-call]
213
+ return cast(str, rv["url"])
214
+
215
+ async def create_oauth_client(self, *, name: str) -> StarletteOAuth2App:
216
+ if not name:
217
+ raise ValueError("name must not be empty")
218
+ await self.ensure_initialized_async()
219
+ return cast(StarletteOAuth2App, self._oauth.create_client(name=name.lower())) # type: ignore[no-untyped-call]
220
+
221
+ @staticmethod
222
+ async def login_and_get_token_with_username_password_async(
223
+ *,
224
+ auth_config: AuthConfig,
225
+ username: str,
226
+ password: str,
227
+ audience: str | None = None,
228
+ token_name: str = "access_token",
229
+ ) -> str:
230
+ """
231
+ Logs in a user with the provided username and password, and retrieves an access token.
232
+
233
+ Args:
234
+ auth_config (AuthConfig): The authentication configuration.
235
+ username (str): The username of the user.
236
+ password (str): The password of the user.
237
+ audience (str | None): The intended audience for the token. Optional.
238
+ token_name (str): The name of the token to retrieve. Defaults to "access_token".
239
+
240
+ Returns:
241
+ str: The access token if login is successful.
242
+
243
+ Raises:
244
+ Exception: If login fails or token retrieval is unsuccessful.
245
+ """
246
+
247
+ # Discover token endpoint
248
+ token_url = None
249
+ if auth_config.well_known_uri:
250
+ try:
251
+ async with httpx.AsyncClient(timeout=5) as async_client:
252
+ resp = await async_client.get(auth_config.well_known_uri)
253
+ resp.raise_for_status()
254
+ token_url = resp.json().get("token_endpoint")
255
+ except Exception as e:
256
+ raise AuthorizationNeededException(
257
+ message=f"Failed to discover token endpoint: {e}"
258
+ )
259
+ if not token_url and auth_config.issuer:
260
+ token_url = (
261
+ auth_config.issuer.rstrip("/") + "/protocol/openid-connect/token"
262
+ )
263
+ if not token_url:
264
+ raise AuthorizationNeededException(
265
+ message="No token endpoint found in AuthConfig."
266
+ )
267
+
268
+ # Prepare OAuth2 client
269
+ client_id = auth_config.client_id
270
+ client_secret = auth_config.client_secret
271
+ audience = audience or auth_config.audience
272
+ client = AsyncOAuth2Client(client_id, client_secret, timeout=10)
273
+
274
+ # Request token
275
+ try:
276
+ # This DOES return a coroutine
277
+ # noinspection PyUnresolvedReferences
278
+ token: Dict[str, Any] = await client.fetch_token(
279
+ url=token_url,
280
+ grant_type="password",
281
+ username=username,
282
+ password=password,
283
+ scope="openid",
284
+ audience=audience,
285
+ )
286
+ if not isinstance(token, dict):
287
+ raise TypeError(f"Expected token to be a dict, got {type(token)}")
288
+
289
+ except Exception as e:
290
+ raise AuthorizationNeededException(message=f"Token request failed: {e}")
291
+
292
+ access_token: str | None = token.get(token_name)
293
+ if not access_token:
294
+ raise AuthorizationNeededException(message="No access token returned.")
295
+
296
+ return access_token
297
+
298
+ def get_auth_config_for_auth_provider(
299
+ self, *, auth_provider: str
300
+ ) -> AuthConfig | None:
301
+ if not auth_provider:
302
+ raise ValueError("auth_provider must not be empty")
303
+ for auth_config in self.auth_configs:
304
+ if auth_config.auth_provider.lower() == auth_provider.lower():
305
+ return auth_config
306
+ return None
307
+
308
+ @staticmethod
309
+ def wait_till_well_known_configuration_available(
310
+ *, auth_config: AuthConfig, timeout_seconds: int = 30
311
+ ) -> None:
312
+ """
313
+ Wait until the well-known configuration is available for the given AuthConfig.
314
+
315
+ This method repeatedly attempts to fetch the well-known configuration from the
316
+ specified URL until it succeeds or the timeout is reached.
317
+
318
+ Args:
319
+ auth_config (AuthConfig): The authentication configuration containing the
320
+ well-known URL.
321
+ timeout_seconds (int): The maximum time to wait in seconds. Defaults to 30 seconds.
322
+ Raises:
323
+ TimeoutError: If the well-known configuration is not available within the timeout period.
324
+ """
325
+ if not auth_config.well_known_uri:
326
+ raise ValueError("AuthConfig must have a well-known URI to wait for.")
327
+
328
+ start_time = time.time()
329
+ while True:
330
+ try:
331
+ with httpx.Client(timeout=5) as client:
332
+ resp = client.get(auth_config.well_known_uri)
333
+ resp.raise_for_status()
334
+ # Successfully fetched the configuration
335
+ logger.info(
336
+ f"Well-known configuration is now available at {auth_config.well_known_uri}"
337
+ )
338
+ return
339
+ except Exception as e:
340
+ elapsed_time = time.time() - start_time
341
+ if elapsed_time >= timeout_seconds:
342
+ raise TimeoutError(
343
+ f"Timed out waiting for well-known configuration at {auth_config.well_known_uri}"
344
+ ) from e
345
+ logger.debug(
346
+ f"Well-known configuration not yet available, retrying... ({elapsed_time:.1f}s elapsed)"
347
+ )
348
+ time.sleep(2) # Wait before retrying
File without changes
@@ -0,0 +1,47 @@
1
+ import uuid
2
+ from abc import abstractmethod, ABCMeta
3
+
4
+
5
+ class OAuthCache(metaclass=ABCMeta):
6
+ """
7
+ Base class for OAuthCache
8
+ """
9
+
10
+ @property
11
+ @abstractmethod
12
+ def id(self) -> uuid.UUID:
13
+ """
14
+ Unique identifier for the cache instance.
15
+ """
16
+ ...
17
+
18
+ @abstractmethod
19
+ async def delete(self, key: str) -> None:
20
+ """
21
+ Delete a cache entry.
22
+
23
+ :param key: Unique identifier for the cache entry.
24
+ """
25
+ ...
26
+
27
+ @abstractmethod
28
+ async def get(self, key: str, default: str | None = None) -> str | None:
29
+ """
30
+ Retrieve a value from the cache.
31
+
32
+ :param key: Unique identifier for the cache entry.
33
+ :param default: Default value to return if the key is not found.
34
+ :return: Retrieved value or None if not found or expired.
35
+ """
36
+ ...
37
+
38
+ @abstractmethod
39
+ async def set(self, key: str, value: str, expires: int | None = None) -> None:
40
+ """
41
+ Set a value in the cache with optional expiration.
42
+
43
+ :param key: Unique identifier for the cache entry.
44
+ :param value: Value to be stored.
45
+ :param expires: Expiration time in seconds. Defaults to None (no expiration).
46
+ """
47
+ ...
@@ -0,0 +1,53 @@
1
+ import uuid
2
+ from typing import override
3
+
4
+ from oidcauthlib.auth.cache.oauth_cache import OAuthCache
5
+
6
+
7
+ class OAuthMemoryCache(OAuthCache):
8
+ """
9
+ In-memory implementation of OAuth cache
10
+ """
11
+
12
+ @property
13
+ @override
14
+ def id(self) -> uuid.UUID:
15
+ return self.id_
16
+
17
+ _cache: dict[str, str] = {}
18
+
19
+ def __init__(self) -> None:
20
+ """Initialize the AuthCache."""
21
+ self.id_ = uuid.uuid4()
22
+
23
+ @override
24
+ async def delete(self, key: str) -> None:
25
+ """
26
+ Delete a cache entry.
27
+
28
+ :param key: Unique identifier for the cache entry.
29
+ """
30
+ if key in self._cache:
31
+ del self._cache[key]
32
+
33
+ @override
34
+ async def get(self, key: str, default: str | None = None) -> str | None:
35
+ """
36
+ Retrieve a value from the cache.
37
+
38
+ :param key: Unique identifier for the cache entry.
39
+ :param default: Default value to return if the key is not found.
40
+ :return: Retrieved value or None if not found or expired.
41
+ """
42
+ return self._cache.get(key) or default
43
+
44
+ @override
45
+ async def set(self, key: str, value: str, expires: int | None = None) -> None:
46
+ """
47
+ Set a value in the cache with optional expiration.
48
+
49
+ :param key: Unique identifier for the cache entry.
50
+ :param value: Value to be stored.
51
+ :param expires: Expiration time in seconds. Defaults to None (no expiration).
52
+ """
53
+ self._cache[key] = value