From e96ceef1a0716285b565dd75a6fa2ea0a3cd9379 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 19 Jul 2026 09:26:56 -0700 Subject: [PATCH 1/3] fix(keycardai-oauth): migrate JOSE handling from authlib to joserfc authlib.jose is deprecated and emits an AuthlibDeprecationWarning on import, surfacing to every SDK user. Migrate JWT signing/verification and JWK handling to joserfc (authlib 's recommended replacement, already in the dependency tree). - decode_and_verify_jwt: import key via joserfc, decode with explicit algorithms, return .claims - get_jwks_key: return import_key(jwk).as_pem() - create_client_assertion / key export: joserfc encode + import_key - derive key type from the JWS algorithm so PEM imports do not emit joserfc's implicit-key SecurityWarning - drop the direct authlib dependency (remains transitively via fastmcp) --- packages/oauth/pyproject.toml | 2 +- .../src/keycardai/oauth/server/__init__.py | 2 +- .../src/keycardai/oauth/server/private_key.py | 12 ++-- .../oauth/src/keycardai/oauth/utils/jwt.py | 35 +++++++++--- .../tests/keycardai/oauth/utils/test_jwt.py | 55 +++++++++---------- uv.lock | 34 ++++++------ 6 files changed, 77 insertions(+), 63 deletions(-) diff --git a/packages/oauth/pyproject.toml b/packages/oauth/pyproject.toml index 81be9727..8b57c67f 100644 --- a/packages/oauth/pyproject.toml +++ b/packages/oauth/pyproject.toml @@ -9,8 +9,8 @@ authors = [{ name = "Keycard", email = "support@keycard.ai" }] dependencies = [ "pydantic>=2.11.7", "httpx>=0.28.1", - "authlib>=1.6.3", "cryptography>=45.0.7", + "joserfc>=1.6.4", ] keywords = ["oauth", "oauth2", "authentication", "tokens", "security"] diff --git a/packages/oauth/src/keycardai/oauth/server/__init__.py b/packages/oauth/src/keycardai/oauth/server/__init__.py index 9a9c0c78..7434a3fa 100644 --- a/packages/oauth/src/keycardai/oauth/server/__init__.py +++ b/packages/oauth/src/keycardai/oauth/server/__init__.py @@ -1,7 +1,7 @@ """Keycard OAuth Server Primitives. Framework-free server components for protecting any HTTP API with Keycard. -These components depend only on pydantic, httpx, authlib, and cryptography — +These components depend only on pydantic, httpx, joserfc, and cryptography — no MCP, Starlette, or other framework dependencies. Core Components: diff --git a/packages/oauth/src/keycardai/oauth/server/private_key.py b/packages/oauth/src/keycardai/oauth/server/private_key.py index 8bf7d9ff..e4b1ef70 100644 --- a/packages/oauth/src/keycardai/oauth/server/private_key.py +++ b/packages/oauth/src/keycardai/oauth/server/private_key.py @@ -22,10 +22,11 @@ from pathlib import Path from typing import Any, Protocol -from authlib.jose import JsonWebKey, JsonWebToken from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import PublicFormat +from joserfc import jwt as jose_jwt +from joserfc.jwk import import_key from pydantic import AnyHttpUrl, BaseModel from keycardai.oauth.types.models import ( @@ -209,7 +210,7 @@ def _generate_and_store_key_pair(self) -> None: format=PublicFormat.SubjectPublicKeyInfo, ) - jwk = JsonWebKey.import_key(public_key_pem) + jwk = import_key(public_key_pem, "RSA") public_key_jwk = jwk.as_dict() public_key_jwk["kid"] = self.key_id @@ -289,12 +290,9 @@ def create_client_assertion( header = {"alg": "RS256", "typ": "JWT", "kid": self.key_id} - jwt = JsonWebToken(["RS256"]) - private_key = serialization.load_pem_private_key( - self._private_key_pem.encode("utf-8"), password=None - ) + private_key = import_key(self._private_key_pem, "RSA") - return jwt.encode(header, payload, private_key) + return jose_jwt.encode(header, payload, private_key) def get_client_id(self) -> str: return self.key_id diff --git a/packages/oauth/src/keycardai/oauth/utils/jwt.py b/packages/oauth/src/keycardai/oauth/utils/jwt.py index 2acb3376..249dfe3b 100644 --- a/packages/oauth/src/keycardai/oauth/utils/jwt.py +++ b/packages/oauth/src/keycardai/oauth/utils/jwt.py @@ -34,7 +34,8 @@ import json from typing import Any -from authlib.jose import JsonWebKey, JsonWebToken +from joserfc import jwt as jose_jwt +from joserfc.jwk import import_key from pydantic import BaseModel from ..exceptions import JWKSError, JWKSFetchError, JWKSKeyNotFoundError @@ -42,6 +43,22 @@ from ..http._wire import HttpRequest from ..types.models import ClientConfig +# joserfc requires an explicit key type when importing a PEM/DER key, otherwise +# it emits a SecurityWarning about implicit key types. Derive the key type from +# the JWS algorithm so PEM imports stay quiet and unambiguous. Importing from a +# JWK dict does not need this since the dict carries its own "kty". +_ALG_KEY_TYPE = { + "RS": "RSA", + "PS": "RSA", + "ES": "EC", + "Ed": "OKP", + "HS": "oct", +} + + +def _key_type_for_algorithm(algorithm: str) -> str: + return _ALG_KEY_TYPE.get(algorithm[:2], "RSA") + def build_substitute_user_token(identifier: str) -> str: """Build an unsigned JWT for user impersonation via token exchange. @@ -398,7 +415,7 @@ def get_scopes(self) -> list[str]: def decode_and_verify_jwt( jwt_token: str, verification_key: str, algorithm: str = "RS256" ) -> dict: - """Decode and verify JWT token signature using authlib. + """Decode and verify JWT token signature using joserfc. Args: jwt_token: JWT token string (without Bearer prefix) @@ -412,9 +429,9 @@ def decode_and_verify_jwt( ValueError: If token is invalid, malformed, or signature verification fails """ try: - jwt = JsonWebToken([algorithm]) - claims = jwt.decode(jwt_token, verification_key) - return claims + key = import_key(verification_key, _key_type_for_algorithm(algorithm)) + token = jose_jwt.decode(jwt_token, key, algorithms=[algorithm]) + return token.claims except Exception as e: raise ValueError(f"JWT verification failed: {e}") from e @@ -554,13 +571,13 @@ async def get_jwks_key( if kid: for key_data in keys: if key_data.get("kid") == kid: - jwk = JsonWebKey.import_key(key_data) - return jwk.get_public_key() # type: ignore + jwk = import_key(key_data) + return jwk.as_pem().decode("utf-8") raise JWKSKeyNotFoundError(f"Key ID '{kid}' not found") else: if len(keys) == 1: - jwk = JsonWebKey.import_key(keys[0]) - return jwk.get_public_key() # type: ignore + jwk = import_key(keys[0]) + return jwk.as_pem().decode("utf-8") elif len(keys) > 1: raise JWKSKeyNotFoundError("Multiple keys in JWKS but no key ID (kid) in token") else: diff --git a/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py b/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py index 593975d3..e869debd 100644 --- a/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py +++ b/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py @@ -6,9 +6,10 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from authlib.jose import JsonWebKey, JsonWebSignature from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa +from joserfc import jwt as jose_jwt +from joserfc.jwk import import_key from keycardai.oauth.exceptions import JWKSFetchError, JWKSKeyNotFoundError from keycardai.oauth.utils.jwt import ( @@ -321,25 +322,27 @@ def test_get_all_claims_optional_fields(self): class TestJWTVerification: """Test JWT verification functionality.""" - @patch("keycardai.oauth.utils.jwt.JsonWebToken") - def test_decode_and_verify_jwt_success(self, mock_jwt_class): + @patch("keycardai.oauth.utils.jwt.import_key") + @patch("keycardai.oauth.utils.jwt.jose_jwt") + def test_decode_and_verify_jwt_success(self, mock_jwt, mock_import_key): """Test successful JWT verification.""" - mock_jwt = Mock() - mock_jwt.decode.return_value = {"sub": "user123", "iss": "example.com"} - mock_jwt_class.return_value = mock_jwt + mock_token = Mock() + mock_token.claims = {"sub": "user123", "iss": "example.com"} + mock_jwt.decode.return_value = mock_token result = decode_and_verify_jwt("token", "key", "RS256") assert result == {"sub": "user123", "iss": "example.com"} - mock_jwt_class.assert_called_once_with(["RS256"]) - mock_jwt.decode.assert_called_once_with("token", "key") + mock_import_key.assert_called_once_with("key", "RSA") + mock_jwt.decode.assert_called_once_with( + "token", mock_import_key.return_value, algorithms=["RS256"] + ) - @patch("keycardai.oauth.utils.jwt.JsonWebToken") - def test_decode_and_verify_jwt_failure(self, mock_jwt_class): + @patch("keycardai.oauth.utils.jwt.import_key") + @patch("keycardai.oauth.utils.jwt.jose_jwt") + def test_decode_and_verify_jwt_failure(self, mock_jwt, mock_import_key): """Test JWT verification failure.""" - mock_jwt = Mock() mock_jwt.decode.side_effect = Exception("Invalid signature") - mock_jwt_class.return_value = mock_jwt with pytest.raises(ValueError, match="JWT verification failed"): decode_and_verify_jwt("token", "key", "RS256") @@ -433,9 +436,9 @@ async def test_get_verification_key_failure(self, mock_get_header): @pytest.mark.asyncio @patch("keycardai.oauth.utils.jwt.HttpxAsyncTransport") @patch("keycardai.oauth.utils.jwt.ClientConfig") - @patch("keycardai.oauth.utils.jwt.JsonWebKey") + @patch("keycardai.oauth.utils.jwt.import_key") async def test_get_jwks_key_with_kid( - self, mock_jwk_class, mock_config_class, mock_transport_class + self, mock_import_key, mock_config_class, mock_transport_class ): """Test JWKS key fetching with specific key ID.""" # Mock response @@ -457,13 +460,13 @@ async def test_get_jwks_key_with_kid( # Mock JWK mock_jwk = Mock() - mock_jwk.get_public_key.return_value = "public_key_pem" - mock_jwk_class.import_key.return_value = mock_jwk + mock_jwk.as_pem.return_value = b"public_key_pem" + mock_import_key.return_value = mock_jwk key = await get_jwks_key("key1", "https://example.com/.well-known/jwks.json") assert key == "public_key_pem" - mock_jwk_class.import_key.assert_called_once_with( + mock_import_key.assert_called_once_with( {"kid": "key1", "kty": "RSA", "use": "sig"} ) @@ -507,9 +510,9 @@ async def test_get_jwks_key_http_error( @pytest.mark.asyncio @patch("keycardai.oauth.utils.jwt.HttpxAsyncTransport") @patch("keycardai.oauth.utils.jwt.ClientConfig") - @patch("keycardai.oauth.utils.jwt.JsonWebKey") + @patch("keycardai.oauth.utils.jwt.import_key") async def test_get_jwks_key_single_key_no_kid( - self, mock_jwk_class, mock_config_class, mock_transport_class + self, mock_import_key, mock_config_class, mock_transport_class ): """Test JWKS key fetching with single key and no kid parameter.""" mock_response = Mock() @@ -523,13 +526,13 @@ async def test_get_jwks_key_single_key_no_kid( mock_transport_class.return_value = mock_transport mock_jwk = Mock() - mock_jwk.get_public_key.return_value = "public_key_pem" - mock_jwk_class.import_key.return_value = mock_jwk + mock_jwk.as_pem.return_value = b"public_key_pem" + mock_import_key.return_value = mock_jwk key = await get_jwks_key(None, "https://example.com/.well-known/jwks.json") assert key == "public_key_pem" - mock_jwk_class.import_key.assert_called_once_with({"kty": "RSA", "use": "sig"}) + mock_import_key.assert_called_once_with({"kty": "RSA", "use": "sig"}) @pytest.mark.asyncio @patch("keycardai.oauth.utils.jwt.HttpxAsyncTransport") @@ -623,12 +626,8 @@ def create_token(claims: dict, kid: str = None, algorithm: str = "RS256") -> str final_claims = {**default_claims, **claims} - jws = JsonWebSignature() - jwk = JsonWebKey.import_key(rsa_key_pair["private_pem"]) - - payload_json = json.dumps(final_claims) - - token = jws.serialize_compact(header, payload_json, jwk) + private_key = import_key(rsa_key_pair["private_pem"], "RSA") + token = jose_jwt.encode(header, final_claims, private_key) return token.decode('utf-8') if isinstance(token, bytes) else token return create_token diff --git a/uv.lock b/uv.lock index b2fdb696..b66b96c9 100644 --- a/uv.lock +++ b/uv.lock @@ -209,9 +209,9 @@ name = "aiologic" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } wheels = [ @@ -1056,8 +1056,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -2310,9 +2310,9 @@ dev = [{ name = "pytest-cov", specifier = ">=6.2.1" }] name = "keycardai-oauth" source = { editable = "packages/oauth" } dependencies = [ - { name = "authlib" }, { name = "cryptography" }, { name = "httpx" }, + { name = "joserfc" }, { name = "pydantic" }, ] @@ -2329,9 +2329,9 @@ dev = [ [package.metadata] requires-dist = [ - { name = "authlib", specifier = ">=1.6.3" }, { name = "cryptography", specifier = ">=45.0.7" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "joserfc", specifier = ">=1.6.4" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.1" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.1.0" }, @@ -3330,11 +3330,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "flatbuffers" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, + { name = "flatbuffers", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", marker = "python_full_version < '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, @@ -3374,10 +3374,10 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13'", ] dependencies = [ - { name = "flatbuffers" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "protobuf" }, + { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c1/00/dccf702195572df51a40784fc939304595a0ae3577537d3b5be79273151a/onnxruntime-1.25.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5cf58ec7601120bb4370f0b868f794d3e3626db7b1b1dba366c27874b224e9de", size = 17762805, upload-time = "2026-04-27T22:00:45.336Z" }, @@ -5348,7 +5348,7 @@ name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mpmath" }, + { name = "mpmath", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ From f60f9e69317c573e31234b0e21d2512564c04810 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 19 Jul 2026 09:55:48 -0700 Subject: [PATCH 2/3] fix(keycardai-oauth): reject unknown JWT algorithms; minimize lockfile diff Address review feedback on the joserfc migration: - _key_type_for_algorithm now raises on an unrecognized algorithm instead of silently defaulting to RSA (a genuine mismatch previously failed later at key import / the algorithms=[...] gate; now it fails explicitly). - Hand-restore uv.lock to a minimal authlib->joserfc swap. Regenerating under a newer uv had added unrelated python_full_version markers to transitive deps (aiologic, onnxruntime deps, sympy). joserfc was already present transitively, so only the oauth entry changes. Verified consistent with uv sync --frozen. --- .../oauth/src/keycardai/oauth/utils/jwt.py | 5 +++- .../tests/keycardai/oauth/utils/test_jwt.py | 7 +++++ uv.lock | 30 +++++++++---------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/oauth/src/keycardai/oauth/utils/jwt.py b/packages/oauth/src/keycardai/oauth/utils/jwt.py index 249dfe3b..071ddf26 100644 --- a/packages/oauth/src/keycardai/oauth/utils/jwt.py +++ b/packages/oauth/src/keycardai/oauth/utils/jwt.py @@ -57,7 +57,10 @@ def _key_type_for_algorithm(algorithm: str) -> str: - return _ALG_KEY_TYPE.get(algorithm[:2], "RSA") + key_type = _ALG_KEY_TYPE.get(algorithm[:2]) + if key_type is None: + raise ValueError(f"Unsupported JWT algorithm: {algorithm}") + return key_type def build_substitute_user_token(identifier: str) -> str: diff --git a/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py b/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py index e869debd..33cd61e8 100644 --- a/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py +++ b/packages/oauth/tests/keycardai/oauth/utils/test_jwt.py @@ -705,6 +705,13 @@ def test_wrong_key_verification_failure(self, rsa_key_pair, jwt_token_factory): with pytest.raises(ValueError, match="JWT verification failed"): decode_and_verify_jwt(token, wrong_public_pem, "RS256") + def test_unsupported_algorithm_rejected(self, rsa_key_pair, jwt_token_factory): + """Unknown algorithms are rejected, not silently mapped to a key type.""" + token = jwt_token_factory({}, kid=rsa_key_pair["kid"]) + + with pytest.raises(ValueError, match="JWT verification failed"): + decode_and_verify_jwt(token, rsa_key_pair["public_pem"], "FOO256") + def test_parse_jwt_access_token_integration(self, rsa_key_pair, jwt_token_factory): """Test the complete parse_jwt_access_token flow with real crypto.""" diff --git a/uv.lock b/uv.lock index b66b96c9..07781d6b 100644 --- a/uv.lock +++ b/uv.lock @@ -209,9 +209,9 @@ name = "aiologic" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "wrapt", marker = "python_full_version < '3.13'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } wheels = [ @@ -1056,8 +1056,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "aiologic" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -3330,11 +3330,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "flatbuffers", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "protobuf", marker = "python_full_version < '3.11'" }, - { name = "sympy", marker = "python_full_version < '3.11'" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, @@ -3374,10 +3374,10 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13'", ] dependencies = [ - { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "protobuf", marker = "python_full_version >= '3.11'" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "protobuf" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c1/00/dccf702195572df51a40784fc939304595a0ae3577537d3b5be79273151a/onnxruntime-1.25.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5cf58ec7601120bb4370f0b868f794d3e3626db7b1b1dba366c27874b224e9de", size = 17762805, upload-time = "2026-04-27T22:00:45.336Z" }, @@ -5348,7 +5348,7 @@ name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mpmath", marker = "python_full_version < '3.11'" }, + { name = "mpmath" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ From 9d7b308b71d54b06673b3d3b33647f270570c760 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 19 Jul 2026 09:58:10 -0700 Subject: [PATCH 3/3] fix(keycardai-oauth): pin joserfc>=1.6.8 to clear GHSA advisories This PR promotes joserfc to a first-class dependency, so it must not ship on a vulnerable floor. joserfc 1.6.4 is affected by: - GHSA-gg9x-qcx2-xmrh (HIGH): HS256/384/512 verify accepts empty/nil HMAC key - GHSA-wphv-vfrh-23q5 (MODERATE): b64=false RFC7797 JWS payload-size bypass 1.6.8 fixes both (superset of the two Socket bot PRs #189/#190, which target 1.6.7 and 1.6.8). Root uv.lock resolves to 1.6.8; tests pass. Neither advisory affects our own code path (we verify RS256), but a correct floor matters for downstream consumers. --- packages/oauth/pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/oauth/pyproject.toml b/packages/oauth/pyproject.toml index 8b57c67f..1015433c 100644 --- a/packages/oauth/pyproject.toml +++ b/packages/oauth/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "pydantic>=2.11.7", "httpx>=0.28.1", "cryptography>=45.0.7", - "joserfc>=1.6.4", + "joserfc>=1.6.8", ] keywords = ["oauth", "oauth2", "authentication", "tokens", "security"] diff --git a/uv.lock b/uv.lock index 07781d6b..c47ce45f 100644 --- a/uv.lock +++ b/uv.lock @@ -1979,14 +1979,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.6.4" +version = "1.6.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/ac/d4fd5b30f82900eac60d765f179f0ba005825ac462cc8ced6e13ec685ab3/joserfc-1.6.8.tar.gz", hash = "sha256:878620c553a6ebdd76ccdc356782fee3f735f21a356d079a546b42a4670ace5f", size = 232930, upload-time = "2026-05-27T03:22:37.819Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, + { url = "https://files.pythonhosted.org/packages/98/8c/5cdce2cf3ce8155849baf9a5e2ce77e89dc87ec3bdb38259e5d85fbc45bd/joserfc-1.6.8-py3-none-any.whl", hash = "sha256:22fb31a69094a5e6f44632002a9df2c30c941fc6c8ce1b037e92c03de954cf9f", size = 70927, upload-time = "2026-05-27T03:22:35.796Z" }, ] [[package]] @@ -2331,7 +2331,7 @@ dev = [ requires-dist = [ { name = "cryptography", specifier = ">=45.0.7" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "joserfc", specifier = ">=1.6.4" }, + { name = "joserfc", specifier = ">=1.6.8" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.1" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.1.0" },