Skip to content

Instantly share code, notes, and snippets.

@wallyhall
Last active October 28, 2025 23:32
Show Gist options
  • Select an option

  • Save wallyhall/915fedb4dfc766b61f442a32c95e1c29 to your computer and use it in GitHub Desktop.

Select an option

Save wallyhall/915fedb4dfc766b61f442a32c95e1c29 to your computer and use it in GitHub Desktop.
Apache Airflow Azure AAD SSO howto

The following instructions for enabling Azure SSO for Apache Airflow nearly take you all the way - but fall short a couple of details around the configuration of airflow itself:

https://objectpartners.com/2021/12/24/enterprise-auth-for-airflow-azure-ad

All the "Azure" instructions there can be safely followed - the resulting webserver_config.py (which can be injected into a dockerised Airflow in /opt/airflow/webserver_config.py) can be built from the following:

from __future__ import annotations

import os

from airflow.www.fab_security.manager import AUTH_OAUTH
from airflow.www.security import AirflowSecurityManager
from airflow.utils.log.logging_mixin import LoggingMixin

basedir = os.path.abspath(os.path.dirname(__file__))

# Flask-WTF flag for CSRF
WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = None

AUTH_TYPE = AUTH_OAUTH

OAUTH_PROVIDERS = [{
    'name':'Microsoft Azure AD',
    'token_key':'access_token',
    'icon':'fa-windows',
    'remote_app': {
        'api_base_url': "https://login.microsoftonline.com/{}".format(os.getenv("AAD_TENANT_ID")),
        'request_token_url': None,
        'request_token_params': {
            'scope': 'openid email profile'
        },
        'access_token_url': "https://login.microsoftonline.com/{}/oauth2/v2.0/token".format(os.getenv("AAD_TENANT_ID")),
        "access_token_params": {
            'scope': 'openid email profile'
        },
        'authorize_url': "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize".format(os.getenv("AAD_TENANT_ID")),
        "authorize_params": {
            'scope': 'openid email profile'
        },
        'client_id': os.getenv("AAD_CLIENT_ID"),
        'client_secret': os.getenv("AAD_CLIENT_SECRET"),
        'jwks_uri': 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
    }
}]

AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH_USER_REGISTRATION = True
AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_ROLES_MAPPING = {
    "airflow_prod_admin": ["Admin"],
    "airflow_prod_user": ["Op"],
    "airflow_prod_viewer": ["Viewer"]
}

class AzureCustomSecurity(AirflowSecurityManager, LoggingMixin):
    def get_oauth_user_info(self, provider, response=None):
        me = self._azure_jwt_token_parse(response["id_token"])
        return {
            "name": me["name"],
            "email": me["email"],
            "first_name": me["given_name"],
            "last_name": me["family_name"],
            "id": me["oid"],
            "username": me["preferred_username"],
            "role_keys": me["roles"]
        }

# the first of these two appears to work with older Airflow versions, the latter newer.
FAB_SECURITY_MANAGER_CLASS = 'webserver_config.AzureCustomSecurity'
SECURITY_MANAGER_CLASS = AzureCustomSecurity

The above assumes environment variables are configured for the OAuth client secret, etc - and has been tested thoroughly and confirmed working.

Note the roles need to match what you configured in Azure (the example above is using airflow_prod_user etc, in deviation to the linked article above).

@simonbjorzen-ts
Copy link

There appears to be built in support for Azure AD / Entra ID auth.

If you look here, there is already an override defined for Azure:
https://github.com/apache/airflow/blob/be464b48d6b329a2510d312d7ff89f3c01d4e62b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py#L2206

So all that is required in webserver_config.py is the following:

Important to make sure that the provider name is "azure" as this is what determines how the token is handled in override.py.

from __future__ import annotations

import os

from airflow.www.fab_security.manager import AUTH_OAUTH

basedir = os.path.abspath(os.path.dirname(__file__))

AZURE_TENANT_ID = os.environ["AZURE_TENANT_ID"]
AZURE_CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
AZURE_CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]

WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = None

AUTH_TYPE = AUTH_OAUTH

