Skip to content

Commit 8648a53

Browse files
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: [email protected] <[email protected]>
1 parent 80b7668 commit 8648a53

File tree

2 files changed

+47
-0
lines changed
  • airbyte_cdk/sources/declarative/auth
  • unit_tests/sources/declarative/auth

2 files changed

+47
-0
lines changed

airbyte_cdk/sources/declarative/auth/jwt.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ def _get_secret_key(self) -> JwtKeyTypes:
183183
"""
184184
secret_key: str = self._secret_key.eval(self.config, json_loads=json.loads)
185185

186+
# Normalize escaped newlines for PEM-style keys
187+
# This handles cases where keys are stored with literal \n characters instead of actual newlines
188+
if isinstance(secret_key, str) and "\\n" in secret_key and "-----BEGIN" in secret_key and "KEY-----" in secret_key:
189+
secret_key = secret_key.replace("\\n", "\n")
190+
186191
if self._passphrase:
187192
passphrase_value = self._passphrase.eval(self.config, json_loads=json.loads)
188193
if passphrase_value:

unit_tests/sources/declarative/auth/test_jwt.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,45 @@ def test_get_request_headers(self, request_option, expected_header_key):
392392
}
393393

394394
assert authenticator.get_auth_header() == expected_headers
395+
396+
def test_get_signed_token_with_escaped_newlines_in_pem_key(self):
397+
"""Test that JWT signing works with PEM keys containing escaped newlines."""
398+
# Generate a test RSA private key
399+
private_key = rsa.generate_private_key(
400+
public_exponent=65537, key_size=2048, backend=default_backend()
401+
)
402+
403+
# Get the PEM representation with actual newlines
404+
pem_with_newlines = private_key.private_bytes(
405+
encoding=serialization.Encoding.PEM,
406+
format=serialization.PrivateFormat.PKCS8,
407+
encryption_algorithm=serialization.NoEncryption(),
408+
).decode()
409+
410+
# Create a version with escaped newlines (as stored in some systems)
411+
pem_with_escaped_newlines = pem_with_newlines.replace("\n", "\\n")
412+
413+
# Test with escaped newlines - should work after normalization
414+
authenticator = JwtAuthenticator(
415+
config={},
416+
parameters={},
417+
secret_key=pem_with_escaped_newlines,
418+
algorithm="RS256",
419+
token_duration=1000,
420+
typ="JWT",
421+
iss="test_issuer",
422+
)
423+
424+
signed_token = authenticator._get_signed_token()
425+
426+
# Verify the token is valid
427+
assert isinstance(signed_token, str)
428+
assert len(signed_token.split(".")) == 3
429+
430+
# Verify we can decode it with the public key
431+
public_key = private_key.public_key()
432+
decoded_payload = jwt.decode(signed_token, public_key, algorithms=["RS256"])
433+
434+
assert decoded_payload["iss"] == "test_issuer"
435+
assert "iat" in decoded_payload
436+
assert "exp" in decoded_payload

0 commit comments

Comments
 (0)