diff --git a/providers/fab/docs/auth-manager/webserver-authentication.rst b/providers/fab/docs/auth-manager/webserver-authentication.rst index 7ecc40ab83c42..6f865ba777fa0 100644 --- a/providers/fab/docs/auth-manager/webserver-authentication.rst +++ b/providers/fab/docs/auth-manager/webserver-authentication.rst @@ -57,9 +57,10 @@ Other Methods ''''''''''''' A ``webserver_config.py`` configuration file is automatically generated and can be used to configure FAB auth manager to support authentication -methods like OAuth, OpenID, LDAP, REMOTE_USER. It should be noted that due to the limitation of Flask AppBuilder -and Authlib, only a selection of OAuth2 providers is supported. This list includes ``github``, ``githublocal``, ``twitter``, -``linkedin``, ``google``, ``azure``, ``openshift``, ``okta``, ``keycloak`` and ``keycloak_before_17``. +methods like OAuth, OpenID, LDAP and REMOTE_USER. It should be noted that due to the limitation of Flask AppBuilder +and Authlib, some OAuth2 providers may not be supported. Currently supported providers include ``github``, ``githublocal``, ``twitter``, +``linkedin``, ``google``, ``azure``, ``openshift``, ``okta``, ``auth0``, ``keycloak``, ``keycloak_before_17`` and ``authentik``. +If your provider is not on the list, you may need to adjust the ``remote_app`` configuration to match your provider's OAuth2 specification. By default, the following entry in the ``$AIRFLOW_HOME/webserver_config.py`` is used. 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 3e63f7053f40e..e86a1a10f79c3 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 @@ -2132,8 +2132,16 @@ def get_oauth_user_info(self, provider: str, resp: dict[str, Any]) -> dict[str, "username": me["nickname"], "role_keys": me.get("groups", []), } - - return {} + # for other providers + data = self.oauth_remotes[provider].userinfo() + log.debug("User info from %s: %s", provider, data) + return { + "username": data.get("preferred_username", ""), + "first_name": data.get("given_name", ""), + "last_name": data.get("family_name", ""), + "email": data.get("email", ""), + "role_keys": data.get("groups", []), + } @staticmethod def oauth_token_getter(): 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 0a4132851718e..7b9e9696e5776 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 @@ -19,6 +19,8 @@ from unittest import mock from unittest.mock import Mock +import pytest + from tests_common.test_utils.compat import ignore_provider_compatibility_error with ignore_provider_compatibility_error("2.9.0+", __file__): @@ -75,3 +77,173 @@ def test_check_password_not_match(self, check_password): sm.find_user = Mock(return_value=mock_user) check_password.return_value = False assert not sm.check_password("test_user", "test_password") + + @pytest.mark.parametrize( + "provider, resp, user_info", + [ + ("github", {"login": "test"}, {"username": "github_test"}), + ("githublocal", {"login": "test"}, {"username": "github_test"}), + ("twitter", {"screen_name": "test"}, {"username": "twitter_test"}), + ( + "linkedin", + {"id": "test", "firstName": "John", "lastName": "Doe", "email-address": "test@example.com"}, + { + "username": "linkedin_test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + }, + ), + ( + "google", + {"id": "test", "given_name": "John", "family_name": "Doe", "email": "test@example.com"}, + { + "username": "google_test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + }, + ), + ( + "azure", + { + "oid": "test", + "given_name": "John", + "family_name": "Doe", + "email": "test@example.com", + "roles": ["admin"], + }, + { + "username": "test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ( + "azure", + { + "oid": "test", + "given_name": "John", + "family_name": "Doe", + "upn": "test@example.com", + "roles": ["admin"], + }, + { + "username": "test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ("openshift", {"metadata": {"name": "test"}}, {"username": "openshift_test"}), + ( + "okta", + { + "sub": "test", + "given_name": "John", + "family_name": "Doe", + "email": "test@example.com", + "groups": ["admin"], + }, + { + "username": "okta_test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ("okta", {"error": "access_denied", "error_description": "Invalid bearer token."}, {}), + ( + "auth0", + { + "sub": "test", + "given_name": "John", + "family_name": "Doe", + "email": "test@example.com", + "groups": ["admin"], + }, + { + "username": "auth0_test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ( + "keycloak", + { + "preferred_username": "test", + "given_name": "John", + "family_name": "Doe", + "email": "test@example.com", + "groups": ["admin"], + }, + { + "username": "test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ( + "keycloak_before_17", + { + "preferred_username": "test", + "given_name": "John", + "family_name": "Doe", + "email": "test@example.com", + "groups": ["admin"], + }, + { + "username": "test", + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ( + "authentik", + { + "nickname": "test", + "given_name": "John", + "preferred_username": "test@example.com", + "groups": ["admin"], + }, + { + "username": "test", + "first_name": "John", + "email": "test@example.com", + "role_keys": ["admin"], + }, + ), + ( + "other", + {"preferred_username": "test", "email": "test@example.com"}, + { + "username": "test", + "first_name": "", + "last_name": "", + "email": "test@example.com", + "role_keys": [], + }, + ), + ], + ) + 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