# Auto create users
AUTH_USER_REGISTRATION = True

OAUTH_PROVIDERS = [
    {
        "name": "azure",
        "token_key": "id_token",
        "icon": "fa-windows",
        "remote_app": {
            "api_base_url": f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2",
            "access_token_url": f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/token",
            "authorize_url": f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/authorize",
            "jwks_uri": f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/discovery/v2.0/keys",
            "request_token_url": None,
            "client_id": AZURE_CLIENT_ID,
            "client_secret": AZURE_CLIENT_SECRET,
            "client_kwargs": {
                "scope": "User.read openid email profile",
                "resource": AZURE_CLIENT_ID,
            },
        },
    }
]

AUTH_ROLES_SYNC_AT_LOGIN = True
PERMANENT_SESSION_LIFETIME = 1800

AUTH_ROLES_MAPPING = {
    "AIRFLOW_VIEWER": ["Viewer"],
    "AIRFLOW_USER": ["User"],
    "AIRFLOW_ADMIN": ["Admin"],
}

@felicienveldema
Copy link

Hi all,

Thanks for the insights!
I'm trying to implement this where the client secret is a certificate. The tenant id and client id are as expected. However, injecting the certificate as a client secret doesn't result in the correct response.

Is there an extra step needed to use a certificate as the client secret?

@abhisam
Copy link

abhisam commented May 12, 2025

I used the above web server config, my redirect URL does not have https.
My request URL is as follows: https://portal.data.local/airflow/login/?next=http%3A%2F%2Fportal.data.local%2Fairflow%2Fhome. My app registration is expecting https://portal.data.local/airflow/login/?next=https%3A%2F%2Fportal.data.local%2Fairflow%2Fhome.

I am not sure how to fix that.

my config :

env:
- name: AIRFLOW__LOGGING__FAB_LOGGING_LEVEL
value: DEBUG
- name: AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX
value: "True"
- name: FORWARDED_ALLOW_IPS
value: "*"

@poorleno1
Copy link

@abhisam have you managed to figure out this problem with http redirect?

@pawgajda-drs
Copy link

pawgajda-drs commented Jun 19, 2025

Does it work with Airflow 3.x?

EDIT:
Seems like it does with the following imports:

    from flask_appbuilder.security.manager import AUTH_OAUTH
    from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
    from airflow.utils.log.logging_mixin import LoggingMixin

there is no airflow.www module anymore apparently.

EDIT 2: This may be significant, redirects for OAuth won't work on Airflow 3.x otherwise: apache/airflow#49781

EDIT 3:
To make Oauth work again without adding ProxyFix snippet to your webserver config:

    from flask_appbuilder.security.manager import AUTH_OAUTH
    from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
    from airflow.utils.log.logging_mixin import LoggingMixin
    from airflow.providers.fab.www.extensions.init_wsgi_middlewares import init_wsgi_middleware

and set AIRFLOW__FAB__ENABLE_PROXY_FIX environment variable to True. The ProxyFix was brought back: apache/airflow#49942

@jjlinares
Copy link

Does it work with Airflow 3.x?

EDIT: Seems like it does with the following imports:

    from flask_appbuilder.security.manager import AUTH_OAUTH
    from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
    from airflow.utils.log.logging_mixin import LoggingMixin

there is no airflow.www module anymore apparently.

EDIT 2: This may be significant, redirects for OAuth won't work on Airflow 3.x otherwise: apache/airflow#49781

EDIT 3: To make Oauth work again without adding ProxyFix snippet to your webserver config:

    from flask_appbuilder.security.manager import AUTH_OAUTH
    from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
    from airflow.utils.log.logging_mixin import LoggingMixin
    from airflow.providers.fab.www.extensions.init_wsgi_middlewares import init_wsgi_middleware

and set AIRFLOW__FAB__ENABLE_PROXY_FIX environment variable to True. The ProxyFix was brought back: apache/airflow#49942

This worked. Saved me many hours. Thanks!

@noktang
Copy link

noktang commented Jul 3, 2025

Does anyone know how I can get the REST API working as well?

