Authentication

Airflow 3 uses a pluggable authentication manager. The examples in this section use the Flask App Builder (FAB) auth manager, which is provided by the apache-airflow-providers-fab package.

To use FAB authentication in Airflow 3, install apache-airflow-providers-fab in the Airflow environment and set AIRFLOW__CORE__AUTH_MANAGER to airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager.

The FAB auth manager supports the following authentication methods (however only one method at any time can be used):

The default FAB authentication method is database authentication, which simply keeps user credentials in your database in hashed form. Users can use the Airflow UI to change their passwords without going through an external service.

We have provided sample configuration for each external authentication method in this section. When using the FAB auth manager, external authentication configuration (like the examples given in below sections) goes into a file called webserver_config.py stored in your $AIRFLOW_HOME directory. More details on each authentication method can be found in FAB’s own documentation on security

For each external authentication method, make sure to disable or update any deployment step that creates a default database-authenticated admin user, such as a Helm createUserJob or Docker Compose bootstrap command.

Database

This is the default authentication method. If you do not require some fine tuning or non-default features, it works out of the box and no extra configuration is necessary.

LDAP

from flask_appbuilder.security.manager import AUTH_LDAP

AUTH_TYPE = AUTH_LDAP
AUTH_LDAP_SERVER = "ldaps://ldap.example.com"
# For STARTTLS, use an ldap:// URL and set AUTH_LDAP_USE_TLS = True instead.
AUTH_LDAP_USE_TLS = False

AUTH_LDAP_SEARCH = "ou=users,dc=example,dc=com"
AUTH_LDAP_UID_FIELD = "uid"
AUTH_LDAP_BIND_USER = "cn=admin,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD = "admin-password"

AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH_LDAP_FIRSTNAME_FIELD = "givenName"
AUTH_LDAP_LASTNAME_FIELD = "sn"
AUTH_LDAP_EMAIL_FIELD = "mail"

AUTH_ROLES_MAPPING = {
    "cn=airflow-admins,ou=Groups,dc=example,dc=com": ["Admin"],
    "cn=airflow-users,ou=Groups,dc=example,dc=com": ["User"],
}
AUTH_LDAP_GROUP_FIELD = "memberOf"
AUTH_ROLES_SYNC_AT_LOGIN = True

LDAP service should separately be configured with the memberOf overlay if role mapping depends on the memberOf attribute (see “Reverse Group Membership Maintenance” section in overlays documentation).

OAuth and OpenID Connect

OAuth can support multiple providers at a time. Modern OpenID Connect identity providers, such as Keycloak, Okta, Auth0, and Azure AD, should also be configured as OAuth providers. A full list of example configs for OAuth can be found in the FAB documentation and the Airflow FAB auth manager documentation.

For OpenID Connect providers, make sure Airflow and users’ browsers use the same issuer URL for the identity provider. For example, do not configure Airflow to retrieve metadata from an internal container hostname while browser redirects use a public hostname. If the issuer in the returned tokens does not match the issuer from the provider metadata, the login callback will be rejected.

The OAuth client in the identity provider must also allow the Airflow callback URL as a redirect URI. This is normally https://<airflow-host>/auth/oauth-authorized/<provider-name>, where <provider-name> matches the name field in OAUTH_PROVIDERS. In Airflow deployments that serve the UI through a proxy or load balancer, set AIRFLOW__API__BASE_URL to the browser-facing Airflow URL so redirects are generated with the externally reachable hostname.

from flask_appbuilder.security.manager import AUTH_OAUTH

AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "User"
AUTH_ROLES_SYNC_AT_LOGIN = True

OAUTH_PROVIDERS = [
    {
        "name": "keycloak",
        "icon": "fa-key",
        "token_key": "access_token",
        "remote_app": {
            "client_id": "airflow",
            "client_secret": "client-secret",
            "server_metadata_url": (
                "https://keycloak.example.com/realms/airflow/.well-known/openid-configuration"
            ),
            "api_base_url": "https://keycloak.example.com/realms/airflow/protocol/openid-connect",
            "client_kwargs": {"scope": "openid email profile"},
            "access_token_url": "https://keycloak.example.com/realms/airflow/protocol/openid-connect/token",
            "authorize_url": "https://keycloak.example.com/realms/airflow/protocol/openid-connect/auth",
            "request_token_url": None,
        },
    },
]

OpenID 2.0 authentication with AUTH_OID is not supported by current Flask App Builder versions. If your identity provider supports OpenID Connect, configure it through AUTH_OAUTH as shown above.

Remote User Authentication

Airflow’s remote user authentication takes the username of the user and creates a local FAB account with the role that you specify in the AUTH_USER_REGISTRATION_ROLE setting. Additionally, for remote user authentication to work you need to use a custom middleware. This is because the AUTH_REMOTE_USER setting looks for the username value in the header REMOTE_USER, however if the user makes a request to the Airflow API server with the header REMOTE_USER, this will be converted to HTTP_REMOTE_USER in the request environ dictionary. Thus, you need a CustomMiddleware like in the below example, which will copy the value of HTTP_REMOTE_USER (or any other header you want) to REMOTE_USER to make remote user authentication work.

Only use remote user authentication behind a trusted reverse proxy or SSO gateway that authenticates users, sets the trusted user header, and strips any incoming REMOTE_USER or HTTP_REMOTE_USER headers from client requests. Without that boundary, a client could spoof the header and impersonate another user.

from typing import Any, Callable

from flask import current_app
from flask_appbuilder.const import AUTH_REMOTE_USER

class CustomMiddleware:
    def __init__(self, wsgi_app: Callable) -> None:
        self.wsgi_app = wsgi_app

    def __call__(self, environ: dict, start_response: Callable) -> Any:
        remote_user = environ.get("HTTP_REMOTE_USER")
        if remote_user:
            environ["REMOTE_USER"] = remote_user
        return self.wsgi_app(environ, start_response)


current_app.wsgi_app = CustomMiddleware(current_app.wsgi_app)

AUTH_TYPE = AUTH_REMOTE_USER
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "User"