Skip to content

Commit b20cd1b

Browse files
authored
✨ airbyte-cdk - Adds JwtAuthenticator to low-code (#37005)
1 parent 0439cbc commit b20cd1b

File tree

11 files changed

+1211
-492
lines changed

11 files changed

+1211
-492
lines changed

airbyte-cdk/python/airbyte_cdk/sources/declarative/auth/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
#
44

55
from airbyte_cdk.sources.declarative.auth.oauth import DeclarativeOauth2Authenticator
6+
from airbyte_cdk.sources.declarative.auth.jwt import JwtAuthenticator
67

78
__all__ = [
89
"DeclarativeOauth2Authenticator",
10+
"JwtAuthenticator"
911
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import base64
6+
from dataclasses import InitVar, dataclass
7+
from datetime import datetime
8+
from typing import Any, Mapping, Optional, Union
9+
10+
import jwt
11+
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator
12+
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
13+
from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
14+
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
15+
16+
17+
class JwtAlgorithm(str):
18+
"""
19+
Enum for supported JWT algorithms
20+
"""
21+
22+
HS256 = "HS256"
23+
HS384 = "HS384"
24+
HS512 = "HS512"
25+
ES256 = "ES256"
26+
ES256K = "ES256K"
27+
ES384 = "ES384"
28+
ES512 = "ES512"
29+
RS256 = "RS256"
30+
RS384 = "RS384"
31+
RS512 = "RS512"
32+
PS256 = "PS256"
33+
PS384 = "PS384"
34+
PS512 = "PS512"
35+
EdDSA = "EdDSA"
36+
37+
38+
@dataclass
39+
class JwtAuthenticator(DeclarativeAuthenticator):
40+
"""
41+
Generates a JSON Web Token (JWT) based on a declarative connector configuration file. The generated token is attached to each request via the Authorization header.
42+
43+
Attributes:
44+
config (Mapping[str, Any]): The user-provided configuration as specified by the source's spec
45+
secret_key (Union[InterpolatedString, str]): The secret key used to sign the JWT
46+
algorithm (Union[str, JwtAlgorithm]): The algorithm used to sign the JWT
47+
token_duration (Optional[int]): The duration in seconds for which the token is valid
48+
base64_encode_secret_key (Optional[Union[InterpolatedBoolean, str, bool]]): Whether to base64 encode the secret key
49+
header_prefix (Optional[Union[InterpolatedString, str]]): The prefix to add to the Authorization header
50+
kid (Optional[Union[InterpolatedString, str]]): The key identifier to be included in the JWT header
51+
typ (Optional[Union[InterpolatedString, str]]): The type of the JWT.
52+
cty (Optional[Union[InterpolatedString, str]]): The content type of the JWT.
53+
iss (Optional[Union[InterpolatedString, str]]): The issuer of the JWT.
54+
sub (Optional[Union[InterpolatedString, str]]): The subject of the JWT.
55+
aud (Optional[Union[InterpolatedString, str]]): The audience of the JWT.
56+
additional_jwt_headers (Optional[Mapping[str, Any]]): Additional headers to include in the JWT.
57+
additional_jwt_payload (Optional[Mapping[str, Any]]): Additional payload to include in the JWT.
58+
"""
59+
60+
config: Mapping[str, Any]
61+
parameters: InitVar[Mapping[str, Any]]
62+
secret_key: Union[InterpolatedString, str]
63+
algorithm: Union[str, JwtAlgorithm]
64+
token_duration: Optional[int]
65+
base64_encode_secret_key: Optional[Union[InterpolatedBoolean, str, bool]] = False
66+
header_prefix: Optional[Union[InterpolatedString, str]] = None
67+
kid: Optional[Union[InterpolatedString, str]] = None
68+
typ: Optional[Union[InterpolatedString, str]] = None
69+
cty: Optional[Union[InterpolatedString, str]] = None
70+
iss: Optional[Union[InterpolatedString, str]] = None
71+
sub: Optional[Union[InterpolatedString, str]] = None
72+
aud: Optional[Union[InterpolatedString, str]] = None
73+
additional_jwt_headers: Optional[Mapping[str, Any]] = None
74+
additional_jwt_payload: Optional[Mapping[str, Any]] = None
75+
76+
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
77+
self._secret_key = InterpolatedString.create(self.secret_key, parameters=parameters)
78+
self._algorithm = JwtAlgorithm(self.algorithm) if isinstance(self.algorithm, str) else self.algorithm
79+
self._base64_encode_secret_key = (
80+
InterpolatedBoolean(self.base64_encode_secret_key, parameters=parameters)
81+
if isinstance(self.base64_encode_secret_key, str)
82+
else self.base64_encode_secret_key
83+
)
84+
self._token_duration = self.token_duration
85+
self._header_prefix = InterpolatedString.create(self.header_prefix, parameters=parameters) if self.header_prefix else None
86+
self._kid = InterpolatedString.create(self.kid, parameters=parameters) if self.kid else None
87+
self._typ = InterpolatedString.create(self.typ, parameters=parameters) if self.typ else None
88+
self._cty = InterpolatedString.create(self.cty, parameters=parameters) if self.cty else None
89+
self._iss = InterpolatedString.create(self.iss, parameters=parameters) if self.iss else None
90+
self._sub = InterpolatedString.create(self.sub, parameters=parameters) if self.sub else None
91+
self._aud = InterpolatedString.create(self.aud, parameters=parameters) if self.aud else None
92+
self._additional_jwt_headers = InterpolatedMapping(self.additional_jwt_headers or {}, parameters=parameters)
93+
self._additional_jwt_payload = InterpolatedMapping(self.additional_jwt_payload or {}, parameters=parameters)
94+
95+
def _get_jwt_headers(self) -> dict[str, Any]:
96+
""" "
97+
Builds and returns the headers used when signing the JWT.
98+
"""
99+
headers = self._additional_jwt_headers.eval(self.config)
100+
if any(prop in headers for prop in ["kid", "alg", "typ", "cty"]):
101+
raise ValueError("'kid', 'alg', 'typ', 'cty' are reserved headers and should not be set as part of 'additional_jwt_headers'")
102+
103+
if self._kid:
104+
headers["kid"] = self._kid.eval(self.config)
105+
if self._typ:
106+
headers["typ"] = self._typ.eval(self.config)
107+
if self._cty:
108+
headers["cty"] = self._cty.eval(self.config)
109+
headers["alg"] = self._algorithm
110+
return headers
111+
112+
def _get_jwt_payload(self) -> dict[str, Any]:
113+
"""
114+
Builds and returns the payload used when signing the JWT.
115+
"""
116+
now = int(datetime.now().timestamp())
117+
exp = now + self._token_duration if isinstance(self._token_duration, int) else now
118+
nbf = now
119+
120+
payload = self._additional_jwt_payload.eval(self.config)
121+
if any(prop in payload for prop in ["iss", "sub", "aud", "iat", "exp", "nbf"]):
122+
raise ValueError(
123+
"'iss', 'sub', 'aud', 'iat', 'exp', 'nbf' are reserved properties and should not be set as part of 'additional_jwt_payload'"
124+
)
125+
126+
if self._iss:
127+
payload["iss"] = self._iss.eval(self.config)
128+
if self._sub:
129+
payload["sub"] = self._sub.eval(self.config)
130+
if self._aud:
131+
payload["aud"] = self._aud.eval(self.config)
132+
payload["iat"] = now
133+
payload["exp"] = exp
134+
payload["nbf"] = nbf
135+
return payload
136+
137+
def _get_secret_key(self) -> str:
138+
"""
139+
Returns the secret key used to sign the JWT.
140+
"""
141+
secret_key: str = self._secret_key.eval(self.config)
142+
return base64.b64encode(secret_key.encode()).decode() if self._base64_encode_secret_key else secret_key
143+
144+
def _get_signed_token(self) -> Union[str, Any]:
145+
"""
146+
Signed the JWT using the provided secret key and algorithm and the generated headers and payload. For additional information on PyJWT see: https://pyjwt.readthedocs.io/en/stable/
147+
"""
148+
try:
149+
return jwt.encode(
150+
payload=self._get_jwt_payload(),
151+
key=self._get_secret_key(),
152+
algorithm=self._algorithm,
153+
headers=self._get_jwt_headers(),
154+
)
155+
except Exception as e:
156+
raise ValueError(f"Failed to sign token: {e}")
157+
158+
def _get_header_prefix(self) -> Union[str, None]:
159+
"""
160+
Returns the header prefix to be used when attaching the token to the request.
161+
"""
162+
return self._header_prefix.eval(self.config) if self._header_prefix else None
163+
164+
@property
165+
def auth_header(self) -> str:
166+
return "Authorization"
167+
168+
@property
169+
def token(self) -> str:
170+
return f"{self._get_header_prefix()} {self._get_signed_token()}" if self._get_header_prefix() else self._get_signed_token()

airbyte-cdk/python/airbyte_cdk/sources/declarative/declarative_component_schema.yaml

+124
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,15 @@ definitions:
257257
- "$ref": "#/definitions/BearerAuthenticator"
258258
- "$ref": "#/definitions/CustomAuthenticator"
259259
- "$ref": "#/definitions/OAuthAuthenticator"
260+
- "$ref": "#/definitions/JwtAuthenticator"
260261
- "$ref": "#/definitions/NoAuth"
261262
- "$ref": "#/definitions/SessionTokenAuthenticator"
262263
- "$ref": "#/definitions/LegacySessionTokenAuthenticator"
263264
examples:
264265
- authenticators:
265266
token: "#/definitions/ApiKeyAuthenticator"
266267
oauth: "#/definitions/OAuthAuthenticator"
268+
jwt: "#/definitions/JwtAuthenticator"
267269
$parameters:
268270
type: object
269271
additionalProperties: true
@@ -833,6 +835,127 @@ definitions:
833835
$parameters:
834836
type: object
835837
additionalProperties: true
838+
JwtAuthenticator:
839+
title: JWT Authenticator
840+
description: Authenticator for requests using JWT authentication flow.
841+
type: object
842+
required:
843+
- type
844+
- secret_key
845+
- algorithm
846+
properties:
847+
type:
848+
type: string
849+
enum: [JwtAuthenticator]
850+
secret_key:
851+
type: string
852+
description: Secret used to sign the JSON web token.
853+
examples:
854+
- "{{ config['secret_key'] }}"
855+
base64_encode_secret_key:
856+
type: boolean
857+
description: When set to true, the secret key will be base64 encoded prior to being encoded as part of the JWT. Only set to "true" when required by the API.
858+
default: False
859+
algorithm:
860+
type: string
861+
description: Algorithm used to sign the JSON web token.
862+
enum:
863+
[
864+
"HS256",
865+
"HS384",
866+
"HS512",
867+
"ES256",
868+
"ES256K",
869+
"ES384",
870+
"ES512",
871+
"RS256",
872+
"RS384",
873+
"RS512",
874+
"PS256",
875+
"PS384",
876+
"PS512",
877+
"EdDSA",
878+
]
879+
examples:
880+
- ES256
881+
- HS256
882+
- RS256
883+
- "{{ config['algorithm'] }}"
884+
token_duration:
885+
type: integer
886+
title: Token Duration
887+
description: The amount of time in seconds a JWT token can be valid after being issued.
888+
default: 1200
889+
examples:
890+
- 1200
891+
- 3600
892+
header_prefix:
893+
type: string
894+
title: Header Prefix
895+
description: The prefix to be used within the Authentication header.
896+
examples:
897+
- "Bearer"
898+
- "Basic"
899+
jwt_headers:
900+
type: object
901+
title: JWT Headers
902+
description: JWT headers used when signing JSON web token.
903+
additionalProperties: false
904+
properties:
905+
kid:
906+
type: string
907+
title: Key Identifier
908+
description: Private key ID for user account.
909+
examples:
910+
- "{{ config['kid'] }}"
911+
typ:
912+
type: string
913+
title: Type
914+
description: The media type of the complete JWT.
915+
default: JWT
916+
examples:
917+
- JWT
918+
cty:
919+
type: string
920+
title: Content Type
921+
description: Content type of JWT header.
922+
examples:
923+
- JWT
924+
additional_jwt_headers:
925+
type: object
926+
title: Additional JWT Headers
927+
description: Additional headers to be included with the JWT headers object.
928+
additionalProperties: true
929+
jwt_payload:
930+
type: object
931+
title: JWT Payload
932+
description: JWT Payload used when signing JSON web token.
933+
additionalProperties: false
934+
properties:
935+
iss:
936+
type: string
937+
title: Issuer
938+
description: The user/principal that issued the JWT. Commonly a value unique to the user.
939+
examples:
940+
- "{{ config['iss'] }}"
941+
sub:
942+
type: string
943+
title: Subject
944+
description: The subject of the JWT. Commonly defined by the API.
945+
aud:
946+
type: string
947+
title: Audience
948+
description: The recipient that the JWT is intended for. Commonly defined by the API.
949+
examples:
950+
- "appstoreconnect-v1"
951+
additional_jwt_payload:
952+
type: object
953+
title: Additional JWT Payload Properties
954+
description: Additional properties to be added to the JWT payload.
955+
additionalProperties: true
956+
$parameters:
957+
type: object
958+
additionalProperties: true
836959
OAuthAuthenticator:
837960
title: OAuth2
838961
description: Authenticator for requests using OAuth 2.0 authorization flow.
@@ -1311,6 +1434,7 @@ definitions:
13111434
- "$ref": "#/definitions/BearerAuthenticator"
13121435
- "$ref": "#/definitions/CustomAuthenticator"
13131436
- "$ref": "#/definitions/OAuthAuthenticator"
1437+
- "$ref": "#/definitions/JwtAuthenticator"
13141438
- "$ref": "#/definitions/NoAuth"
13151439
- "$ref": "#/definitions/SessionTokenAuthenticator"
13161440
- "$ref": "#/definitions/LegacySessionTokenAuthenticator"

0 commit comments

Comments
 (0)