Please before simply posting the link to the Superset or Keycloak documentation, I would like to clarify that I have already made numerous attempts, including following the official documentation, but without success.
Details of my deployment in GCP:
Apache Superset:
Version 2.1.1
project-a
Keycloak:
Version 22.0.5
Located in another GKE cluster and in another GCP project within the same organization.
project-b
I couldn’t install Superset via Helm, so I installed it through the marketplace, and that’s also why whenever I need to modify superset_config.py, I edit the Superset ConfigMap.
To install the Authlib library, I created a custom image in the Artifact Registry based on the original Superset marketplace image ‘gcr.io/cloud-marketplace/google/superset:2.1.1-20240331-143231’.
The company I work for has some security procedures that take some time to be configured by the network team, so for now, I am testing as follows: Keycloak is in production in project-b and exposed to the internet, while Superset is in another GKE cluster in project-a and its service is not yet exposed, it is set as ClusterIP. For testing purposes, I am port-forwarding Superset on port 8088 and using the URL http://localhost:8088 in Keycloak’s configuration to test the connection.
I would like help to authenticate two types of SSO providers with Superset, Keycloak, and Google OAuth together.
In Keycloak, I created a realm for Superset, within this realm I created a client, a group, and two users: one admin and one test_1.
I tested with JWTs enabled and disabled, also without the jwks_uri parameter. This example shows one test with JWT enabled on my keycloak realm.
My superset_configmap.yaml is configured as follows:
apiVersion: v1
data:
custom_sso_security_manager.py: |-
import logging
from superset.security import SupersetSecurityManager
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
logging.debug("OAuth2 provider: {0}.".format(provider))
if provider == 'keycloak':
user_info = self.appbuilder.sm.oauth_remotes[provider].get('userinfo').data
logging.debug("User data: {0}".format(user_info))
return {
'username': user_info['preferred_username'],
'email': user_info['email'],
'first_name': user_info['given_name'],
'last_name': user_info['family_name']
}
return None
google_credentials.json: |-
{
“web”:{
“client_id”:“<client_id>”,
“project_id”:“”,
“auth_uri”:“Sign in - Google Accounts”,
“token_uri”:“https://oauth2.googleapis.com/token”,
“auth_provider_x509_cert_url”:“https://www.googleapis.com/oauth2/v1/certs”,
“client_secret”:“<client_secret>”
}
}
superset_config.py: |-
# Superset specific config
ROW_LIMIT = 20000
SUPERSET_WEBSERVER_PORT = 8088
# Flask App Builder configuration
# Your App secret key will be used for securely signing the session cookie
# and encrypting sensitive information on the database
# Make sure you are changing this key for your deployment with a strong key.
# You can generate a strong key using `openssl rand -base64 42`
SECRET_KEY = '<secret_key>'
# The SQLAlchemy connection string to your database backend
# This connection defines the path to the database that stores your
# superset metadata (slices, connections, tables, dashboards, ...).
# Note that the connection information to connect to the datasources
# you want to explore are managed directly in the web UI
SQLALCHEMY_DATABASE_URI = '<sqlalchemy_database_uri>'
# Flask-WTF flag for CSRF
WTF_CSRF_ENABLED = False
# Add endpoints that need to be exempt from CSRF protection
WTF_CSRF_EXEMPT_LIST = ['http://localhost:8080/*', 'http://localhost:8088/*']
# A CSRF token that expires in 1 year
WTF_CSRF_TIME_LIMIT = 60 * 60 * 24 * 365
# Set this API key to enable Mapbox visualizations
MAPBOX_API_KEY = ''
CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_',
'CACHE_REDIS_HOST': '<cache_redis_host>',
'CACHE_REDIS_URL': '<cache_redis_url>'
}
FILTER_STATE_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_filter_',
'CACHE_REDIS_URL': '<cache_redis_url>'
}
EXPLORE_FORM_DATA_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_explore_',
'CACHE_REDIS_URL': '<cache_redis_url>'
}
DATA_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_KEY_PREFIX': 'superset_results_',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_REDIS_URL': '<cache_redis_url>'
}
from superset.stats_logger import StatsdStatsLogger
STATS_LOGGER = StatsdStatsLogger(host='localhost', port=9125, prefix='superset')
APP_NAME = 'Application Name'
from custom_sso_security_manager import CustomSsoSecurityManager
from flask_appbuilder.security.manager import AUTH_OAUTH
import json
import os
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'google_credentials.json')
# Open and read the JSON file
with open(file_path, 'r') as file:
auth_credentials = json.load(file)
auth_credentials = auth_credentials['web']
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = 'Gamma'
AUTH_ROLES_MAPPING = {
'Superset_Admin': ['Admin'],
'Superset_Alpha': ['Alpha'],
'Superset_Gamma': ['Gamma']
}
# OAuth provider configuration for Keycloak
OAUTH_PROVIDERS = [
{
'name': 'keycloak',
'icon': 'fa-address-card',
'token_key': 'access_token',
'remote_app': {
'client_id': '<client_id>',
'client_secret': '<client_secret>',
'client_kwargs': {
'scope': 'openid profile email',
},
'access_token_method': 'POST',
'access_token_params':{ # Additional parameters for calls to access_token_url
'client_id': '<client_id>',
'client_secret': '<client_secret>',
},
'jwks_uri': 'https://<keycloak_url>/realms/<realms>/protocol/openid-connect/certs',
'access_token_headers':{ # Additional headers for calls to access_token_url
'Authorization': 'Basic Base64EncodedClientIdAndSecret'
},
'api_base_url': 'https://<keycloak_url>/realms/<realms>/protocol/openid-connect/',
'access_token_url': 'https://<keycloak_url>/realms/<realms>/protocol/openid-connect/token',
'authorize_url': 'https://<keycloak_url>/realms/<realms>/protocol/openid-connect/auth'
}
},
{
'name': 'google',
# email whitelist
'whitelist': ['@company1.com', '@company2.com', '@company3.com'],
'icon': 'fa-google',
'token_key': 'access_token',
'remote_app': {
'base_url': 'https://www.googleapis.com/oauth2/v2/',
'request_token_params': {
'scope': 'email profile'
},
'request_token_url': None,
'access_token_url': auth_credentials['token_uri'],
'authorize_url': auth_credentials['auth_uri'],
# google api & services client id and secret
'consumer_key': auth_credentials['client_id'],
'consumer_secret': auth_credentials['client_secret']
}
}
]
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
import logging
logging.basicConfig(level=logging.DEBUG)
When I edit the Superset ConfigMap and restart the Superset StatefulSet, everything runs successfully. I run kubectl port-forward --namespace superset svc/superset-1-superset-svc 8088 and access http://localhost:8088/, then the Superset login screen appears.
My client in the Keycloak realm has these URLs:
Settings Tab:
Home URL: http://localhost:8088/
Valid redirect URIs: First tried with http://localhost:8088/oauth-authorized/keycloak, but only works with http://localhost:8088/*
Web origins: *
Client authentication: On
Authorization: Off
Authentication flow: Standard flow: On and Direct access grants: On
Front channel logout: Off
Backchannel logout session required: Off
Keys Tab:
Use JWKS URL: On
JWKS URL: https://<keycloak_url>/realms//protocol/openid-connect/certs
Credentials Tab:
Client Authenticator: Client Id and Secret
I created a client secret (the same one I put in the ConfigMap)
Roles Tab:
Admin
Gamma
Groups Section:
I created a group named Grupo-admins
Users Section:
I created a user named admin within the Grupo-admins group and a user named teste_1
In Identity Providers, I configured Google as an IdP. In GCP, I went to APIs & Services, configured the OAuth consent screen, and created a credential of type OAuth Client ID.
When I edit the Superset ConfigMap and restart the Superset StatefulSet, everything runs successfully. The pods, StatefulSet, and services run correctly. I execute kubectl port-forward --namespace superset svc/superset-1-superset-svc 8088 and access http://localhost:8088/, and the Superset login screen appears (first screenshot) then when I click the Keycloak button redirects to keycloak page (second screenshot), then I when I insert the user and password the page redirects to superset login page with the message “Invalid login. Please try again.” (third screenshot)
The last attempt was to login with Google OAuth, when I click redirects to the another page with the message “Access blocked: Authorization Error Missing required parameter: scope Learn more about this error If you are a developer of this app, see error details. Error 400: invalid_request” (fourth screenshot)
