Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions airflow-core/tests/unit/api_fastapi/auth/test_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,12 @@ def test_revoke_token_persists_in_db(self):
}
token = jwt.encode(payload, "secret", algorithm="HS256")

validator = JWTValidator(secret_key="secret", audience="test", algorithm=["HS256"], leeway=0)
# Pass issuer=None explicitly so the validator does not pick up the
# process-wide test-env default `[api_auth] jwt_issuer` and demand an
# `iss` claim that the synthetic tokens below intentionally omit.
validator = JWTValidator(
secret_key="secret", audience="test", algorithm=["HS256"], leeway=0, issuer=None
)
validator.revoke_token(token)

assert RevokedToken.is_revoked("revoke-test-jti") is True
Expand All @@ -338,7 +343,12 @@ def test_revoke_token_without_jti_does_not_persist(self):
payload = {"sub": "user", "exp": now + 3600, "iat": now, "nbf": now, "aud": "test"}
token = jwt.encode(payload, "secret", algorithm="HS256")

validator = JWTValidator(secret_key="secret", audience="test", algorithm=["HS256"], leeway=0)
# Pass issuer=None explicitly so the validator does not pick up the
# process-wide test-env default `[api_auth] jwt_issuer` and demand an
# `iss` claim that the synthetic tokens below intentionally omit.
validator = JWTValidator(
secret_key="secret", audience="test", algorithm=["HS256"], leeway=0, issuer=None
)
validator.revoke_token(token)

assert RevokedToken.is_revoked("any-jti") is False
Expand All @@ -347,7 +357,12 @@ def test_revoke_token_with_invalid_token_does_not_raise(self):
"""Test that revoke_token logs a warning instead of raising for an invalid token."""
from airflow.models.revoked_token import RevokedToken

validator = JWTValidator(secret_key="secret", audience="test", algorithm=["HS256"], leeway=0)
# Pass issuer=None explicitly so the validator does not pick up the
# process-wide test-env default `[api_auth] jwt_issuer` and demand an
# `iss` claim that the synthetic tokens below intentionally omit.
validator = JWTValidator(
secret_key="secret", audience="test", algorithm=["HS256"], leeway=0, issuer=None
)
validator.revoke_token("invalid-token")

assert RevokedToken.is_revoked("any-jti") is False
Expand All @@ -370,7 +385,12 @@ def test_revoke_token_with_db_error_does_not_raise(self):
}
token = jwt.encode(payload, "secret", algorithm="HS256")

validator = JWTValidator(secret_key="secret", audience="test", algorithm=["HS256"], leeway=0)
# Pass issuer=None explicitly so the validator does not pick up the
# process-wide test-env default `[api_auth] jwt_issuer` and demand an
# `iss` claim that the synthetic tokens below intentionally omit.
validator = JWTValidator(
secret_key="secret", audience="test", algorithm=["HS256"], leeway=0, issuer=None
)
with patch(
"airflow.models.revoked_token.RevokedToken.revoke", side_effect=SQLAlchemyError("db down")
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,20 @@ def logout_client(self):
def test_logout_revokes_token(self, logout_client):
"""Test that logout revokes the JWT token and persists it in the database."""
now = int(time.time())
auth_manager = logout_client.app.state.auth_manager
signer = auth_manager._get_token_signer()
token_payload = {
"sub": "admin",
"jti": "test-jti-123",
"exp": now + 3600,
"iat": now,
"nbf": now,
"aud": "apache-airflow",
# Include the signer's configured issuer so the validator (which
# reads `[api_auth] jwt_issuer` from the same config) does not
# reject the synthetic token for missing iss.
"iss": signer.issuer,
}
auth_manager = logout_client.app.state.auth_manager
signer = auth_manager._get_token_signer()
token_str = jwt.encode(token_payload, signer._secret_key, algorithm=signer.algorithm)

logout_client.cookies.set(COOKIE_NAME_JWT_TOKEN, token_str)
Expand Down
9 changes: 9 additions & 0 deletions devel-common/src/tests_common/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@ def mock_plugins_manager_for_all_non_db_tests():
os.environ["AIRFLOW__CORE__UNIT_TEST_MODE"] = "True"
os.environ["AWS_DEFAULT_REGION"] = os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
os.environ["CREDENTIALS_DIR"] = os.environ.get("CREDENTIALS_DIR") or "/files/airflow-breeze-config/keys"
# PyJWT 2.12.0 (2026-03-12) added strict type validation that rejects iss=None.
# Current main's airflow-core deletes iss from the claims when the configured
# `[api_auth] jwt_issuer` is falsy (commit a440d1db93, 2026-01-31), but the
# `Compat 3.0.x` matrix tests install older airflow-core releases (e.g. 3.0.6,
# 2025-08-25) that predate that fix. Setting a default test issuer here keeps
# every JWT-generating test path safe across all supported airflow-core versions
# without leaking the upper bound to user-facing dependencies. Tests that need
# to override this still can via `conf_vars(...)`.
os.environ.setdefault("AIRFLOW__API_AUTH__JWT_ISSUER", "test-airflow-issuer")


@pytest.fixture
Expand Down
1 change: 1 addition & 0 deletions providers/fab/docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ PIP package Version required
``blinker`` ``>=1.6.2``
``flask`` ``>=2.2.1``
``flask-appbuilder`` ``==5.2.0``
``pyjwt`` ``>=2.11.0``
``flask-login`` ``>=0.6.2; python_version < "3.14"``
``flask-login`` ``>=0.6.3; python_version >= "3.14"``
``flask-session`` ``>=0.8.0``
Expand Down
6 changes: 6 additions & 0 deletions providers/fab/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ dependencies = [
# `airflow/providers/fab/auth_manager/security_manager/override.py` with their upstream counterparts.
# In particular, make sure any breaking changes, for example any new methods, are accounted for.
"flask-appbuilder==5.2.0", # Whenever updating the version, run test_fab_alignment.py to verify.
# Transitive via flask-appbuilder -> flask-jwt-extended; pinned here so the FAB
# provider keeps installing cleanly when paired with older airflow-core releases
# (the compat-3.0.6 matrix job) whose own pyjwt floor predates `jwt.types.Options`
# (added in PyJWT 2.11.0). Without this, `from jwt.types import Options` in
# `flask_jwt_extended.tokens` raises ImportError at module import time.
"pyjwt>=2.11.0",
"flask-login>=0.6.2; python_version < '3.14'",
"flask-login>=0.6.3; python_version >= '3.14'",
"flask-session>=0.8.0",
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading