From 40d393870208db99d872da8e30516bda92ac17dd Mon Sep 17 00:00:00 2001 From: primetheus <865381+primetheus@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:43:15 -0400 Subject: [PATCH 1/2] fix: cast self.id to str in JWT payload for PyJWT 2.12+ compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyJWT 2.12+ enforces that the 'iss' claim must be a string per RFC 7519 Section 4.1.1 (StringOrURI). Previously self.id (an int) was passed directly, which worked with older PyJWT but fails with: TypeError: Issuer (iss) must be a string. This is safe for all PyJWT versions — older versions accept both int and str, and GitHub's API matches on value regardless of JSON type. --- src/githubapp/core.py | 2 +- tests/test_core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/githubapp/core.py b/src/githubapp/core.py index 754ef80..16f9a48 100644 --- a/src/githubapp/core.py +++ b/src/githubapp/core.py @@ -578,7 +578,7 @@ def _create_jwt(self, expiration=60): :return string: """ now = int(time.time()) - payload = {"iat": now, "exp": now + expiration, "iss": self.id} + payload = {"iat": now, "exp": now + expiration, "iss": str(self.id)} encrypted = jwt.encode(payload, key=self.key, algorithm="RS256") if isinstance(encrypted, bytes): diff --git a/tests/test_core.py b/tests/test_core.py index 6d96d02..652cc5d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -203,7 +203,7 @@ def test_create_jwt_payload_structure(self, mock_time, mock_jwt): github_app = GitHubApp(github_app_id=123, github_app_key=b"test_key") github_app._create_jwt(expiration=300) - expected_payload = {"iat": 1640995200, "exp": 1640995200 + 300, "iss": 123} + expected_payload = {"iat": 1640995200, "exp": 1640995200 + 300, "iss": "123"} mock_jwt.encode.assert_called_once_with( expected_payload, key=b"test_key", algorithm="RS256" ) From fc23f10d31e94e58ecc5d42aed711740548a3416 Mon Sep 17 00:00:00 2001 From: primetheus <865381+primetheus@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:17:08 -0400 Subject: [PATCH 2/2] fix: guard against None app ID before JWT creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback — str(None) would produce 'None' as the issuer, which passes PyJWT's type check but gets rejected by GitHub. Raise GitHubAppError with a clear message instead. --- src/githubapp/core.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/githubapp/core.py b/src/githubapp/core.py index 16f9a48..688bb0a 100644 --- a/src/githubapp/core.py +++ b/src/githubapp/core.py @@ -577,6 +577,12 @@ def _create_jwt(self, expiration=60): :param expiration: int :return string: """ + if self.id is None: + raise GitHubAppError( + message="GitHub App ID (github_app_id) is not configured; cannot generate JWT.", + status=None, + data=None, + ) now = int(time.time()) payload = {"iat": now, "exp": now + expiration, "iss": str(self.id)} encrypted = jwt.encode(payload, key=self.key, algorithm="RS256")