Search code examples
pythonopenid-connectkeycloakflask-securityapache-superset

Using OpenID/Keycloak with Superset


I want to use keycloak to authenticate my users in our Superset environment.

Superset is using flask-openid, as implemented in flask-security:

To enable a different user authentication than the regular one (database), you need to override the AUTH_TYPE parameter in your superset_config.py file. You will also need to provide a reference to your openid-connect realm and enable user registration. As I understand, it should look something like this:

from flask_appbuilder.security.manager import AUTH_OID
AUTH_TYPE = AUTH_OID
OPENID_PROVIDERS = [
    { 'name':'keycloak', 'url':'http://localhost:8080/auth/realms/superset' }
]
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = 'Gamma'

With this configuration, the login page changes to a prompt where the user can select the desired OpenID provider (in our case keycloak). We also have two buttons, one to sign in (for existing users) and one to register as a new user.

I would expect that either of these buttons would take me to my keycloak login page. However, this does not happen. Instead, I am redirected right back to the login page.

In the case where I press the registration button, I get a message that says 'Not possible to register you at the moment, try again later'. When I press the sign in button, no message is displayed. The Superset logs show the request that loads the login page, but no requests to keycloak. I have tried the same using the Google OpenID provider, which works just fine.

Since I am seeing no requests to keycloak, this makes me think that I am either missing a configuration setting somewhere, or that I am using the wrong settings. Could you please help me figure out which settings I should be using?


Solution

  • 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('/') + 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'