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 54259ae1795 Verify Azure AD OAuth id_token signatures by default in
FAB auth manager (#69374)
54259ae1795 is described below
commit 54259ae1795eb460835559af3ecbab35d5e65cad
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Jul 7 11:59:39 2026 +0200
Verify Azure AD OAuth id_token signatures by default in FAB auth manager
(#69374)
* Verify Azure AD OAuth id_token signatures by default in FAB auth manager
The FAB auth manager's Azure AD OAuth provider decoded the incoming id_token
with verify_signature defaulting to False, so the token payload was accepted
without verifying its signature unless a deployment explicitly set the
option.
The Authentik OAuth provider in the same security manager already defaults
verify_signature to True.
Default verify_signature to True for the Azure AD provider, so id_token
signatures are verified against the Microsoft JWKS by default, consistent
with
the Authentik provider, and log a warning when signature verification is
skipped.
Deployments that intentionally relied on the previous default must set
verify_signature: False explicitly in the Azure provider client_kwargs to
keep
the old behaviour.
Generated-by: Claude Opus 4.8 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
* Note Azure AD id_token signature verification default in FAB changelog
Generated-by: Claude Opus 4.8 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
* Add "Authentik" to the docs spelling wordlist
The FAB changelog note references the Authentik OAuth provider by name,
which the spellchecker rejects as an unknown word. Authentik is a proper
product name, so add it to the shared wordlist.
Generated-by: Claude Opus 4.8 (1M context)
---
docs/spelling_wordlist.txt | 1 +
providers/fab/docs/changelog.rst | 7 +++++
.../fab/auth_manager/security_manager/override.py | 3 +-
.../auth_manager/security_manager/test_override.py | 32 ++++++++++++++++++++++
4 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index d1299ad03ce..2b7c13ee660 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -121,6 +121,7 @@ auditable
Auth
auth
authenticator
+Authentik
Authlib
authMechanism
authorised
diff --git a/providers/fab/docs/changelog.rst b/providers/fab/docs/changelog.rst
index cc1093fa143..df7d7d402ed 100644
--- a/providers/fab/docs/changelog.rst
+++ b/providers/fab/docs/changelog.rst
@@ -20,6 +20,13 @@
Changelog
---------
+.. note::
+ The Azure AD OAuth provider in the FAB auth manager now verifies the
``id_token``
+ signature by default: ``verify_signature`` now defaults to ``True``
(previously
+ ``False``), consistent with the Authentik provider. Deployments that
intentionally
+ relied on skipping signature verification must set ``verify_signature:
False``
+ explicitly in the Azure provider ``client_kwargs`` to keep the previous
behaviour.
+
3.7.2
.....
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
index 9c85c2e0151..41ab5a9faac 100644
---
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
+++
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
@@ -2424,7 +2424,7 @@ class
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
return requests.get(MICROSOFT_KEY_SET_URL, timeout=30).json()
def _decode_and_validate_azure_jwt(self, id_token: str) -> dict[str, str]:
- verify_signature =
self.oauth_remotes["azure"].client_kwargs.get("verify_signature", False)
+ verify_signature =
self.oauth_remotes["azure"].client_kwargs.get("verify_signature", True)
if verify_signature:
from authlib.jose import JsonWebKey, jwt as authlib_jwt
@@ -2433,6 +2433,7 @@ class
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
claims.validate()
return claims
+ log.warning("JWT token is not validated!")
_parts = id_token.split(".")
_payload = _parts[1] + "=" * (-len(_parts[1]) % 4)
return json.loads(base64.urlsafe_b64decode(_payload))
diff --git
a/providers/fab/tests/unit/fab/auth_manager/security_manager/test_override.py
b/providers/fab/tests/unit/fab/auth_manager/security_manager/test_override.py
index 76849374fe7..924ec22f6ab 100644
---
a/providers/fab/tests/unit/fab/auth_manager/security_manager/test_override.py
+++
b/providers/fab/tests/unit/fab/auth_manager/security_manager/test_override.py
@@ -472,6 +472,38 @@ class TestFabAirflowSecurityManagerOverride:
assert user_info["email"] == "[email protected]"
assert user_info["role_keys"] == ["admin-group", "viewer-group"]
+ def test_decode_and_validate_azure_jwt_verifies_signature_by_default(self):
+ """Azure AD id_token signatures are verified by default
(verify_signature defaults to True)."""
+ sm = EmptySecurityManager()
+ # client_kwargs does not set verify_signature -> it must default to
verifying
+ sm.oauth_remotes = {"azure": Mock(client_kwargs={})}
+
+ with mock.patch.object(
+ EmptySecurityManager, "_get_microsoft_jwks",
side_effect=RuntimeError("verify-branch-reached")
+ ) as mock_jwks:
+ with pytest.raises(RuntimeError, match="verify-branch-reached"):
+ sm._decode_and_validate_azure_jwt("header.payload.signature")
+
+ # entering the verifying branch means the Microsoft JWKS were fetched
+ mock_jwks.assert_called_once()
+
+ def
test_decode_and_validate_azure_jwt_skips_verification_when_opted_out(self):
+ """With verify_signature explicitly False, the token is decoded
without signature verification."""
+ import base64
+ import json as _json
+
+ payload = base64.urlsafe_b64encode(_json.dumps({"oid":
"user-1"}).encode()).decode().rstrip("=")
+ id_token = f"header.{payload}.signature"
+
+ sm = EmptySecurityManager()
+ sm.oauth_remotes = {"azure": Mock(client_kwargs={"verify_signature":
False})}
+
+ with mock.patch.object(EmptySecurityManager, "_get_microsoft_jwks") as
mock_jwks:
+ result = sm._decode_and_validate_azure_jwt(id_token)
+
+ mock_jwks.assert_not_called()
+ assert result == {"oid": "user-1"}
+
def test_ldap_search_escapes_username_and_validates_filter():
"""Test that LDAP search properly escapes username and validates search
filter."""