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):