Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Integrating Apache Superset into Your Application with JWT Single Sign-On

Tech 2

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.