My users will not have user and password, which doesn't allow me to use a basic auth provider. session backend probably works in the browser because it does some magic with the cookies, but I would like to call the REST API using curl

Hi do you figure out how to use api with sso enabled?

@EbrahimKaram
Copy link

EbrahimKaram commented Oct 6, 2025

Make sure you have
apache-airflow-providers-fab==2.4.4 installed

This worked with Apache airflow 3.1.0
This is what I got to work on my end: this needs to be on webserver_config.py (that would be at the root location of apache home)


from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
from flask_appbuilder.security.manager import AUTH_OAUTH

# Use OAuth
AUTH_TYPE = AUTH_OAUTH

# OAuth provider settings
CLIENT_ID = 
TENANT_ID = 

## References
# https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/auth-manager/index.html?utm_source=chatgpt.com
# https://www.nextlytics.com/blog/implementing-single-sign-on-sso-authentication-in-apache-airflow

# Resources: https://github.com/Azure-Samples/ms-identity-python-webapp/tree/main
# https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/webserver-authentication.html (the most useful and direct)

# Gist Github
# https://gist.github.com/wallyhall/915fedb4dfc766b61f442a32c95e1c29


# what you Replace on the config for this work
# Auth manager
# auth_manager = airflow.api_fastapi.auth.managers.simple.simple_auth_manager.SimpleAuthManager

# airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager


APP_CLIENT_SECRET=CLIENT_SECRET_VALUE = 

CLIENT_SECRET_ID = 

# Redirect URI: https:/AIRFLOWDOMAIN/auth/oauth-authorized/azure
OAUTH_PROVIDERS = [
    {
        'name': 'azure',
        'icon': 'fa-windows',
        'token_key': 'access_token',
        'remote_app': {
            'client_id': CLIENT_ID,
            'client_secret': CLIENT_SECRET_VALUE,
            'api_base_url': 'https://graph.microsoft.com/v1.0/',
            'client_kwargs': {
                'scope': 'User.Read openid email profile',
            },
            "request_token_url": None,
            'authorize_url': f'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize',
            'access_token_url': f'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token',
            'jwks_uri': 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
        },
    }
]


AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH_USER_REGISTRATION = True
AUTH_ROLES_SYNC_AT_LOGIN = True

# Optional: map Microsoft users to FAB roles
# These are the Roles you can have
# https://airflow.apache.org/docs/apache-airflow/3.1.0/security/security_model.html#regular-users
# https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/access-control.html
# Permisions:  Admin, Op, User, Viewer,  Public  (most permissive to least permissive)

AUTH_ROLES_MAPPING = {

    "airflow_prod_admin": ["Admin"],
    "airflow_prod_user": ["Op"],
    "airflow_prod_viewer": ["Viewer"],
    "Admin": ["Admin"],
    "Op": ["Op"],
    "User": ["User"],
    "Viewer": ["Viewer"],
    "Public": ["Public"]

}


class AzureCustomSecurity(FabAirflowSecurityManagerOverride):
    def get_oauth_user_info(self, provider, response=None):
        try:
            me = super().get_oauth_user_info(provider, response)
            self.log.debug("User info from Azure: %s", me)
            print("User info from Azure: %s", me)
        except Exception as e:
            import traceback
            traceback.print_exc()
            self.log.debug(e)
            self.log.debug("The super call failed, trying to continue...")

        return {
            "name": me["first_name"] + " " + me["last_name"],
            "email": me["email"],
            "first_name": me["first_name"],
            "Last_name": me["last_name"],
            "id": me["username"],
            "username": me["email"],
            # "role_keys": me.get("email", ["Viewer"]).split("@")[0].split(".")  # Example: derive roles from email prefix

            "role_keys":["Public"]  
        }


# the first of these two appears to work with older Airflow versions, the latter newer.
FAB_SECURITY_MANAGER_CLASS = 'webserver_config.AzureCustomSecurity'
SECURITY_MANAGER_CLASS = AzureCustomSecurity


@Yatharth0045
Copy link

Yatharth0045 commented Oct 9, 2025

I've used the above configuration, and literally every version shown in this gist but none of them worked. Everytime, it stuck on http error which says this

