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.
This commit is contained in:
Ejiro Asiuwhu
2026-03-26 09:27:21 +01:00
parent 5cbad57027
commit 37118e0871
2 changed files with 36 additions and 10 deletions
@@ -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,
)
@@ -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."""