Skip to content

Commit a1f173f

Browse files
refactor(cdk): replace token_prefix with interpolation approach for SessionTokenAuthenticator
This replaces the token_prefix approach with a more flexible interpolation approach using api_token template. Users can now use Jinja templates like 'Token {{ session_token }}' for Django REST Framework APIs. Changes: - Replace PrefixedTokenProvider with InterpolatedSessionTokenProvider - Update schema to use api_token field instead of token_prefix - Default api_token is '{{ session_token }}' for backward compatibility - Update factory to always wrap with InterpolatedSessionTokenProvider - Update tests to reflect new approach Co-Authored-By: Ryan Waskewich <ryan.waskewich@airbyte.io>
1 parent 1c36090 commit a1f173f

6 files changed

Lines changed: 83 additions & 53 deletions

File tree

airbyte_cdk/sources/declarative/auth/token_provider.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,21 @@ def get_token(self) -> str:
8585

8686

8787
@dataclass
88-
class PrefixedTokenProvider(TokenProvider):
89-
"""Wraps a TokenProvider and prepends a prefix to the token value.
88+
class InterpolatedSessionTokenProvider(TokenProvider):
89+
"""Provides a token by interpolating a template with the session token.
9090
91-
This is useful for APIs that require a specific prefix before the token,
92-
such as Django REST Framework APIs that expect "Token <value>" format.
91+
This allows flexible token formatting, such as "Token {{ session_token }}"
92+
for Django REST Framework APIs that expect "Authorization: Token <value>".
9393
"""
9494

95-
token_provider: TokenProvider
96-
prefix: str
95+
config: Config
96+
api_token: Union[InterpolatedString, str]
97+
session_token_provider: TokenProvider
98+
parameters: Mapping[str, Any]
99+
100+
def __post_init__(self) -> None:
101+
self._token_template = InterpolatedString.create(self.api_token, parameters=self.parameters)
97102

98103
def get_token(self) -> str:
99-
return f"{self.prefix}{self.token_provider.get_token()}"
104+
session_token = self.session_token_provider.get_token()
105+
return str(self._token_template.eval(self.config, session_token=session_token))

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,14 +2067,18 @@ definitions:
20672067
field_name: Authorization
20682068
- inject_into: request_parameter
20692069
field_name: authKey
2070-
token_prefix:
2071-
title: Token Prefix
2072-
description: 'A prefix to prepend to the session token when injecting it into requests. For example, use "Token " (with trailing space) for APIs that expect "Authorization: Token <token>".'
2070+
api_token:
2071+
title: API Token Template
2072+
description: 'A template for the token value to inject. Use {{ session_token }} to reference the session token. For example, use "Token {{ session_token }}" for APIs that expect "Authorization: Token <token>".'
20732073
type: string
2074-
default: ""
2074+
default: "{{ session_token }}"
2075+
interpolation_context:
2076+
- config
2077+
- session_token
20752078
examples:
2076-
- "Token "
2077-
- "Bearer "
2079+
- "{{ session_token }}"
2080+
- "Token {{ session_token }}"
2081+
- "Bearer {{ session_token }}"
20782082
SessionTokenRequestBearerAuthenticator:
20792083
title: Bearer Authenticator
20802084
description: Authenticator for requests using the session token as a standard bearer token.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
13
# generated by datamodel-codegen:
24
# filename: declarative_component_schema.yaml
35

@@ -2055,11 +2057,15 @@ class SessionTokenRequestApiKeyAuthenticator(BaseModel):
20552057
],
20562058
title="Inject API Key Into Outgoing HTTP Request",
20572059
)
2058-
token_prefix: Optional[str] = Field(
2059-
"",
2060-
description='A prefix to prepend to the session token when injecting it into requests. For example, use "Token " (with trailing space) for APIs that expect "Authorization: Token <token>".',
2061-
examples=["Token ", "Bearer "],
2062-
title="Token Prefix",
2060+
api_token: Optional[str] = Field(
2061+
"{{ session_token }}",
2062+
description='A template for the token value to inject. Use {{ session_token }} to reference the session token. For example, use "Token {{ session_token }}" for APIs that expect "Authorization: Token <token>".',
2063+
examples=[
2064+
"{{ session_token }}",
2065+
"Token {{ session_token }}",
2066+
"Bearer {{ session_token }}",
2067+
],
2068+
title="API Token Template",
20632069
)
20642070

20652071

@@ -2745,7 +2751,7 @@ class HttpRequester(BaseModelWithDeprecations):
27452751
)
27462752
use_cache: Optional[bool] = Field(
27472753
False,
2748-
description="Enables stream requests caching. When set to true, repeated requests to the same URL will return cached responses. Parent streams automatically have caching enabled. Only set this to false if you are certain that caching should be disabled, as it may negatively impact performance when the same data is needed multiple times (e.g., for scroll-based pagination APIs where caching causes duplicate records).",
2754+
description="Enables stream requests caching. This field is automatically set by the CDK.",
27492755
title="Use Cache",
27502756
)
27512757
parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@
6767
LegacySessionTokenAuthenticator,
6868
)
6969
from airbyte_cdk.sources.declarative.auth.token_provider import (
70+
InterpolatedSessionTokenProvider,
7071
InterpolatedStringTokenProvider,
71-
PrefixedTokenProvider,
7272
SessionTokenProvider,
7373
TokenProvider,
7474
)
@@ -1170,14 +1170,16 @@ def create_session_token_authenticator(
11701170
token_provider=token_provider,
11711171
)
11721172
else:
1173-
# Get the token_prefix if specified, wrap the token provider if needed
1174-
token_prefix = getattr(model.request_authentication, "token_prefix", None) or ""
1175-
final_token_provider: TokenProvider = token_provider
1176-
if token_prefix:
1177-
final_token_provider = PrefixedTokenProvider(
1178-
token_provider=token_provider,
1179-
prefix=token_prefix,
1180-
)
1173+
# Get the api_token template if specified, default to just the session token
1174+
api_token_template = (
1175+
getattr(model.request_authentication, "api_token", None) or "{{ session_token }}"
1176+
)
1177+
final_token_provider: TokenProvider = InterpolatedSessionTokenProvider(
1178+
config=config,
1179+
api_token=api_token_template,
1180+
session_token_provider=token_provider,
1181+
parameters=model.parameters or {},
1182+
)
11811183
return self.create_api_key_authenticator(
11821184
ApiKeyAuthenticatorModel(
11831185
type="ApiKeyAuthenticator",

unit_tests/sources/declarative/auth/test_token_provider.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from isodate import parse_duration
1010

1111
from airbyte_cdk.sources.declarative.auth.token_provider import (
12+
InterpolatedSessionTokenProvider,
1213
InterpolatedStringTokenProvider,
13-
PrefixedTokenProvider,
1414
SessionTokenProvider,
1515
)
1616
from airbyte_cdk.sources.declarative.exceptions import ReadException
@@ -83,19 +83,26 @@ def test_session_token_provider_ignored_response():
8383

8484

8585
@pytest.mark.parametrize(
86-
"prefix,expected_token",
86+
"api_token_template,expected_token",
8787
[
88-
pytest.param("Token ", "Token my_token", id="token_prefix"),
89-
pytest.param("Bearer ", "Bearer my_token", id="bearer_prefix"),
90-
pytest.param("", "my_token", id="empty_prefix"),
91-
pytest.param("Custom-", "Custom-my_token", id="custom_prefix"),
88+
pytest.param("Token {{ session_token }}", "Token my_token", id="token_prefix"),
89+
pytest.param("Bearer {{ session_token }}", "Bearer my_token", id="bearer_prefix"),
90+
pytest.param("{{ session_token }}", "my_token", id="just_session_token"),
91+
pytest.param("Custom-{{ session_token }}", "Custom-my_token", id="custom_prefix"),
92+
pytest.param(
93+
"realm=xyz, token={{ session_token }}",
94+
"realm=xyz, token=my_token",
95+
id="complex_format",
96+
),
9297
],
9398
)
94-
def test_prefixed_token_provider(prefix, expected_token):
95-
"""Test that PrefixedTokenProvider correctly prepends prefix to token."""
99+
def test_interpolated_session_token_provider(api_token_template, expected_token):
100+
"""Test that InterpolatedSessionTokenProvider correctly interpolates session token."""
96101
underlying_provider = create_session_token_provider()
97-
prefixed_provider = PrefixedTokenProvider(
98-
token_provider=underlying_provider,
99-
prefix=prefix,
102+
interpolated_provider = InterpolatedSessionTokenProvider(
103+
config={"some_config": "value"},
104+
api_token=api_token_template,
105+
session_token_provider=underlying_provider,
106+
parameters={},
100107
)
101-
assert prefixed_provider.get_token() == expected_token
108+
assert interpolated_provider.get_token() == expected_token

unit_tests/sources/declarative/parsers/test_model_to_component_factory.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
LegacySessionTokenAuthenticator,
4444
)
4545
from airbyte_cdk.sources.declarative.auth.token_provider import (
46-
PrefixedTokenProvider,
46+
InterpolatedSessionTokenProvider,
4747
SessionTokenProvider,
4848
)
4949
from airbyte_cdk.sources.declarative.checks import CheckStream
@@ -1844,22 +1844,25 @@ def test_create_request_with_session_authenticator():
18441844
)
18451845

18461846
assert isinstance(selector.authenticator, ApiKeyAuthenticator)
1847-
assert isinstance(selector.authenticator.token_provider, SessionTokenProvider)
1848-
assert selector.authenticator.token_provider.session_token_path == ["id"]
1849-
assert isinstance(selector.authenticator.token_provider.login_requester, HttpRequester)
1850-
assert selector.authenticator.token_provider.session_token_path == ["id"]
1847+
# Default behavior wraps with InterpolatedSessionTokenProvider using "{{ session_token }}"
1848+
assert isinstance(selector.authenticator.token_provider, InterpolatedSessionTokenProvider)
1849+
assert selector.authenticator.token_provider.api_token == "{{ session_token }}"
1850+
session_token_provider = selector.authenticator.token_provider.session_token_provider
1851+
assert isinstance(session_token_provider, SessionTokenProvider)
1852+
assert session_token_provider.session_token_path == ["id"]
1853+
assert isinstance(session_token_provider.login_requester, HttpRequester)
18511854
assert (
1852-
selector.authenticator.token_provider.login_requester._url_base.eval(input_config)
1855+
session_token_provider.login_requester._url_base.eval(input_config)
18531856
== "https://api.sendgrid.com"
18541857
)
1855-
assert selector.authenticator.token_provider.login_requester.get_request_body_json() == {
1858+
assert session_token_provider.login_requester.get_request_body_json() == {
18561859
"username": "lists",
18571860
"password": "verysecrettoken",
18581861
}
18591862

18601863

1861-
def test_create_request_with_session_authenticator_with_token_prefix():
1862-
"""Test that token_prefix wraps the token provider with PrefixedTokenProvider."""
1864+
def test_create_request_with_session_authenticator_with_api_token_template():
1865+
"""Test that api_token wraps the token provider with InterpolatedSessionTokenProvider."""
18631866
content = """
18641867
requester:
18651868
type: HttpRequester
@@ -1888,7 +1891,7 @@ def test_create_request_with_session_authenticator_with_token_prefix():
18881891
type: RequestOption
18891892
field_name: Authorization
18901893
inject_into: header
1891-
token_prefix: "Token "
1894+
api_token: "Token {{ session_token }}"
18921895
"""
18931896
name = "name"
18941897
parsed_manifest = YamlDeclarativeSource._parse(content)
@@ -1906,9 +1909,11 @@ def test_create_request_with_session_authenticator_with_token_prefix():
19061909
)
19071910

19081911
assert isinstance(selector.authenticator, ApiKeyAuthenticator)
1909-
assert isinstance(selector.authenticator.token_provider, PrefixedTokenProvider)
1910-
assert selector.authenticator.token_provider.prefix == "Token "
1911-
assert isinstance(selector.authenticator.token_provider.token_provider, SessionTokenProvider)
1912+
assert isinstance(selector.authenticator.token_provider, InterpolatedSessionTokenProvider)
1913+
assert selector.authenticator.token_provider.api_token == "Token {{ session_token }}"
1914+
assert isinstance(
1915+
selector.authenticator.token_provider.session_token_provider, SessionTokenProvider
1916+
)
19121917

19131918

19141919
def test_given_composite_error_handler_does_not_match_response_then_fallback_on_default_error_handler(

0 commit comments

Comments
 (0)