keycloakapache-supersetauthlib

How to configure Keycloak for Superset with Authlib?


I'm trying to use Authlib to setup Keycloak as SSO for Superset. Everything works fine up until when user is redirected back to Superset. Then this error occured:

authlib.integrations.base_client.errors.MismatchingStateError: mismatching_state: CSRF Warning! State not equal in request and response.

Here's my code:

In superset_config.py

AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = 'Public'
CSRF_ENABLED = True
ENABLE_PROXY_FIX = True
OAUTH_PROVIDERS = [
    {
        'name': 'keycloak',
        'token_key': 'access_token',
        'icon': 'fa-icon',
        'remote_app': {
            'client_id': CLIENT_ID,
            'client_secret': CLIENT_SECRET,
            'client_kwargs': {
                'scope': 'openid email profile'
            },
            'access_token_method': 'POST',
            'api_base_url': 'https://KEYCLOAK_URL/auth/realms/REALM_NAME/protocol/openid-connect/',
            'access_token_url': 'https://KEYCLOAK_URL/auth/realms/REALM_NAME/protocol/openid-connect/token',
            'authorize_url': 'https://KEYCLOAK_URL/auth/realms/REALM_NAME/protocol/openid-connect/auth',
        },
    }
]
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager

Here's my OIDCSecurityManager:

class OIDCSecurityManager(SupersetSecurityManager):
    def get_oauth_user_info(self, provider, response=None):
        if provider == 'keycloak':
            me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
            return {
                "first_name": me.data.get("given_name", ""),
                "last_name": me.data.get("family_name", ""),
                "email": me.data.get("email", "")
            }

What can I do to resolve this problem? (this happens on all browser chrome, firefox, etc.)


Solution

  • I also had trouble with the OIDC configs, but this security manager configuration works for me.
    Note - I've added roles to my Client configuration in Keycloak, and a mapper so the roles can be picked from the user info response.

    class CustomSsoSecurityManager(SupersetSecurityManager):
    
        def oauth_user_info(self, provider, response=None):
            logging.debug("Oauth2 provider: {0}.".format(provider))
    
            logging.debug("Oauth2 oauth_remotes provider: {0}.".format(self.appbuilder.sm.oauth_remotes[provider]))
    
            if provider == 'keycloak':
                # Get the user info using the access token
                res = self.appbuilder.sm.oauth_remotes[provider].get(os.getenv('KEYCLOAK_BASE_URL') + '/userinfo')
    
                logger.info(f"userinfo response:")
                for attr, value in vars(res).items():
                    print(attr, '=', value)
    
                if res.status_code != 200:
                    logger.error('Failed to obtain user info: %s', res._content)
                    return
    
                #dict_str = res._content.decode("UTF-8")
                me = json.loads(res._content)
    
                logger.debug(" user_data: %s", me)
                return {
                    'username' : me['preferred_username'],
                    'name' : me['name'],
                    'email' : me['email'],
                    'first_name': me['given_name'],
                    'last_name': me['family_name'],
                    'roles': me['roles'],
                    'is_active': True,
                }
    
        def auth_user_oauth(self, userinfo):
            user = super(CustomSsoSecurityManager, self).auth_user_oauth(userinfo)
            roles = [self.find_role(x) for x in userinfo['roles']]
            roles = [x for x in roles if x is not None]
            user.roles = roles
            logger.debug(' Update <User: %s> role to %s', user.username, roles)
            self.update_user(user)  # update user roles
            return user