Keycloak OAuth
Superset runs the OAuth flow itself, through Flask-AppBuilder. The gateway routes; it does not authenticate.
nebariapp: auth: enabled: true provider: keycloak provisionClient: true enforceAtGateway: false redirectURI: "/oauth-authorized/keycloak" scopes: [openid, profile, email]Why enforceAtGateway: false
Section titled “Why enforceAtGateway: false”An Envoy SecurityPolicy in front of Superset would authenticate the user and forward an
already-authorized request — and Superset would have no idea who they were, because
Flask-AppBuilder’s user model is populated by its own OAuth callback. You would get a
double login, or a logged-in gateway session in front of a Superset that still shows a
login page.
Letting Superset own the flow is what makes AUTH_ROLES_MAPPING work at all.
redirectURI: /oauth-authorized/keycloak is Flask-AppBuilder’s callback path for a
provider named keycloak. Rename the provider in OAUTH_PROVIDERS and this must change to
match.
The OIDC secret
Section titled “The OIDC secret”With provisionClient: true the operator creates a confidential client in the realm and
writes a Secret named <release>-nebari-superset-oidc-client:
| Key | Contents |
|---|---|
client-id | OIDC client ID |
client-secret | client secret |
issuer-url | Keycloak issuer URL |
Inject it into Superset with extraEnvRaw:
superset: extraEnvRaw: - name: OAUTH_CLIENT_ID valueFrom: secretKeyRef: name: superset-nebari-superset-oidc-client key: client-id - name: OAUTH_CLIENT_SECRET valueFrom: secretKeyRef: name: superset-nebari-superset-oidc-client key: client-secret - name: OAUTH_SERVER_METADATA_URL valueFrom: secretKeyRef: name: superset-nebari-superset-oidc-client key: issuer-url optional: trueConfirm it exists before debugging anything else:
kubectl -n superset get secret superset-nebari-superset-oidc-client -o jsonpath='{.data}' | jq 'keys'The Superset config
Section titled “The Superset config”OAuth is configured through superset.configOverrides.oauth_config, a Python snippet the
upstream chart appends to superset_config.py. The complete block is in
examples/nebari-values.yaml;
the parts that matter:
class CustomSecurityManager(SupersetSecurityManager): def oauth_user_info(self, provider, response=None): if provider == 'keycloak': me = self.appbuilder.sm.oauth_remotes[provider].userinfo() roles = me.get('roles', me.get('groups', [])) return { 'username': me.get('preferred_username', ''), 'first_name': me.get('given_name', ''), 'last_name': me.get('family_name', ''), 'email': me.get('email', ''), 'role_keys': roles, } return super().oauth_user_info(provider, response)
CUSTOM_SECURITY_MANAGER = CustomSecurityManagerAUTH_TYPE = AUTH_OAUTHThe security manager exists to produce role_keys. Flask-AppBuilder looks those up in
AUTH_ROLES_MAPPING; without the override there are no role keys and every user falls
through to the registration role.
Note the fallback: me.get('roles', me.get('groups', [])). Keycloak may present the
information as realm roles or as group membership depending on the mappers configured on
the client, and the code accepts either.
Role mapping
Section titled “Role mapping”AUTH_USER_REGISTRATION = TrueAUTH_USER_REGISTRATION_ROLE = "Gamma"AUTH_ROLES_MAPPING = { 'superset-admin': ['Admin'], 'superset-user': ['Gamma'], 'superset-public': ['Public'],}AUTH_ROLES_SYNC_AT_LOGIN = TrueAUTH_ROLE_ADMIN = 'Admin'AUTH_ROLE_PUBLIC = 'Public'| Setting | Effect |
|---|---|
AUTH_USER_REGISTRATION | Creates a Superset user on first login instead of rejecting unknown users. |
AUTH_USER_REGISTRATION_ROLE | Role for a user matching nothing in the mapping. Gamma is Superset’s standard limited role. |
AUTH_ROLES_MAPPING | Keycloak group or role name → Superset roles. |
AUTH_ROLES_SYNC_AT_LOGIN | Re-evaluates roles on every login, so Keycloak stays authoritative. Without it, roles are assigned once at registration and drift forever. |
The mapping keys must match the group names in nebariapp.auth.groups. They are two halves
of one configuration and there is no validation tying them together.
Scopes and claims
Section titled “Scopes and claims”nebariapp.auth.scopes requests openid, profile, and email from the operator side.
The Superset side asks for more:
'client_kwargs': {'scope': 'openid profile email roles'},The extra roles scope is what puts group or role information into the userinfo response
that oauth_user_info reads. If every user lands in Gamma regardless of their Keycloak
groups, this is the first thing to check — followed by whether the client actually has a
mapper emitting the claim.
Log what Keycloak returns:
kubectl -n superset logs deploy/superset | grep "Keycloak userinfo"The example security manager logs the full userinfo payload at info level, which shows exactly which claim is present and what it contains.
That payload includes the user’s email and name, on every login, in plain pod logs. It is a
debugging aid rather than a setting to leave on: drop the logger.info line from
oauth_config once role mapping works, or lower it to debug.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
| Redirect loop | redirectURI does not match Flask-AppBuilder’s callback path, or the client’s registered redirect URI is stale. |
Pod stuck in CreateContainerConfigError | The extraEnvRaw secret name or key does not match the actual secret. client-id and client-secret are not optional, so the container cannot start. |
| “Invalid client” from Keycloak | The secret exists but its client-id or client-secret is stale or empty, usually after the Keycloak client was re-provisioned or its secret rotated. |
| Login fails with an opaque Authlib error | issuer-url is missing from the secret, or present but empty. Missing is tolerated because it is the one key marked optional: true; either way the pod starts and server_metadata_url falls back to ''. |
Every user is Gamma | The roles claim is missing — scope or client mapper — or the group names do not match AUTH_ROLES_MAPPING. |
| Double login prompt | enforceAtGateway got set to true; check for a SecurityPolicy. |
| Login succeeds, then 500 | Often a SUPERSET_SECRET_KEY rotation — see Secret key. |
Local accounts alongside OAuth
Section titled “Local accounts alongside OAuth”AUTH_TYPE = AUTH_OAUTH replaces the local login form. The admin account the upstream
chart bootstraps still exists in the metadata database and can be reached with
AUTH_TYPE = AUTH_DB temporarily if you lock yourself out — useful when a role mapping
change leaves nobody with Admin.