Using OpenID/Keycloak with Superset

Update 03-02-2020

@s.j.meyer has written an updated guide which works with Superset 0.28.1 and up. I haven’t tried it myself, but thanks @nawazxy for confirming this solution works.


I managed to solve my own question. The main problem was caused by a wrong assumption I made regarding the flask-openid plugin that superset is using. This plugin actually supports OpenID 2.x, but not OpenID-Connect (which is the version implemented by Keycloak).

As a workaround, I decided to switch to the flask-oidc plugin. Switching to a new authentication provider actually requires some digging work. To integrate the plugin, I had to follow these steps:

Configue flask-oidc for keycloak

Unfortunately, flask-oidc does not support the configuration format generated by Keycloak. Instead, your configuration should look something like this:

{
    "web": {
        "realm_public_key": "<YOUR_REALM_PUBLIC_KEY>",
        "issuer": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>",
        "auth_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/auth",
        "client_id": "<YOUR_CLIENT_ID>",
        "client_secret": "<YOUR_SECRET_KEY>",
        "redirect_urls": [
            "http://<YOUR_DOMAIN>/*"
        ],
        "userinfo_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/userinfo",
        "token_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/token",
        "token_introspection_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/token/introspect"
    }
}

Flask-oidc expects the configuration to be in a file. I have stored mine in client_secret.json. You can configure the path to the configuration file in your superset_config.py.

Extend the Security Manager

Firstly, you will want to make sure that flask stops using flask-openid ad starts using flask-oidc instead. To do so, you will need to create your own security manager that configures flask-oidc as its authentication provider. I have implemented my security manager like this:

from flask_appbuilder.security.manager import AUTH_OID
from flask_appbuilder.security.sqla.manager import SecurityManager
from flask_oidc import OpenIDConnect
    
class OIDCSecurityManager(SecurityManager):

def __init__(self,appbuilder):
    super(OIDCSecurityManager, self).__init__(appbuilder)
    if self.auth_type == AUTH_OID:
        self.oid = OpenIDConnect(self.appbuilder.get_app)
    self.authoidview = AuthOIDCView

To enable OpenID in Superset, you would previously have had to set the authentication type to AUTH_OID. My security manager still executes all the behaviour of the super class, but overrides the oid attribute with the OpenIDConnect object. Further, it replaces the default OpenID authentication view with a custom one. I have implemented mine like this:

from flask_appbuilder.security.views import AuthOIDView
from flask_login import login_user
from urllib import quote

class AuthOIDCView(AuthOIDView):

@expose('/login/', methods=['GET', 'POST'])
def login(self, flag=True):
    
    sm = self.appbuilder.sm
    oidc = sm.oid

    @self.appbuilder.sm.oid.require_login
    def handle_login(): 
        user = sm.auth_user_oid(oidc.user_getfield('email'))
        
        if user is None:
            info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email'])
            user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'), info.get('email'), sm.find_role('Gamma')) 
        
        login_user(user, remember=False)
        return redirect(self.appbuilder.get_url_for_index)  
   
return handle_login()  

@expose('/logout/', methods=['GET', 'POST'])
def logout(self):
    
    oidc = self.appbuilder.sm.oid
    
    oidc.logout()
    super(AuthOIDCView, self).logout()        
    redirect_url = request.url_root.strip("https://stackoverflow.com/") + self.appbuilder.get_url_for_login
    
    return redirect(oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))

My view overrides the behaviours at the /login and /logout endpoints. On login, the handle_login method is run. It requires the user to be authenticated by the OIDC provider. In our case, this means the user will first be redirected to Keycloak to log in.

On authentication, the user is redirected back to Superset. Next, we look up whether we recognize the user. If not, we create the user based on their OIDC user info. Finally, we log the user into Superset and redirect them to the landing page.

On logout, we will need to invalidate these cookies:

  1. The superset session
  2. The OIDC token
  3. The cookies set by Keycloak

By default, Superset will only take care of the first. The extended logout method takes care of all three points.

Configure Superset

Finally, we need to add some parameters to our superset_config.py. This is how I’ve configured mine:

'''
AUTHENTICATION
'''
AUTH_TYPE = AUTH_OID
OIDC_CLIENT_SECRETS = 'client_secret.json'
OIDC_ID_TOKEN_COOKIE_SECURE = False
OIDC_REQUIRE_VERIFIED_EMAIL = False
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = 'Gamma'

Leave a Comment