The redirect URI 'http://<airflow-domain>/auth/oauth-authorized/azure' specified in the request does not match the redirect URIs configured for the application 'XXXXXXXXX'. Make sure the redirect URI sent in the request matches one added to your application in the Azure portal. Navigate to https://aka.ms/redirectUriMismatchError to learn more about how to fix this.

Here, it says that the redirect URI is http but I don't know why because I've configured airflow with https and I'm even accessing the UI with https://<airflow-domain> and its working.

Sharing some of the relevant configuration I already have in my values.yaml for reference

images:
  airflow:
    repository: apache/airflow
    tag: "3.1.0"

## FYI, we are using traefik v2.6 as ingress controller

ingress:
  apiServer:
    enabled: true
    annotations: {}
    path: "/"
    pathType: "ImplementationSpecific"
    hosts: 
    - name: "<airflow-domain>"
      tls:
        enabled: true
        secretName: ""
    ingressClassName: "traefik"

executor: "KubernetesExecutor"

extraEnv: |
  - name: AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX
    value: 'True'

config:
  core:
    auth_manager: "airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager"
  logging:
    fab_logging_level: 'DEBUG'
  fab:
    enable_proxy_fix: 'True'

Also, adding my traefik ingress yaml for reference

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    meta.helm.sh/release-name: airflow-v3-poc
    meta.helm.sh/release-namespace: airflow-v3-poc
  creationTimestamp: "2025-10-09T02:43:40Z"
  generation: 2
  labels:
    app.kubernetes.io/managed-by: Helm
    chart: airflow-1.18.0
    component: airflow-ingress
    heritage: Helm
    release: airflow-v3-poc
    tier: airflow
  name: airflow-v3-poc-ingress
  namespace: airflow-v3-poc
  resourceVersion: "337356144"
  uid: 627dd685-1ae6-4a84-89db-f1633005bada
spec:
  ingressClassName: traefik
  rules:
  - host: <airflow-domain>
    http:
      paths:
      - backend:
          service:
            name: airflow-v3-poc-api-server
            port:
              name: api-server
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - <airflow-domain>

Not sure If I'm missing something here.

@pawgajda-drs
Copy link

pawgajda-drs commented Oct 9, 2025

I've used the above configuration, and literally every version shown in this gist but none of them worked. Everytime, it stuck on http error which says this

The redirect URI 'http://<airflow-domain>/auth/oauth-authorized/azure' specified in the request does not match the redirect URIs configured for the application 'XXXXXXXXX'. Make sure the redirect URI sent in the request matches one added to your application in the Azure portal. Navigate to https://aka.ms/redirectUriMismatchError to learn more about how to fix this.

Here, it says that the redirect URI is http but I don't know why because I've configured airflow with https and I'm even accessing the UI with https://<airflow-domain> and its working.

Sharing some of the relevant configuration I already have in my values.yaml for reference

images:
  airflow:
    repository: apache/airflow
    tag: "3.1.0"

## FYI, we are using traefik v2.6 as ingress controller

ingress:
  apiServer:
    enabled: true
    annotations: {}
    path: "/"
    pathType: "ImplementationSpecific"
    hosts: 
    - name: "<airflow-domain>"
      tls:
        enabled: true
        secretName: ""
    ingressClassName: "traefik"

executor: "KubernetesExecutor"

extraEnv: |
  - name: AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX
    value: 'True'

config:
  core:
    auth_manager: "airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager"
  logging:
    fab_logging_level: 'DEBUG'
  fab:
    enable_proxy_fix: 'True'

Also, adding my traefik ingress yaml for reference

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    meta.helm.sh/release-name: airflow-v3-poc
    meta.helm.sh/release-namespace: airflow-v3-poc
  creationTimestamp: "2025-10-09T02:43:40Z"
  generation: 2
  labels:
    app.kubernetes.io/managed-by: Helm
    chart: airflow-1.18.0
    component: airflow-ingress
    heritage: Helm
    release: airflow-v3-poc
    tier: airflow
  name: airflow-v3-poc-ingress
  namespace: airflow-v3-poc
  resourceVersion: "337356144"
  uid: 627dd685-1ae6-4a84-89db-f1633005bada
