Skip to content

Commit ab9f644

Browse files
maxi297octavia-squidington-iii
andauthored
feat(oauth): Support refresh_token_error on the OAuthAuthenticatorModel level and … (#829)
Co-authored-by: octavia-squidington-iii <contact@airbyte.com>
1 parent 6395265 commit ab9f644

File tree

5 files changed

+121
-15
lines changed

5 files changed

+121
-15
lines changed

airbyte_cdk/sources/declarative/auth/oauth.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
from dataclasses import InitVar, dataclass, field
77
from datetime import datetime, timedelta
8-
from typing import Any, List, Mapping, Optional, Union
8+
from typing import Any, List, Mapping, Optional, Tuple, Union
99

1010
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator
1111
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
@@ -46,6 +46,9 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
4646
refresh_request_headers (Optional[Mapping[str, Any]]): The request headers to send in the refresh request
4747
grant_type: The grant_type to request for access_token. If set to refresh_token, the refresh_token parameter has to be provided
4848
message_repository (MessageRepository): the message repository used to emit logs on HTTP requests
49+
refresh_token_error_status_codes (Tuple[int, ...]): Status codes to identify refresh token errors in response
50+
refresh_token_error_key (str): Key to identify refresh token error in response
51+
refresh_token_error_values (Tuple[str, ...]): List of values to check for exception during token refresh process
4952
"""
5053

5154
config: Mapping[str, Any]
@@ -72,9 +75,16 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
7275
message_repository: MessageRepository = NoopMessageRepository()
7376
profile_assertion: Optional[DeclarativeAuthenticator] = None
7477
use_profile_assertion: Optional[Union[InterpolatedBoolean, str, bool]] = False
78+
refresh_token_error_status_codes: Tuple[int, ...] = ()
79+
refresh_token_error_key: str = ""
80+
refresh_token_error_values: Tuple[str, ...] = ()
7581

7682
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
77-
super().__init__()
83+
super().__init__(
84+
refresh_token_error_status_codes=self.refresh_token_error_status_codes,
85+
refresh_token_error_key=self.refresh_token_error_key,
86+
refresh_token_error_values=self.refresh_token_error_values,
87+
)
7888
if self.token_refresh_endpoint is not None:
7989
self._token_refresh_endpoint: Optional[InterpolatedString] = InterpolatedString.create(
8090
self.token_refresh_endpoint, parameters=parameters

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,28 @@ definitions:
14271427
type: string
14281428
examples:
14291429
- "%Y-%m-%d %H:%M:%S.%f+00:00"
1430+
refresh_token_error_status_codes:
1431+
title: Refresh Token Error Status Codes
1432+
description: Status Codes to Identify refresh token error in response (Refresh Token Error Key and Refresh Token Error Values should be also specified). Responses with one of the error status code and containing an error value will be flagged as a config error
1433+
type: array
1434+
items:
1435+
type: integer
1436+
examples:
1437+
- [400, 500]
1438+
refresh_token_error_key:
1439+
title: Refresh Token Error Key
1440+
description: Key to Identify refresh token error in response (Refresh Token Error Status Codes and Refresh Token Error Values should be also specified).
1441+
type: string
1442+
examples:
1443+
- "error"
1444+
refresh_token_error_values:
1445+
title: Refresh Token Error Values
1446+
description: 'List of values to check for exception during token refresh process. Used to check if the error found in the response matches the key from the Refresh Token Error Key field (e.g. response={"error": "invalid_grant"}). Only responses with one of the error status code and containing an error value will be flagged as a config error'
1447+
type: array
1448+
items:
1449+
type: string
1450+
examples:
1451+
- ["invalid_grant", "invalid_permissions"]
14301452
refresh_token_updater:
14311453
title: Refresh Token Updater
14321454
description: When the refresh token updater is defined, new refresh tokens, access tokens and the access token expiry date are written back from the authentication response to the config object. This is important if the refresh token can only used once.
@@ -1468,7 +1490,7 @@ definitions:
14681490
examples:
14691491
- ["credentials", "token_expiry_date"]
14701492
refresh_token_error_status_codes:
1471-
title: Refresh Token Error Status Codes
1493+
title: (Deprecated - Use the same field on the OAuthAuthenticator level) Refresh Token Error Status Codes
14721494
description: Status Codes to Identify refresh token error in response (Refresh Token Error Key and Refresh Token Error Values should be also specified). Responses with one of the error status code and containing an error value will be flagged as a config error
14731495
type: array
14741496
items:
@@ -1477,14 +1499,14 @@ definitions:
14771499
examples:
14781500
- [400, 500]
14791501
refresh_token_error_key:
1480-
title: Refresh Token Error Key
1502+
title: (Deprecated - Use the same field on the OAuthAuthenticator level) Refresh Token Error Key
14811503
description: Key to Identify refresh token error in response (Refresh Token Error Status Codes and Refresh Token Error Values should be also specified).
14821504
type: string
14831505
default: ""
14841506
examples:
14851507
- "error"
14861508
refresh_token_error_values:
1487-
title: Refresh Token Error Values
1509+
title: (Deprecated - Use the same field on the OAuthAuthenticator level) Refresh Token Error Values
14881510
description: 'List of values to check for exception during token refresh process. Used to check if the error found in the response matches the key from the Refresh Token Error Key field (e.g. response={"error": "invalid_grant"}). Only responses with one of the error status code and containing an error value will be flagged as a config error'
14891511
type: array
14901512
items:

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

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

@@ -426,19 +424,19 @@ class RefreshTokenUpdater(BaseModel):
426424
[],
427425
description="Status Codes to Identify refresh token error in response (Refresh Token Error Key and Refresh Token Error Values should be also specified). Responses with one of the error status code and containing an error value will be flagged as a config error",
428426
examples=[[400, 500]],
429-
title="Refresh Token Error Status Codes",
427+
title="(Deprecated - Use the same field on the OAuthAuthenticator level) Refresh Token Error Status Codes",
430428
)
431429
refresh_token_error_key: Optional[str] = Field(
432430
"",
433431
description="Key to Identify refresh token error in response (Refresh Token Error Status Codes and Refresh Token Error Values should be also specified).",
434432
examples=["error"],
435-
title="Refresh Token Error Key",
433+
title="(Deprecated - Use the same field on the OAuthAuthenticator level) Refresh Token Error Key",
436434
)
437435
refresh_token_error_values: Optional[List[str]] = Field(
438436
[],
439437
description='List of values to check for exception during token refresh process. Used to check if the error found in the response matches the key from the Refresh Token Error Key field (e.g. response={"error": "invalid_grant"}). Only responses with one of the error status code and containing an error value will be flagged as a config error',
440438
examples=[["invalid_grant", "invalid_permissions"]],
441-
title="Refresh Token Error Values",
439+
title="(Deprecated - Use the same field on the OAuthAuthenticator level) Refresh Token Error Values",
442440
)
443441

444442

@@ -1900,6 +1898,24 @@ class OAuthAuthenticator(BaseModel):
19001898
examples=["%Y-%m-%d %H:%M:%S.%f+00:00"],
19011899
title="Token Expiry Date Format",
19021900
)
1901+
refresh_token_error_status_codes: Optional[List[int]] = Field(
1902+
None,
1903+
description="Status Codes to Identify refresh token error in response (Refresh Token Error Key and Refresh Token Error Values should be also specified). Responses with one of the error status code and containing an error value will be flagged as a config error",
1904+
examples=[[400, 500]],
1905+
title="Refresh Token Error Status Codes",
1906+
)
1907+
refresh_token_error_key: Optional[str] = Field(
1908+
None,
1909+
description="Key to Identify refresh token error in response (Refresh Token Error Status Codes and Refresh Token Error Values should be also specified).",
1910+
examples=["error"],
1911+
title="Refresh Token Error Key",
1912+
)
1913+
refresh_token_error_values: Optional[List[str]] = Field(
1914+
None,
1915+
description='List of values to check for exception during token refresh process. Used to check if the error found in the response matches the key from the Refresh Token Error Key field (e.g. response={"error": "invalid_grant"}). Only responses with one of the error status code and containing an error value will be flagged as a config error',
1916+
examples=[["invalid_grant", "invalid_permissions"]],
1917+
title="Refresh Token Error Values",
1918+
)
19031919
refresh_token_updater: Optional[RefreshTokenUpdater] = Field(
19041920
None,
19051921
description="When the refresh token updater is defined, new refresh tokens, access tokens and the access token expiry date are written back from the authentication response to the config object. This is important if the refresh token can only used once.",

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Mapping,
1919
MutableMapping,
2020
Optional,
21+
Tuple,
2122
Type,
2223
Union,
2324
cast,
@@ -400,6 +401,9 @@
400401
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
401402
RecordSelector as RecordSelectorModel,
402403
)
404+
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
405+
RefreshTokenUpdater as RefreshTokenUpdaterModel,
406+
)
403407
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
404408
RemoveFields as RemoveFieldsModel,
405409
)
@@ -2789,6 +2793,9 @@ def create_oauth_authenticator(
27892793
else None
27902794
)
27912795

2796+
refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2797+
self._get_refresh_token_error_information(model)
2798+
)
27922799
if model.refresh_token_updater:
27932800
# ignore type error because fixing it would have a lot of dependencies, revisit later
27942801
return DeclarativeSingleUseRefreshTokenOauth2Authenticator( # type: ignore
@@ -2839,9 +2846,9 @@ def create_oauth_authenticator(
28392846
token_expiry_date_format=model.token_expiry_date_format,
28402847
token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
28412848
message_repository=self._message_repository,
2842-
refresh_token_error_status_codes=model.refresh_token_updater.refresh_token_error_status_codes,
2843-
refresh_token_error_key=model.refresh_token_updater.refresh_token_error_key,
2844-
refresh_token_error_values=model.refresh_token_updater.refresh_token_error_values,
2849+
refresh_token_error_status_codes=refresh_token_error_status_codes,
2850+
refresh_token_error_key=refresh_token_error_key,
2851+
refresh_token_error_values=refresh_token_error_values,
28452852
)
28462853
# ignore type error because fixing it would have a lot of dependencies, revisit later
28472854
return DeclarativeOauth2Authenticator( # type: ignore
@@ -2868,8 +2875,59 @@ def create_oauth_authenticator(
28682875
message_repository=self._message_repository,
28692876
profile_assertion=profile_assertion,
28702877
use_profile_assertion=model.use_profile_assertion,
2878+
refresh_token_error_status_codes=refresh_token_error_status_codes,
2879+
refresh_token_error_key=refresh_token_error_key,
2880+
refresh_token_error_values=refresh_token_error_values,
28712881
)
28722882

2883+
@staticmethod
2884+
def _get_refresh_token_error_information(
2885+
model: OAuthAuthenticatorModel,
2886+
) -> Tuple[Tuple[int, ...], str, Tuple[str, ...]]:
2887+
"""
2888+
In a previous version of the CDK, the auth error as config_error was only done if a refresh token updater was
2889+
defined. As a transition, we added those fields on the OAuthAuthenticatorModel. This method ensures that the
2890+
information is defined only once and return the right fields.
2891+
"""
2892+
refresh_token_updater = model.refresh_token_updater
2893+
is_defined_on_refresh_token_updated = refresh_token_updater and (
2894+
refresh_token_updater.refresh_token_error_status_codes
2895+
or refresh_token_updater.refresh_token_error_key
2896+
or refresh_token_updater.refresh_token_error_values
2897+
)
2898+
is_defined_on_oauth_authenticator = (
2899+
model.refresh_token_error_status_codes
2900+
or model.refresh_token_error_key
2901+
or model.refresh_token_error_values
2902+
)
2903+
if is_defined_on_refresh_token_updated and is_defined_on_oauth_authenticator:
2904+
raise ValueError(
2905+
"refresh_token_error should either be defined on the OAuthAuthenticatorModel or the RefreshTokenUpdaterModel, not both"
2906+
)
2907+
2908+
if is_defined_on_refresh_token_updated:
2909+
not_optional_refresh_token_updater: RefreshTokenUpdaterModel = refresh_token_updater # type: ignore # we know from the condition that this is not None
2910+
return (
2911+
tuple(not_optional_refresh_token_updater.refresh_token_error_status_codes)
2912+
if not_optional_refresh_token_updater.refresh_token_error_status_codes
2913+
else (),
2914+
not_optional_refresh_token_updater.refresh_token_error_key or "",
2915+
tuple(not_optional_refresh_token_updater.refresh_token_error_values)
2916+
if not_optional_refresh_token_updater.refresh_token_error_values
2917+
else (),
2918+
)
2919+
elif is_defined_on_oauth_authenticator:
2920+
return (
2921+
tuple(model.refresh_token_error_status_codes)
2922+
if model.refresh_token_error_status_codes
2923+
else (),
2924+
model.refresh_token_error_key or "",
2925+
tuple(model.refresh_token_error_values) if model.refresh_token_error_values else (),
2926+
)
2927+
2928+
# returning default values we think cover most cases
2929+
return (400,), "error", ("invalid_grant", "invalid_permissions")
2930+
28732931
def create_offset_increment(
28742932
self,
28752933
model: OffsetIncrementModel,

unit_tests/sources/declarative/parsers/test_model_to_component_factory.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,9 @@ def test_single_use_oauth_branch():
636636
# default values
637637
assert authenticator._access_token_config_path == ["credentials", "access_token"]
638638
assert authenticator._token_expiry_date_config_path == ["credentials", "token_expiry_date"]
639-
assert authenticator._refresh_token_error_status_codes == [400]
639+
assert authenticator._refresh_token_error_status_codes == (400,)
640640
assert authenticator._refresh_token_error_key == "error"
641-
assert authenticator._refresh_token_error_values == ["invalid_grant"]
641+
assert authenticator._refresh_token_error_values == ("invalid_grant",)
642642

643643

644644
def test_list_based_stream_slicer_with_values_refd():

0 commit comments

Comments
 (0)