You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by el...@apache.org on 2026/04/26 11:15:03 UTC

(airflow) branch main updated: fix(providers/airbyte): make OAuth credentials optional for Airbyte OSS deployments (#64986)

This is an automated email from the ASF dual-hosted git repository.

eladkal pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new bd6b2d58365 fix(providers/airbyte): make OAuth credentials optional for Airbyte OSS deployments (#64986)
bd6b2d58365 is described below

commit bd6b2d5836546c9ffac3498b20990fba672fde66
Author: Aldo <ao...@gmail.com>
AuthorDate: Sun Apr 26 08:14:55 2026 -0300

    fix(providers/airbyte): make OAuth credentials optional for Airbyte OSS deployments (#64986)
    
    The AirbyteHook unconditionally requires OAuth client credentials
    (client_id/client_secret) and calls /v1/applications/token on every
    request. This endpoint does not exist on Airbyte OSS v2.x without
    Keycloak/Enterprise auth, breaking the provider for all self-hosted
    OSS deployments.
    
    When client_id and client_secret are not provided in the connection,
    the hook now creates the API session without authentication
    (security=None), which the airbyte-api SDK already supports.
    
    Fixes #47161
---
 providers/airbyte/docs/connections.rst             | 45 ++++++++++++++--------
 .../src/airflow/providers/airbyte/hooks/airbyte.py | 43 ++++++++++++++++-----
 .../tests/unit/airbyte/hooks/test_airbyte.py       | 44 ++++++++++++++++++++-
 3 files changed, 106 insertions(+), 26 deletions(-)

diff --git a/providers/airbyte/docs/connections.rst b/providers/airbyte/docs/connections.rst
index 1f2a00643ed..5d41d2096ea 100644
--- a/providers/airbyte/docs/connections.rst
+++ b/providers/airbyte/docs/connections.rst
@@ -19,30 +19,45 @@
 
 Airbyte Connection
 ==================
-The Airbyte connection type use the Airbyte API Python SDK to authenticate to the server.
+The Airbyte connection type uses the Airbyte API Python SDK to communicate with the server.
+
+Authentication is **optional**: when Client ID and Client Secret are provided, the hook
+authenticates via OAuth2 Client Credentials (required for Airbyte Cloud and Enterprise).
+When they are omitted, the hook connects without authentication, which is suitable for
+Airbyte OSS deployments that do not have auth enabled.
+
+Host (required)
+    The fully qualified host URL of the Airbyte server.
+
+    * Airbyte Cloud: ``https://api.airbyte.com/v1/``
+    * Airbyte OSS (with auth): ``http://localhost:8000/api/public/v1/``
+    * Airbyte OSS (without auth): ``http://localhost:8000/api/v1/``
 
-Host(required)
-    The full qualified host domain to connect to the Airbyte server.
-    If you are using Airbyte Cloud: ``https://api.airbyte.com/v1/``
-    If you are using Airbyte OSS: ``http://localhost:8000/api/public/v1/``
     Be aware: If you're changing the API path, you must update the value accordingly.
 
 Token URL (optional)
-    The prefix for URL used to create the access token.
-    If you are using Airbyte Cloud: ``v1/applications/token`` (default value)
-    If you are using Airbyte OSS: ``/api/public/v1/applications/token```
+    The prefix for the URL used to create the access token. Only used when
+    Client ID and Client Secret are provided.
+
+    * Airbyte Cloud: ``v1/applications/token`` (default value)
+    * Airbyte OSS: ``/api/public/v1/applications/token``
+
     Be aware: If you're changing the API path, you must update the value accordingly.
 
-Client ID (required)
-    The Client ID to connect to the Airbyte server.
-    You can find this information in the Settings / Applications page in Airbyte UI.
+Client ID (optional)
+    The Client ID for OAuth2 authentication.
+    Required for Airbyte Cloud and Enterprise deployments.
+    You can find this in Settings > Applications in the Airbyte UI.
+    Leave blank for Airbyte OSS deployments without auth enabled.
 
-Client Secret (required)
-    The Client Secret to connect to the Airbyte server.
-    You can find this information in the Settings / Applications page in Airbyte UI.
+Client Secret (optional)
+    The Client Secret for OAuth2 authentication.
+    Required for Airbyte Cloud and Enterprise deployments.
+    You can find this in Settings > Applications in the Airbyte UI.
+    Leave blank for Airbyte OSS deployments without auth enabled.
 
 Extra (optional)
-    Specify custom proxies in json format.
+    Specify custom proxies in JSON format.
     Following default requests parameters are taken into account:
 
     * ``proxies``
diff --git a/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py b/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py
index 4094b83d316..a30012d8394 100644
--- a/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py
+++ b/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py
@@ -75,12 +75,37 @@ class AirbyteHook(BaseHook):
         return conn_params
 
     def create_api_session(self) -> AirbyteAPI:
-        """Create Airbyte API session."""
-        credentials = SchemeClientCredentials(
-            client_id=self.conn["client_id"],
-            client_secret=self.conn["client_secret"],
-            token_url=self.conn["token_url"],
-        )
+        """
+        Create Airbyte API session.
+
+        When ``client_id`` and ``client_secret`` are provided (via the
+        connection's *Login* and *Password* fields), the SDK authenticates
+        with OAuth2 Client Credentials — required for Airbyte Cloud and
+        Enterprise deployments.
+
+        When credentials are **not** provided, the session is created
+        without authentication.  This is the correct mode for Airbyte OSS
+        deployments that do not have the ``/v1/applications/token``
+        endpoint enabled.
+        """
+        client_id = self.conn["client_id"]
+        client_secret = self.conn["client_secret"]
+
+        security: Security | None = None
+        if client_id and client_secret:
+            credentials = SchemeClientCredentials(
+                client_id=client_id,
+                client_secret=client_secret,
+                token_url=self.conn["token_url"],
+            )
+            security = Security(client_credentials=credentials)
+        else:
+            self.log.info(
+                "No client_id/client_secret provided in connection '%s'. "
+                "Creating Airbyte API session without authentication "
+                "(suitable for Airbyte OSS without auth enabled).",
+                self.airbyte_conn_id,
+            )
 
         client = None
         if self.conn["proxies"]:
@@ -90,7 +115,7 @@ class AirbyteHook(BaseHook):
 
         return AirbyteAPI(
             server_url=self.conn["host"],
-            security=Security(client_credentials=credentials),
+            security=security,
             client=client,
         )
 
@@ -104,8 +129,8 @@ class AirbyteHook(BaseHook):
             ],
             "relabeling": {
                 "host": "Server URL",
-                "login": "Client ID",
-                "password": "Client Secret",
+                "login": "Client ID (optional)",
+                "password": "Client Secret (optional)",
                 "schema": "Token URL",
             },
             "placeholders": {},
diff --git a/providers/airbyte/tests/unit/airbyte/hooks/test_airbyte.py b/providers/airbyte/tests/unit/airbyte/hooks/test_airbyte.py
index f75fc1cb76a..030c5de5bdb 100644
--- a/providers/airbyte/tests/unit/airbyte/hooks/test_airbyte.py
+++ b/providers/airbyte/tests/unit/airbyte/hooks/test_airbyte.py
@@ -36,6 +36,7 @@ class TestAirbyteHook:
     conn_type = "airbyte"
     airbyte_conn_id = "airbyte_conn_id_test"
     airbyte_conn_id_with_proxy = "airbyte_conn_id_test_with_proxy"
+    airbyte_conn_id_with_credentials = "airbyte_conn_id_test_with_credentials"
     connection_id = "conn_test_sync"
     job_id = 1
     host = "http://test-airbyte:8000/public/v1/api/"
@@ -70,6 +71,16 @@ class TestAirbyteHook:
                 extra=self._mock_proxy,
             )
         )
+        create_connection_without_db(
+            Connection(
+                conn_id=self.airbyte_conn_id_with_credentials,
+                conn_type=self.conn_type,
+                host=self.host,
+                port=self.port,
+                login="test-client-id",
+                password="test-client-secret",
+            )
+        )
         self.hook = AirbyteHook(airbyte_conn_id=self.airbyte_conn_id)
         self.hook_with_proxy = AirbyteHook(airbyte_conn_id=self.airbyte_conn_id_with_proxy)
 
@@ -235,6 +246,35 @@ class TestAirbyteHook:
         assert hook.airbyte_api is not None
         assert hook.airbyte_api.sdk_configuration.client.proxies == self._mock_proxy["proxies"]
 
+    def test_create_api_session_without_credentials(self):
+        """Test that a session without OAuth credentials creates an unauthenticated client."""
+        # The default connection (self.airbyte_conn_id) has no login/password
+        hook = AirbyteHook(airbyte_conn_id=self.airbyte_conn_id)
+        assert hook.airbyte_api is not None
+        # SDK should have been created with security=None
+        assert hook.airbyte_api.sdk_configuration.security is None
+
+    def test_create_api_session_with_credentials(self):
+        """Test that a session with OAuth credentials uses client_credentials auth."""
+        hook = AirbyteHook(airbyte_conn_id=self.airbyte_conn_id_with_credentials)
+        assert hook.airbyte_api is not None
+        security = hook.airbyte_api.sdk_configuration.security
+        assert security is not None
+        assert security.client_credentials is not None
+        assert security.client_credentials.client_id == "test-client-id"
+        assert security.client_credentials.client_secret == "test-client-secret"
+
+    @mock.patch("airbyte_api.jobs.Jobs.create_job")
+    def test_submit_sync_connection_without_credentials(self, create_job_mock):
+        """Test that sync submission works without OAuth credentials."""
+        mock_response = mock.Mock()
+        mock_response.job_response = self._mock_sync_conn_success_response_body
+        create_job_mock.return_value = mock_response
+
+        # Use the default hook which has no credentials
+        resp = self.hook.submit_sync_connection(connection_id=self.connection_id)
+        assert resp == self._mock_sync_conn_success_response_body
+
     def test_get_ui_field_behaviour(self):
         """
         Test the UI field behavior configuration for Airbyte connections.
@@ -243,8 +283,8 @@ class TestAirbyteHook:
             "hidden_fields": ["extra", "port"],
             "relabeling": {
                 "host": "Server URL",
-                "login": "Client ID",
-                "password": "Client Secret",
+                "login": "Client ID (optional)",
+                "password": "Client Secret (optional)",
                 "schema": "Token URL",
             },
             "placeholders": {},