将 OpenID/Keycloak 与超集一起使用

Using OpenID/Keycloak with Superset

我想在我们的 Superset 环境中使用 keycloak 来验证我的用户。

Superset 正在使用 flask-openid,在 flask-security 中实现:

要启用不同于常规用户身份验证(数据库)的用户身份验证,您需要覆盖 superset_config.py 文件中的 AUTH_TYPE 参数。您还需要提供对您的 openid-connect 领域的引用并启用用户注册。据我了解,它应该看起来像这样:

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'

使用此配置,登录页面变为提示,用户可以在其中 select 所需的 OpenID 提供程序(在我们的例子中为 keycloak)。我们还有两个按钮,一个用于登录(针对现有用户),一个用于注册为新用户。

我希望这些按钮中的任何一个都能将我带到我的 keycloak 登录页面。但是,这不会发生。相反,我被重定向回 登录页面。

在我按下注册按钮的情况下,我收到一条消息 'Not possible to register you at the moment, try again later'。当我按下登录按钮时,没有显示任何消息。 Superset 日志显示加载登录页面的请求,但没有对 keycloak 的请求。我已经尝试使用 Google OpenID 提供程序进行相同的操作,效果很好。

由于我没有看到对 keycloak 的请求,这让我觉得我要么在某处缺少配置设置,要么我使用了错误的设置。你能帮我弄清楚我应该使用哪些设置吗?

更新 03-02-2020

@s.j.meyer 写了 an updated guide which works with Superset 0.28.1 and up。我自己还没有尝试过,但感谢@nawazxy 确认此解决方案有效。


我设法解决了我自己的问题。主要问题是由于我对 superset 使用的 flask-openid 插件所做的错误假设引起的。该插件实际上支持OpenID 2.x,但不支持OpenID-Connect(这是Keycloak实现的版本)。

作为解决方法,我决定切换到 flask-oidc 插件。切换到新的身份验证提供程序实际上需要一些挖掘工作。要集成插件,我必须遵循以下步骤:

为 keycloak

配置 flask-oidc

很遗憾,flask-oidc不支持Keycloak生成的配置格式。相反,您的配置应如下所示:

{
    "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 期望配置在文件中。我已将我的存储在 client_secret.json 中。您可以在 superset_config.py.

中配置配置文件的路径

扩展安全管理器

首先,您需要确保 Flask 停止使用 flask-openid 广告并开始使用 flask-oidc。为此,您需要创建自己的安全管理器,将 flask-oidc 配置为其身份验证提供程序。我已经像这样实现了我的安全管理器:

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

要在 Superset 中启用 OpenID,您以前必须将身份验证类型设置为 AUTH_OID。我的安全管理器仍然执行超级 class 的所有行为,但使用 OpenIDConnect object 覆盖 oid 属性。此外,它将默认的 OpenID 身份验证视图替换为自定义视图。我是这样实现我的:

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))

我的视图覆盖了 /login 和 /logout 端点处的行为。登录时,handle_login 方法是 运行。它要求用户通过 OIDC 提供商的身份验证。在我们的例子中,这意味着用户将首先被重定向到 Keycloak 以登录。

在身份验证时,用户被重定向回 Superset。接下来,我们查看我们是否认识用户。如果不是,我们将根据他们的 OIDC 用户信息创建用户。最后,我们将用户登录到 Superset 并将他们重定向到登录页面。

注销时,我们需要使这些 cookie 失效:

  1. 超集session
  2. OIDC 代币
  3. Keycloak设置的cookies

默认情况下,Superset 只会处理第一个。扩展注销方法兼顾了这三点。

配置超集

最后,我们需要向 superset_config.py 添加一些参数。这就是我配置我的方式:

'''
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'

我在使用 OIDC 库时遇到了一些问题,所以我对其进行了一些不同的配置 -

在 Keycloak 中,我创建了一个具有 standard flowconfidential 访问权限的新 client
我还在映射器中添加了一个 roles 令牌声明,因此我可以将“客户端角色”映射到超集角色。

对于超集, 我将自定义配置文件挂载到我的容器 [在我的例子中是 k8s]。

/app/pythonpath/custom_sso_security_manager.py

import logging
import os
import json
from superset.security import SupersetSecurityManager


logger = logging.getLogger('oauth_login')

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

并在 /app/pythonpath/superset_config.py 中添加了一些配置 -


from flask_appbuilder.security.manager import AUTH_OAUTH, AUTH_REMOTE_USER

from custom_sso_security_manager import CustomSsoSecurityManager
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager

oauthSecretPair = env('OAUTH_CLIENT_ID') + ':' + env('OAUTH_CLIENT_SECRET')

AUTH_TYPE = AUTH_OAUTH

OAUTH_PROVIDERS = [
    {   'name':'keycloak',
        'token_key':'access_token', # Name of the token in the response of access_token_url
        'icon':'fa-address-card',   # Icon for the provider
        'remote_app': {
            'api_base_url': env('KEYCLOAK_BASE_URL', 'http://CHANGEME'),
            'client_id':env('OAUTH_CLIENT_ID'),  # Client Id (Identify Superset application)
            'client_secret':env('OAUTH_CLIENT_SECRET'), # Secret for this Client Id (Identify Superset application)
            'client_kwargs':{
                'scope': 'profile'               # Scope for the Authorization
            },
            'request_token_url':None,
            'access_token_url': env('KEYCLOAK_BASE_URL', 'http://CHANGEME') + '/token',
            'authorize_url': env('KEYCLOAK_BASE_URL', 'http://CHANGEME') + '/auth',
        }
    }
]

# Will allow user self registration, allowing to create Flask users from Authorized User
AUTH_USER_REGISTRATION = True

# The default user self registration role
AUTH_USER_REGISTRATION_ROLE = "Gamma"

# This will make sure the redirect_uri is properly computed, even with SSL offloading
ENABLE_PROXY_FIX = True

这些配置需要一些环境参数 -

KEYCLOAK_BASE_URL
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET

我尝试按照post中的评论提示进行操作,但即便如此,过程中仍有其他疑问,我设法解决了问题并且完美运行,我会喜欢分享解决问题的代码superset-keycloak。此 aprouch 使用 docker 部署超集应用程序。