Integrating Apache Superset into Your Application with JWT Single Sign-On
Apache Superset is a web-based analytics and dashboarding platform built on Flask-AppBuilder (FAB). Integrating it with an existing product typically requires controlling authentication and sestion creation so users can transition from your app to Superset without re-entering credantials.
Features relevant to integration include:
- SQL IDE and query runner
- Chart and dashboard authoring with role-based access control (RBAC)
- Pluggable authentication through a customizable Security Manager
Containerized setup
-
Clone and bootstrap the Docker environment:
git clone https://github.com/apache/incubator-superset.git cd incubator-superset/contrib/docker # Optional: load example data and dashboards SUPERSET_LOAD_EXAMPLES=yes docker-compose run --rm superset ./docker-init.sh # Start the stack docker-compose up -d -
Navigate to http://localhost:8088.
Authenticasion architecture
Superset wires its auth layer via Flask-AppBuilder’s AppBuilder and a Security Manager. The application constructs AppBuilder with a security_manager_class, which can be replaced by a custom implementation to alter login behavior and permission checks.
-
AppBuilder setup (simplified representation):
# superset/__init__.py (conceptual) CustomSM = app.config.get("CUSTOM_SECURITY_MANAGER") or SupersetSecurityManager appbuilder = AppBuilder( app, db.session, base_template="superset/base.html", indexview=MyIndexView, security_manager_class=CustomSM, update_perms=get_update_perms_flag(), ) security_manager = appbuilder.sm -
For database-backed auth, the Security Manager uses an AuthDBView to handle the login route and form-based authentication:
class AuthDBView(AuthView): login_template = "appbuilder/general/security/login_db.html" @expose("/login/", methods=["GET", "POST"]) def login(self): if g.user is not None and g.user.is_authenticated: return redirect(self.appbuilder.get_url_for_index) form = LoginForm_db() if form.validate_on_submit(): user = self.appbuilder.sm.auth_user_db(form.username.data, form.password.data) if not user: flash(as_unicode(self.invalid_login_message), "warning") return redirect(self.appbuilder.get_url_for_login) login_user(user, remember=False) return redirect(self.appbuilder.get_url_for_index) return self.render_template(self.login_template, title=self.title, form=form, appbuilder=self.appbuilder)
To implement SSO, create a subclass of AuthDBView that accepts a token from your app, verifies it, and programmatically authenticates the user.
JWT-based SSO (no password prompt)
-
Goals:
- Accept a JWT via query string, cookie, or header
- Validate the token and find or provision the user
- Create a Superset session and redirect to the home page or a specified URL
-
Example implementation:
# my_security.py import os from flask import request, redirect, flash from flask_appbuilder.security.views import AuthDBView from flask_login import login_user import jwt # Choose a signing algorithm that matches how your app issues tokens. # HS256 (shared secret) is shown for brevity; for RS256 use a public key. JWT_ALGO = os.getenv("JWT_ALGO", "HS256") JWT_SECRET = os.getenv("JWT_SHARED_SECRET", "change-me") # or load a public key JWT_QUERY_KEY = os.getenv("JWT_QUERY_KEY", "access_token") JWT_COOKIE_KEY = os.getenv("JWT_COOKIE_KEY", "access_token") JWT_HEADER_KEY = os.getenv("JWT_HEADER_KEY", "X-Access-Token") class JwtAuthDBView(AuthDBView): login_template = "appbuilder/general/security/login_db.html" def _extract_token(self): token = request.args.get(JWT_QUERY_KEY) token = token or request.cookies.get(JWT_COOKIE_KEY) token = token or request.headers.get(JWT_HEADER_KEY) return token def _decode(self, token): try: return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO]) except Exception: return None def _resolve_redirect(self): next_url = request.args.get("redirect") or request.args.get("next") return next_url or self.appbuilder.get_url_for_index @expose("/login/", methods=["GET", "POST"]) def login(self): # If already authenticated, go to index if getattr(getattr(request, 'user', None), 'is_authenticated', False): return redirect(self.appbuilder.get_url_for_index) token = self._extract_token() if token: payload = self._decode(token) if not payload: flash("Invalid or expired token", "warning") return super().login() # Map claims from your issuer (customize to your token schema) username = payload.get("preferred_username") or payload.get("sub") or payload.get("username") email = payload.get("email") or f"{username}@example.com" first = payload.get("given_name") or username last = payload.get("family_name") or "" role_names = payload.get("roles") or [] if not username: flash("Token missing username claim", "warning") return super().login() sm = self.appbuilder.sm user = sm.find_user(username=username) # Create user on the fly if needed (assign Admin only if your policy allows it) if not user: # Map incoming roles to Superset roles; fallback to Gamma role_objs = [] for r in role_names: role = sm.find_role(r) if role: role_objs.append(role) if not role_objs: role_objs = [sm.find_role("Gamma")] # minimal access user = sm.add_user( username=username, first_name=first, last_name=last, email=email, role=role_objs[0] if role_objs else None, # FAB add_user can accept a single role; for multiple, set user.roles later password=None, ) if user and len(role_objs) > 1: user.roles = role_objs sm.get_session.merge(user) sm.get_session.commit() if user: login_user(user, remember=False) return redirect(self._resolve_redirect()) # Fallback to form-based login return super().login() -
Security Manager hook-up:
# my_security.py (continued) from superset.security import SupersetSecurityManager class MySecurityManager(SupersetSecurityManager): authdbview = JwtAuthDBView -
Activate the custom manager in superset_config.py:
# superset_config.py from my_security import MySecurityManager CUSTOM_SECURITY_MANAGER = MySecurityManager
Token issuance from your application
-
Issue a short-lived JWT after your user signs in to your app. Example (Python, HS256):
import time import jwt def make_superset_token(user): now = int(time.time()) payload = { "sub": user.id, "preferred_username": user.username, "email": user.email, "roles": ["Gamma"], # or map your roles to Superset ones "iat": now, "exp": now + 300, # 5 minutes } return jwt.encode(payload, "change-me", algorithm="HS256")
Embedding or redirecting to Superset
-
Redirect-based SSO:
- Send users to: https://<SUPERESET_HOST>/login/?access_token=<JWT>&redirect=/superset/welcome
-
Iframe embedding (ensure your Superset deployment and browser policy allow iframes):
<iframe src="https://superset.example.com/login/?access_token=YOUR_JWT&redirect=/superset/dashboard/p/MyDashboard" style="width:100%;height:100vh;border:0" referrerpolicy="no-referrer" sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-popups" ></iframe>
Notes and considerations
- Align JWT algorithm and keys with your identity provider (HS256 for shared secret; RS256 for public/private key pairs).
- Map external roles to Superset roles careful; avoid granting Admin broadly.
- Consider CSRF and XSS protections when embedding in iframes; set appropriate headers (Content-Security-Policy, X-Frame-Options).
- For logout behavior, coordinate your app’s session and Superset’s session lifecycle.
- In production, store secrets/keys securely (environment variables, secret managers) and prefer HTTPS for all traffic.