From 37118e0871c42666054e61b2186059bc7146a609 Mon Sep 17 00:00:00 2001 From: Ejiro Asiuwhu Date: Thu, 26 Mar 2026 09:27:21 +0100 Subject: [PATCH] fix: skip aud/iss verification when not provided and narrow data vault error handling - PyJWT defaults verify_aud and verify_iss to True, which rejects tokens with iss/aud claims when no expected value is passed. Now explicitly disables these checks when audience/issuer are not provided. - Data vault get/delete now only suppress key-not-found errors (DATA_VAULT_STORE_HASHED_KEY_DOES_NOT_EXIST), letting store-not-found errors (DATA_VAULT_STORE_DOES_NOT_EXIST) propagate as they should. --- .../python/src/stack_auth/_jwt.py | 14 ++++++-- .../src/stack_auth/models/data_vault.py | 32 ++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/sdks/implementations/python/src/stack_auth/_jwt.py b/sdks/implementations/python/src/stack_auth/_jwt.py index 121d9703a..13adc0356 100644 --- a/sdks/implementations/python/src/stack_auth/_jwt.py +++ b/sdks/implementations/python/src/stack_auth/_jwt.py @@ -169,16 +169,21 @@ async def async_verify_token( key = await fetcher.get_signing_key(kid) kwargs: dict[str, Any] = {} + options: dict[str, bool] = {"verify_exp": True} if audience is not None: kwargs["audience"] = audience + else: + options["verify_aud"] = False if issuer is not None: kwargs["issuer"] = issuer + else: + options["verify_iss"] = False return jwt.decode( # type: ignore[no-any-return] token, key, algorithms=ALLOWED_ALGORITHMS, - options={"verify_exp": True}, + options=options, **kwargs, ) @@ -218,15 +223,20 @@ def sync_verify_token( key = fetcher.get_signing_key(kid) kwargs: dict[str, Any] = {} + options: dict[str, bool] = {"verify_exp": True} if audience is not None: kwargs["audience"] = audience + else: + options["verify_aud"] = False if issuer is not None: kwargs["issuer"] = issuer + else: + options["verify_iss"] = False return jwt.decode( # type: ignore[no-any-return] token, key, algorithms=ALLOWED_ALGORITHMS, - options={"verify_exp": True}, + options=options, **kwargs, ) diff --git a/sdks/implementations/python/src/stack_auth/models/data_vault.py b/sdks/implementations/python/src/stack_auth/models/data_vault.py index 11330791e..569161192 100644 --- a/sdks/implementations/python/src/stack_auth/models/data_vault.py +++ b/sdks/implementations/python/src/stack_auth/models/data_vault.py @@ -6,6 +6,14 @@ from typing import Any from stack_auth.errors import NotFoundError +# Only suppress key-not-found errors; store-not-found should propagate. +_KEY_NOT_FOUND_CODE = "DATA_VAULT_STORE_HASHED_KEY_DOES_NOT_EXIST" + + +def _is_key_not_found(err: NotFoundError) -> bool: + """Return True if the error is specifically a missing key, not a missing store.""" + return getattr(err, "code", None) == _KEY_NOT_FOUND_CODE + class DataVaultStore: """Synchronous data vault store -- a server-side key-value store. @@ -22,8 +30,10 @@ class DataVaultStore: """Get the value for a key, or ``None`` if not found.""" try: data = self._client.request("GET", f"{self._base_path}/{key}") - except NotFoundError: - return None + except NotFoundError as err: + if _is_key_not_found(err): + return None + raise if data is None: return None return data.get("value") if isinstance(data, dict) else data @@ -36,8 +46,10 @@ class DataVaultStore: """Delete a key-value pair. No error if key doesn't exist.""" try: self._client.request("DELETE", f"{self._base_path}/{key}") - except NotFoundError: - pass + except NotFoundError as err: + if _is_key_not_found(err): + return + raise def list_keys(self) -> list[str]: """Return all keys in the store.""" @@ -65,8 +77,10 @@ class AsyncDataVaultStore: """Get the value for a key, or ``None`` if not found.""" try: data = await self._client.request("GET", f"{self._base_path}/{key}") - except NotFoundError: - return None + except NotFoundError as err: + if _is_key_not_found(err): + return None + raise if data is None: return None return data.get("value") if isinstance(data, dict) else data @@ -79,8 +93,10 @@ class AsyncDataVaultStore: """Delete a key-value pair. No error if key doesn't exist.""" try: await self._client.request("DELETE", f"{self._base_path}/{key}") - except NotFoundError: - pass + except NotFoundError as err: + if _is_key_not_found(err): + return + raise async def list_keys(self) -> list[str]: """Return all keys in the store."""