From 8648a53ec1c8e41c98280f3a6958cb3b58ad73a0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 04:55:56 +0000 Subject: [PATCH 1/2] fix(jwt-authenticator): handle PEM private keys with escaped newlines Fixes an issue where JWT authentication fails when PEM-formatted private keys contain escaped newlines (\n) instead of actual newline characters. This commonly occurs when keys are stored in configuration systems like Airbyte Cloud. The fix adds normalization logic in JwtAuthenticator._get_secret_key() that: - Detects PEM-style keys (containing '-----BEGIN' and 'KEY-----') - Converts escaped newlines to actual newlines before JWT signing - Is guarded to only affect PEM keys, leaving other secret types unchanged This resolves the same issue fixed for the Okta connector in airbytehq/airbyte#69831, but at the CDK level so all declarative/Builder connectors using JWT authentication with private keys benefit from the fix. Includes a unit test that verifies JWT signing works with escaped newlines. Co-Authored-By: syed.khadeer@airbyte.io --- airbyte_cdk/sources/declarative/auth/jwt.py | 5 +++ .../sources/declarative/auth/test_jwt.py | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/airbyte_cdk/sources/declarative/auth/jwt.py b/airbyte_cdk/sources/declarative/auth/jwt.py index 65eb6d1e5..31b55f6f6 100644 --- a/airbyte_cdk/sources/declarative/auth/jwt.py +++ b/airbyte_cdk/sources/declarative/auth/jwt.py @@ -183,6 +183,11 @@ def _get_secret_key(self) -> JwtKeyTypes: """ secret_key: str = self._secret_key.eval(self.config, json_loads=json.loads) + # Normalize escaped newlines for PEM-style keys + # This handles cases where keys are stored with literal \n characters instead of actual newlines + if isinstance(secret_key, str) and "\\n" in secret_key and "-----BEGIN" in secret_key and "KEY-----" in secret_key: + secret_key = secret_key.replace("\\n", "\n") + if self._passphrase: passphrase_value = self._passphrase.eval(self.config, json_loads=json.loads) if passphrase_value: diff --git a/unit_tests/sources/declarative/auth/test_jwt.py b/unit_tests/sources/declarative/auth/test_jwt.py index de6d7ac32..35b1efd8e 100644 --- a/unit_tests/sources/declarative/auth/test_jwt.py +++ b/unit_tests/sources/declarative/auth/test_jwt.py @@ -392,3 +392,45 @@ def test_get_request_headers(self, request_option, expected_header_key): } assert authenticator.get_auth_header() == expected_headers + + def test_get_signed_token_with_escaped_newlines_in_pem_key(self): + """Test that JWT signing works with PEM keys containing escaped newlines.""" + # Generate a test RSA private key + private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + + # Get the PEM representation with actual newlines + pem_with_newlines = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode() + + # Create a version with escaped newlines (as stored in some systems) + pem_with_escaped_newlines = pem_with_newlines.replace("\n", "\\n") + + # Test with escaped newlines - should work after normalization + authenticator = JwtAuthenticator( + config={}, + parameters={}, + secret_key=pem_with_escaped_newlines, + algorithm="RS256", + token_duration=1000, + typ="JWT", + iss="test_issuer", + ) + + signed_token = authenticator._get_signed_token() + + # Verify the token is valid + assert isinstance(signed_token, str) + assert len(signed_token.split(".")) == 3 + + # Verify we can decode it with the public key + public_key = private_key.public_key() + decoded_payload = jwt.decode(signed_token, public_key, algorithms=["RS256"]) + + assert decoded_payload["iss"] == "test_issuer" + assert "iat" in decoded_payload + assert "exp" in decoded_payload From 68d58f47ad1d2e7df42a70a10753586671db29b7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 04:58:21 +0000 Subject: [PATCH 2/2] style: apply ruff formatting to jwt.py Co-Authored-By: syed.khadeer@airbyte.io --- airbyte_cdk/sources/declarative/auth/jwt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/airbyte_cdk/sources/declarative/auth/jwt.py b/airbyte_cdk/sources/declarative/auth/jwt.py index 31b55f6f6..933856c32 100644 --- a/airbyte_cdk/sources/declarative/auth/jwt.py +++ b/airbyte_cdk/sources/declarative/auth/jwt.py @@ -185,7 +185,12 @@ def _get_secret_key(self) -> JwtKeyTypes: # Normalize escaped newlines for PEM-style keys # This handles cases where keys are stored with literal \n characters instead of actual newlines - if isinstance(secret_key, str) and "\\n" in secret_key and "-----BEGIN" in secret_key and "KEY-----" in secret_key: + if ( + isinstance(secret_key, str) + and "\\n" in secret_key + and "-----BEGIN" in secret_key + and "KEY-----" in secret_key + ): secret_key = secret_key.replace("\\n", "\n") if self._passphrase: