Skip to main content

Auth0 SSO

Superset uses Flask-AppBuilder for login. This setup turns on OAuth, adds an Auth0 button on the login page, and maps Auth0 roles onto Superset roles at each login.

Roles are read from a custom claim on the ID token (https://superset.org/roles). Auth0 does not put that claim on /userinfo, so the security manager decodes the ID token after Auth0 returns it.

1. Create the Auth0 application​

In the Auth0 dashboard, create a Regular Web Application.

SettingValue
Allowed Callback URLshttps://<your-superset-host>/oauth-authorized/auth0
Allowed Logout URLshttps://<your-superset-host>/logout/
Allowed Web Originshttps://<your-superset-host>

For a local install, use http://localhost:8088 as the host. The path /oauth-authorized/auth0 must match the provider name in OAUTH_PROVIDERS (auth0).

Copy the Domain, Client ID, and Client Secret. You will paste them into superset_config.py.

2. Add roles to the ID token​

Create an Auth0 role for each Superset role you want to assign. Prefix every role with superset_:

Auth0 roleSuperset role
superset_AdminAdmin
superset_AlphaAlpha
superset_GammaGamma
superset_sql_labsql_lab

The Superset role must already exist. The mapper lowercases the name after the prefix, so superset_Admin matches the built-in Admin role.

Assign those roles to users, then add a Post Login Action that copies them onto the ID token:

exports.onExecutePostLogin = async (event, api) => {
const namespace = "https://superset.org";
const roles = event.authorization?.roles || [];
api.idToken.setCustomClaim(`${namespace}/roles`, roles);
};

Deploy the Action and attach it to the Login flow.

3. Install dependencies in the image​

The security manager imports Authlib, requests, and PyJWT. A Postgres metadata database also needs psycopg2. The official image does not always include those packages in /app/.venv, so add Dockerfile.auth in the Superset docker directory:

FROM apachesuperset.docker.scarf.sh/apache/superset:6.1.0

USER root

RUN /app/.venv/bin/python -m ensurepip \
&& /app/.venv/bin/python -m pip install --no-cache-dir \
Authlib requests psycopg2-binary PyJWT \
&& /app/.venv/bin/python -c \
"import authlib, requests, psycopg2, jwt; print('Dependencies OK')"

USER superset

PyJWT is on that install line because oauth_user_info does import jwt. Build the image from the directory that contains Dockerfile.auth:

docker build -f Dockerfile.auth -t superset-auth:6.1.0 .

Use superset-auth:6.1.0 for every service that runs the Superset image (the app, init, worker, and beat). Those processes all load superset_config.py.

On a non-Docker install, install the same packages into the virtualenv that runs Superset:

pip install Authlib requests psycopg2-binary PyJWT

4. Configure superset_config.py​

Add the following to superset_config.py. Replace YOUR_AUTH0_DOMAIN, YOUR_AUTH0_CLIENT_ID, and YOUR_AUTH0_CLIENT_SECRET with the values from the Auth0 application.

AUTH_ROLES_MAPPING must be a non-empty dict. Flask-AppBuilder only calls get_roles_from_keys when that mapping is set. The __dynamic__ key is a placeholder so the custom method can decide the roles. AUTH_ROLES_SYNC_AT_LOGIN updates an existing user's roles on every login.

import logging

import jwt
from flask_appbuilder.security.manager import AUTH_OAUTH
from requests.exceptions import HTTPError

from superset.security import SupersetSecurityManager

AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH_ROLES_SYNC_AT_LOGIN = True
# Non-empty, or FAB skips get_roles_from_keys
AUTH_ROLES_MAPPING = {"__dynamic__": []}

OAUTH_PROVIDERS = [
{
"name": "auth0",
"icon": "fa-at",
"token_key": "access_token",
"remote_app": {
"client_id": "YOUR_AUTH0_CLIENT_ID",
"client_secret": "YOUR_AUTH0_CLIENT_SECRET",
"client_kwargs": {"scope": "openid profile email"},
"server_metadata_url": "https://YOUR_AUTH0_DOMAIN/.well-known/openid-configuration",
},
}
]

NS = "https://superset.org"
USERINFO_URL = "https://YOUR_AUTH0_DOMAIN/userinfo"
ROLE_PREFIX = "superset_"


class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None): # noqa: ARG002
try:
remote_app = self.appbuilder.sm.oauth_remotes.get(provider)
if remote_app is None:
logging.error("Provider %s is not configured in oauth_remotes.", provider)
return {}

me = remote_app.get(USERINFO_URL)
me.raise_for_status()
data = me.json()
logging.info("User info from Auth0: %s", data)

# Custom claims (roles) live in the ID token, not in /userinfo.
# Authlib has already validated this token with Auth0.
# verify_aud must be off: Auth0 sets aud to the client id, and
# PyJWT raises InvalidAudienceError when audience is not passed.
claims = {}
if response and response.get("id_token"):
claims = jwt.decode(
response["id_token"],
options={"verify_signature": False, "verify_aud": False},
algorithms=["RS256"],
)
logging.info("ID token claims: %s", claims)

roles = claims.get(f"{NS}/roles", [])
logging.info("Roles: %s", roles)
return {
"username": data.get("nickname", data.get("email", "")),
"first_name": data.get("given_name", ""),
"last_name": data.get("family_name", ""),
"email": data.get("email", ""),
"role_keys": roles,
}
except HTTPError as e:
logging.error("HTTP error occurred: %s", e)
except Exception as e:
logging.error("An error occurred while fetching user info: %s", e)

return {}

def get_roles_from_keys(self, role_keys):
# lowercase name -> Role object, one DB query
superset_roles = {r.name.lower(): r for r in self.get_all_roles()}

matched = set()
for key in role_keys:
if not key.lower().startswith(ROLE_PREFIX):
continue
name = key[len(ROLE_PREFIX) :].lower() # superset_Admin -> admin
role = superset_roles.get(name)
if role:
matched.add(role)
else:
logging.warning(
"No Superset role named '%s' for Auth0 role '%s'", name, key
)
return list(matched)


CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager

SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SECURE = False # True only when you serve HTTPS
SESSION_COOKIE_HTTPONLY = True

Set SESSION_COOKIE_SECURE = True when Superset is served over HTTPS.

If login raises jwt.exceptions.InvalidAudienceError: Invalid audience, the ID token decode is missing algorithms=["RS256"] and options={"verify_signature": False, "verify_aud": False}. Auth0 always sets aud to the client id. The snippet above already passes both, because Authlib validated the token before this method runs.

5. Restart Superset​

Restart the Superset process so it loads the new security manager. The login page should show an Auth0 button. After login, the user's Superset roles match the superset_* roles on the ID token. Users with no matching role stay on Public, the registration role.