From aabfce30aca572ba3df0016d3f54777d048134b5 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Sun, 4 Oct 2026 13:49:24 +0530 Subject: [PATCH] fix: make JWT signing independent of platform path and time formatting --- atlassian_jwt_auth/key.py | 3 ++- atlassian_jwt_auth/signer.py | 5 ++++- atlassian_jwt_auth/tests/test_key.py | 15 +++++++++++++++ atlassian_jwt_auth/tests/test_signer.py | 17 +++++++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/atlassian_jwt_auth/key.py b/atlassian_jwt_auth/key.py index c72ecaa..7391385 100644 --- a/atlassian_jwt_auth/key.py +++ b/atlassian_jwt_auth/key.py @@ -1,6 +1,7 @@ import base64 import logging import os +import posixpath import re from collections.abc import Generator, Iterable from email.message import EmailMessage @@ -43,7 +44,7 @@ def validate_key_identifier(identifier: str) -> str: raise KeyIdentifierException(_error_msg) if not regex.match(identifier): raise KeyIdentifierException(_error_msg) - normalised = os.path.normpath(identifier) + normalised = posixpath.normpath(identifier) if normalised != identifier: raise KeyIdentifierException(_error_msg) if normalised.startswith("/"): diff --git a/atlassian_jwt_auth/signer.py b/atlassian_jwt_auth/signer.py index 2571299..edec3d2 100644 --- a/atlassian_jwt_auth/signer.py +++ b/atlassian_jwt_auth/signer.py @@ -64,7 +64,10 @@ def _generate_claims( "iat": now, "aud": audience, "jti": "%s:%s" - % (now.strftime("%s"), random.SystemRandom().getrandbits(32)), + % ( + calendar.timegm(now.utctimetuple()), + random.SystemRandom().getrandbits(32), + ), "nbf": now, "sub": self.subject or self.issuer, } diff --git a/atlassian_jwt_auth/tests/test_key.py b/atlassian_jwt_auth/tests/test_key.py index 34d3127..ccdc087 100644 --- a/atlassian_jwt_auth/tests/test_key.py +++ b/atlassian_jwt_auth/tests/test_key.py @@ -1,6 +1,9 @@ +import ntpath import unittest +from unittest.mock import patch import atlassian_jwt_auth +from atlassian_jwt_auth import key as key_module class TestKeyModule(unittest.TestCase): @@ -22,6 +25,10 @@ def test_key_identifier_with_invalid_keys(self) -> None: "a/#a", "a/a?x", "a/a;", + "a//b", + "a/./b", + "a/../b", + "a/b/", ] for key in keys: with self.assertRaises(ValueError): @@ -32,3 +39,11 @@ def test_key_identifier_with_valid_keys(self) -> None: for key in ["oa.oo/a", "oo.sasdf.asdf/yes", "oo/o"]: key_id = atlassian_jwt_auth.KeyIdentifier(identifier=key) self.assertEqual(key_id.key_id, key) + + def test_key_identifier_is_independent_of_windows_paths(self) -> None: + with patch.object(key_module, "os") as windows_os: + windows_os.path = ntpath + identifier = "issuer.example/signing-key" + self.assertEqual( + atlassian_jwt_auth.KeyIdentifier(identifier).key_id, identifier + ) diff --git a/atlassian_jwt_auth/tests/test_signer.py b/atlassian_jwt_auth/tests/test_signer.py index 5b8aa40..a9e6b7e 100644 --- a/atlassian_jwt_auth/tests/test_signer.py +++ b/atlassian_jwt_auth/tests/test_signer.py @@ -56,8 +56,21 @@ def test_jti_changes(self): first = jwt_auth_signer._generate_claims(aud)["jti"] second = jwt_auth_signer._generate_claims(aud)["jti"] self.assertNotEqual(first, second) - self.assertTrue(str(expected_now.strftime("%s")) in first) - self.assertTrue(str(expected_now.strftime("%s")) in second) + self.assertTrue(first.startswith("978307200:")) + self.assertTrue(second.startswith("978307200:")) + + def test_generate_claims_without_platform_strftime_extensions(self): + class PortableDatetime(datetime.datetime): + def strftime(self, fmt): + if "%s" in fmt: + raise ValueError("Invalid format string") + return super().strftime(fmt) + + signer = utils.get_example_jwt_auth_signer( + algorithm=self.algorithm, private_key_pem=self._private_key_pem + ) + signer._now = lambda: PortableDatetime(2001, 1, 1, tzinfo=datetime.timezone.utc) + self.assertTrue(signer._generate_claims("aud")["jti"].startswith("978307200:")) @mock.patch("jwt.encode") def test_generate_jwt(self, m_jwt_encode):