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

potiuk 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 ed26436f2a1 Unify jwt_audience config key for signer and validator 
(#67494)
ed26436f2a1 is described below

commit ed26436f2a1f7cc9ef4578f144010fed881833aa
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue May 26 00:07:56 2026 +0200

    Unify jwt_audience config key for signer and validator (#67494)
    
    * Unify jwt_audience config key for signer and validator
    
    The JWT signer in BaseAuthManager._get_token_signer() read `jwt_audience`
    from the `[api]` section while the validator in _get_token_validator()
    read from `[api_auth]` — the documented option (only `[api_auth]
    jwt_audience` is declared in config.yml; `[api] jwt_audience` is not
    documented anywhere).
    
    Both defaults are `apache-airflow` so out-of-box behaviour is correct,
    but a deployment that sets a custom audience under the documented
    `[api_auth]` section would have its tokens signed with the default
    "apache-airflow" while the validator looks for the configured audience,
    silently rejecting every token.
    
    Switch the signer to read from `[api_auth] jwt_audience` (the documented
    section). The undocumented `[api] jwt_audience` setting was never part of
    the schema, so removing it does not constitute a backwards-incompatible
    change for any documented configuration.
    
    * Honour legacy [api] jwt_audience with deprecation warning
    
    Keep accepting jwt_audience from the undocumented [api] section that
    the buggy signer used to read, so deployments that worked around the
    original bug by setting it there continue to function. Emit a
    DeprecationWarning steering operators to the documented [api_auth]
    location, and add a newsfragment explaining the bug and the migration
    path for each configuration shape ([api_auth] only, [api] only, both,
    neither).
---
 airflow-core/newsfragments/67494.significant.rst   | 43 +++++++++++++
 .../api_fastapi/auth/managers/base_auth_manager.py | 36 ++++++++++-
 .../auth/managers/test_base_auth_manager.py        | 71 ++++++++++++++++++++++
 3 files changed, 148 insertions(+), 2 deletions(-)

diff --git a/airflow-core/newsfragments/67494.significant.rst 
b/airflow-core/newsfragments/67494.significant.rst
new file mode 100644
index 00000000000..93c56980e5e
--- /dev/null
+++ b/airflow-core/newsfragments/67494.significant.rst
@@ -0,0 +1,43 @@
+Fix ``jwt_audience`` for the public API being read from two different config 
sections
+
+Earlier 3.x releases contained a bug in ``BaseAuthManager``: the JWT signer 
read
+``jwt_audience`` from the ``[api]`` section while the validator read it from 
the documented
+``[api_auth]`` section. Only ``[api_auth] jwt_audience`` is declared in 
``config.yml``;
+``[api] jwt_audience`` was never part of the schema. Default deployments were 
unaffected
+because both sides fell back to ``apache-airflow``, but a deployment that set 
a custom
+audience under the documented ``[api_auth]`` location had its tokens signed 
with the
+default value and silently rejected by the validator. Some operators worked 
around the
+bug by setting the audience under the undocumented ``[api]`` section instead, 
which made
+the signer emit the configured value but left the validator on the default.
+
+Both the signer and validator now read ``jwt_audience`` from ``[api_auth]``.
+
+**What you should do:**
+
+- If you set ``[api_auth] jwt_audience`` (env 
``AIRFLOW__API_AUTH__JWT_AUDIENCE``):
+  nothing — this is the documented location and continues to work.
+- If you worked around the original bug by setting ``[api] jwt_audience`` (env
+  ``AIRFLOW__API__JWT_AUDIENCE``): move the value to ``[api_auth] 
jwt_audience``
+  (env ``AIRFLOW__API_AUTH__JWT_AUDIENCE``). The value under ``[api]`` is still
+  honoured for backwards compatibility and emits a ``DeprecationWarning``; 
support
+  for it will be removed in a future release.
+- If you set both: the documented ``[api_auth]`` value wins; remove the 
``[api]``
+  one to silence the deprecation warning.
+
+**Behaviour changes:**
+
+- Deployments that set ``[api_auth] jwt_audience`` will now also have their 
tokens
+  signed with that value (previously only validated against it). If signer and
+  validator were on different audiences because of this bug, expect tokens
+  generated after upgrade to be accepted again.
+
+* Types of change
+
+  * [ ] Dag changes
+  * [x] Config changes
+  * [ ] API changes
+  * [ ] CLI changes
+  * [x] Behaviour changes
+  * [ ] Plugin changes
+  * [ ] Dependency changes
+  * [ ] Code interface changes
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py 
b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
index 4bd49cccf47..4ba08e5b447 100644
--- a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
+++ b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
@@ -18,6 +18,7 @@
 from __future__ import annotations
 
 import logging
+import warnings
 from abc import ABCMeta, abstractmethod
 from collections import defaultdict
 from enum import Enum
@@ -832,6 +833,37 @@ class BaseAuthManager(Generic[T], LoggingMixin, 
metaclass=ABCMeta):
         """
         return None
 
+    @staticmethod
+    def _get_jwt_audience() -> str:
+        """
+        Resolve the JWT audience from the documented ``[api_auth] 
jwt_audience`` option.
+
+        Falls back to the undocumented ``[api] jwt_audience`` location used by 
the signer in
+        earlier 3.x releases (with a deprecation warning) so deployments that 
set the wrong
+        section continue to work until they migrate. Returns the default 
``apache-airflow``
+        when neither is configured.
+
+        :meta private:
+        """
+        if conf.has_option("api_auth", "jwt_audience"):
+            return conf.get("api_auth", "jwt_audience")
+        if conf.has_option("api", "jwt_audience"):
+            # Bug context in PR https://github.com/apache/airflow/pull/67494: 
the signer used to
+            # read `[api] jwt_audience` while the validator read `[api_auth] 
jwt_audience`, so
+            # any deployment that hit the bug set the value under `[api]`. 
Honour it with a
+            # deprecation warning until the fallback can be removed in a 
future major release.
+            warnings.warn(
+                "The `[api] jwt_audience` configuration option is deprecated 
and was never "
+                "documented. It was read only by the JWT signer due to a bug; 
the validator "
+                "always read `[api_auth] jwt_audience`. Move the value to 
`[api_auth] "
+                "jwt_audience` (env var `AIRFLOW__API_AUTH__JWT_AUDIENCE`). 
Support for the "
+                "`[api]` location will be removed in a future release.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+            return conf.get("api", "jwt_audience")
+        return "apache-airflow"
+
     @classmethod
     @cache
     def _get_token_signer(
@@ -848,7 +880,7 @@ class BaseAuthManager(Generic[T], LoggingMixin, 
metaclass=ABCMeta):
         return JWTGenerator(
             **get_signing_args(),
             valid_for=expiration_time_in_seconds,
-            audience=conf.get("api", "jwt_audience", 
fallback="apache-airflow"),
+            audience=cls._get_jwt_audience(),
         )
 
     @classmethod
@@ -862,5 +894,5 @@ class BaseAuthManager(Generic[T], LoggingMixin, 
metaclass=ABCMeta):
         return JWTValidator(
             **get_sig_validation_args(),
             leeway=conf.getint("api_auth", "jwt_leeway"),
-            audience=conf.get("api_auth", "jwt_audience", 
fallback="apache-airflow"),
+            audience=cls._get_jwt_audience(),
         )
diff --git 
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py 
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
index d82b9009c49..18898bddd2d 100644
--- 
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
+++ 
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
@@ -16,6 +16,7 @@
 # under the License.
 from __future__ import annotations
 
+import warnings
 from typing import TYPE_CHECKING, Any
 from unittest.mock import AsyncMock, MagicMock, Mock, patch
 
@@ -290,6 +291,76 @@ class TestBaseAuthManager:
 
         validator.revoke_token.assert_called_once_with(token)
 
+    @patch(
+        "airflow.api_fastapi.auth.managers.base_auth_manager.get_signing_args",
+        return_value={"secret_key": "k", "algorithm": "HS256"},
+    )
+    @patch("airflow.api_fastapi.auth.managers.base_auth_manager.JWTGenerator", 
autospec=True)
+    def test_token_signer_reads_audience_from_api_auth_section(
+        self, mock_jwt_generator, mock_get_signing_args, auth_manager
+    ):
+        """Signer and validator must read `jwt_audience` from the same 
`[api_auth]` section.
+
+        Regression test: the signer previously read `[api] jwt_audience` while 
the validator read
+        `[api_auth] jwt_audience` (the documented option). Both defaults are 
`apache-airflow` so
+        out-of-box behaviour was correct, but a custom audience set under the 
documented
+        `[api_auth]` section would silently mismatch.
+        """
+        EmptyAuthManager._get_token_signer.cache_clear()
+        try:
+            with conf_vars({("api_auth", "jwt_audience"): 
"configured-audience"}):
+                auth_manager._get_token_signer()
+        finally:
+            EmptyAuthManager._get_token_signer.cache_clear()
+        assert mock_jwt_generator.call_args.kwargs["audience"] == 
"configured-audience"
+
+    @patch(
+        "airflow.api_fastapi.auth.managers.base_auth_manager.get_signing_args",
+        return_value={"secret_key": "k", "algorithm": "HS256"},
+    )
+    @patch("airflow.api_fastapi.auth.managers.base_auth_manager.JWTGenerator", 
autospec=True)
+    def test_token_signer_falls_back_to_deprecated_api_section_with_warning(
+        self, mock_jwt_generator, mock_get_signing_args, auth_manager
+    ):
+        """Honour an audience set under the (deprecated) ``[api]`` section 
with a warning.
+
+        Deployments that hit the original bug worked around it by setting 
``[api] jwt_audience``
+        so the signer would emit the configured value. Keep accepting that 
until the next major
+        release, but emit ``DeprecationWarning`` so operators move it to 
``[api_auth]``.
+        """
+        EmptyAuthManager._get_token_signer.cache_clear()
+        try:
+            with conf_vars({("api", "jwt_audience"): "legacy-audience"}):
+                with pytest.warns(DeprecationWarning, match=r"\[api\] 
jwt_audience"):
+                    auth_manager._get_token_signer()
+        finally:
+            EmptyAuthManager._get_token_signer.cache_clear()
+        assert mock_jwt_generator.call_args.kwargs["audience"] == 
"legacy-audience"
+
+    @patch(
+        "airflow.api_fastapi.auth.managers.base_auth_manager.get_signing_args",
+        return_value={"secret_key": "k", "algorithm": "HS256"},
+    )
+    @patch("airflow.api_fastapi.auth.managers.base_auth_manager.JWTGenerator", 
autospec=True)
+    def test_token_signer_prefers_api_auth_over_deprecated_api_section(
+        self, mock_jwt_generator, mock_get_signing_args, auth_manager
+    ):
+        """When both sections are set, the documented ``[api_auth]`` option 
wins (no warning)."""
+        EmptyAuthManager._get_token_signer.cache_clear()
+        try:
+            with conf_vars(
+                {
+                    ("api_auth", "jwt_audience"): "documented-audience",
+                    ("api", "jwt_audience"): "legacy-audience",
+                }
+            ):
+                with warnings.catch_warnings():
+                    warnings.simplefilter("error", DeprecationWarning)
+                    auth_manager._get_token_signer()
+        finally:
+            EmptyAuthManager._get_token_signer.cache_clear()
+        assert mock_jwt_generator.call_args.kwargs["audience"] == 
"documented-audience"
+
     @patch("airflow.api_fastapi.auth.managers.base_auth_manager.JWTGenerator", 
autospec=True)
     @patch.object(EmptyAuthManager, "serialize_user")
     def test_generate_jwt_token(self, mock_serialize_user, mock_jwt_generator, 
auth_manager):

Reply via email to