spec:
  ingressClassName: traefik
  rules:
  - host: <airflow-domain>
    http:
      paths:
      - backend:
          service:
            name: airflow-v3-poc-api-server
            port:
              name: api-server
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - <airflow-domain>

Not sure If I'm missing something here.

Wrong environment variable, try:

env:
  # Fixes Oauth Issues
  - name: "AIRFLOW__FAB__ENABLE_PROXY_FIX"
    value: "True"

Worked for me with airflow/airflow versions: 3.0.2 and 3.0.6.

EDIT:

Yes, I see you have it set in config but the question is if it works as intended. I personally haven't used config keyword in values.yaml files at all. Everything is in webserver-configmap and then set as Environment Variables.

@Yatharth0045
Copy link

Yatharth0045 commented Oct 9, 2025

Wrong environment variable, try:

env:
  # Fixes Oauth Issues
  - name: "AIRFLOW__FAB__ENABLE_PROXY_FIX"
    value: "True"

Worked for me with airflow/airflow versions: 3.0.2 and 3.0.6.

EDIT:

Yes, I see you have it set in config but the question is if it works as intended. I personally haven't used config keyword in values.yaml files at all. Everything is in webserver-configmap and then set as Environment Variables.

Tried but didn't worked.
Updated env to AIRFLOW__FAB__ENABLE_PROXY_FIX=True
@pawgajda-drs

@pawgajda-drs
Copy link

pawgajda-drs commented Oct 9, 2025

Wrong environment variable, try:

env:
  # Fixes Oauth Issues
  - name: "AIRFLOW__FAB__ENABLE_PROXY_FIX"
    value: "True"

Worked for me with airflow/airflow versions: 3.0.2 and 3.0.6.
EDIT:
Yes, I see you have it set in config but the question is if it works as intended. I personally haven't used config keyword in values.yaml files at all. Everything is in webserver-configmap and then set as Environment Variables.

Tried but didn't worked. Updated env to AIRFLOW__FAB__ENABLE_PROXY_FIX=True @pawgajda-drs

Does your ingress redirect to HTTPS automatically?
Your config on Airflow side seems correct then, given that your Configmap does not have any errors in it.

You can also try this to make sure your Configmap is loaded correctly:

# apiServer
apiServer:
  apiServerConfigConfigMapName: airflow-webserver-config

# Webserver
webserver:
  # this is also needed for apiServer (that replaces Webserver in Airflow 3.x)
  # airflow_api_server_config_configmap_name refers to webserverConfigConfigMapName
  # https://github.com/apache/airflow/blob/main/chart/templates/_helpers.yaml#L607
  webserverConfigConfigMapName: airflow-webserver-config

on the newest stable version of Helm Chart I had issues with loading my config file so after reading the source code I came up with that workaround.

@Yatharth0045
Copy link

Yatharth0045 commented Oct 9, 2025

Does your ingress redirect to HTTPS automatically? Your config on Airflow side seems correct then, given that your Configmap does not have any errors in it.

You can also try this to make sure your Configmap is loaded correctly:

# apiServer
apiServer:
  apiServerConfigConfigMapName: airflow-webserver-config

# Webserver
webserver:
  # this is also needed for apiServer (that replaces Webserver in Airflow 3.x)
  # airflow_api_server_config_configmap_name refers to webserverConfigConfigMapName
  # https://github.com/apache/airflow/blob/main/chart/templates/_helpers.yaml#L607
  webserverConfigConfigMapName: airflow-webserver-config

on the newest stable version of Helm Chart I had issues with loading my config file so after reading the source code I came up with that workaround.

Configmap would be loading as I was able to atleast redirect to azure-sso, and my changes were taking effect, I can say that because I use to change the SSO icon, and that use to work. Also, I'm passing inline config for it.

apiServerConfig: |
    ...
    AUTH_TYPE = AUTH_OAUTH
    ...

I believe it might be an issue with my traefik ingress controller.

It would be great if someone can help me with ingress part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment