Skip to content
Merged
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
59 changes: 59 additions & 0 deletions providers/fab/docs/auth-manager/sso.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,65 @@ Provider Examples
For Azure app registration and OAuth setup, see :doc:`apache-airflow-providers-microsoft-azure:connections/azure`
and the `Azure OAuth2 documentation <https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow>`_.

**Azure AD with Group-Based Authorization**

.. code-block:: python

from flask_appbuilder.security.manager import AUTH_OAUTH

AUTH_TYPE = AUTH_OAUTH

AUTH_OAUTH_ROLE_KEYS = {
"azure": "groups",
}

OAUTH_PROVIDERS = [
{
"name": "azure",
"token_key": "access_token",
"icon": "fa-windows",
"remote_app": {
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"api_base_url": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"client_kwargs": {
"scope": "openid email profile groups",
"resource": "your-client-id",
"verify_signature": True,
},
"request_token_url": None,
"access_token_url": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token",
"authorize_url": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize",
},
}
]

AUTH_ROLES_MAPPING = {
"airflow-admin-group": ["Admin"],
"airflow-op-group": ["Op"],
"airflow-user-group": ["User"],
"airflow-viewer-group": ["Viewer"],
}

AUTH_ROLES_SYNC_AT_LOGIN = True

AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Viewer"

.. note::
When using Azure AD groups:

- Ensure the ``groups`` scope is included in ``client_kwargs``
- Configure group claims in your Azure app registration
- The ``AUTH_OAUTH_ROLE_KEYS`` setting allows you to specify which claim field
contains the authorization information (``roles`` or ``groups``)
- Group names from Azure AD will be matched against ``AUTH_ROLES_MAPPING``

.. important::
The ``AUTH_OAUTH_ROLE_KEYS`` configuration is provider-specific. For Azure,
you can set it to ``"roles"`` (default) or ``"groups"`` depending on your
Azure AD setup. Other OAuth providers may use different field names.

**Google OAuth2**

.. code-block:: bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2145,12 +2145,13 @@ def get_oauth_user_info(self, provider: str, resp: dict[str, Any]) -> dict[str,
me = self._decode_and_validate_azure_jwt(resp["id_token"])
log.debug("User info from Azure: %s", me)
# https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference#payload-claims
role_key = current_app.config.get("AUTH_OAUTH_ROLE_KEYS", {}).get("azure", "roles")
return {
"email": me["email"] if "email" in me else me["upn"],
"first_name": me.get("given_name", ""),
"last_name": me.get("family_name", ""),
"username": me["oid"],
"role_keys": me.get("roles", []),
"role_keys": me.get(role_key, []),
}
# for OpenShift
if provider == "openshift":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ def test_check_password_not_match(self, check_password):
"role_keys": ["admin"],
},
),
(
"azure",
{
"oid": "test",
"given_name": "John",
"family_name": "Doe",
"email": "test@example.com",
"groups": ["group1", "group2"],
},
{
"username": "test",
"first_name": "John",
"last_name": "Doe",
"email": "test@example.com",
"role_keys": [],
},
),
("openshift", {"metadata": {"name": "test"}}, {"username": "openshift_test"}),
(
"okta",
Expand Down Expand Up @@ -234,13 +251,43 @@ def test_check_password_not_match(self, check_password):
],
)
def test_get_oauth_user_info(self, provider, resp, user_info):
sm = EmptySecurityManager()
sm.appbuilder = Mock(sm=sm)
sm.oauth_remotes = {}
sm.oauth_remotes[provider] = Mock(
get=Mock(return_value=Mock(json=Mock(return_value=resp))),
userinfo=Mock(return_value=resp),
)
sm._decode_and_validate_azure_jwt = Mock(return_value=resp)
sm._get_authentik_token_info = Mock(return_value=resp)
assert sm.get_oauth_user_info(provider, {"id_token": None}) == user_info
from flask import Flask

app = Flask(__name__)
with app.app_context():
sm = EmptySecurityManager()
sm.appbuilder = Mock(sm=sm)
sm.oauth_remotes = {}
sm.oauth_remotes[provider] = Mock(
get=Mock(return_value=Mock(json=Mock(return_value=resp))),
userinfo=Mock(return_value=resp),
)
sm._decode_and_validate_azure_jwt = Mock(return_value=resp)
sm._get_authentik_token_info = Mock(return_value=resp)
assert sm.get_oauth_user_info(provider, {"id_token": None}) == user_info

def test_get_oauth_user_info_azure_with_groups_config(self):
from flask import Flask

app = Flask(__name__)
app.config["AUTH_OAUTH_ROLE_KEYS"] = {"azure": "groups"}

azure_response = {
"oid": "user-123",
"given_name": "Jane",
"family_name": "Smith",
"email": "jane.smith@example.com",
"groups": ["admin-group", "viewer-group"],
}

with app.app_context():
sm = EmptySecurityManager()
sm.appbuilder = Mock(sm=sm)
sm.oauth_remotes = {}
sm._decode_and_validate_azure_jwt = Mock(return_value=azure_response)

user_info = sm.get_oauth_user_info("azure", {"id_token": "test-token"})

assert user_info["username"] == "user-123"
assert user_info["email"] == "jane.smith@example.com"
assert user_info["role_keys"] == ["admin-group", "viewer-group"]