Apache Superset 接入 Keycloak OIDC:IAM 单点登录与组角色映射排错 | IDaaS Book
场景
- 自建 Apache Superset 想用 Keycloak 做统一登录,配置照抄官方文档的
OAUTH_PROVIDERS示例,浏览器却在 Keycloak 认证成功后被弹回登录页,只看到一句Invalid login. Please try again.。 - 或者登录是通了,但所有 SSO 用户拿到的都是同一个角色:在 Keycloak 里分了
superset_admins/superset_users两个组,Superset 里没有任何区别;后来把某人从管理员组移除,权限也照旧。 - 这个组合的坑几乎都不在「OIDC 会不会配」这一层,而在 Superset 的 OAuth 由 Flask-AppBuilder(FAB)实现,而 FAB 对 Keycloak 走了内置分支:用户信息与组只从
/userinfo端点读,provider 名不在内置名单里就直接抛错,取角色的 claim 名被硬编码成groups。行为核对自 FABflask_appbuilder/security/{manager,views}.py、FAB 官方文档docs/security.rst、Authlibintegrations/base_client/sync_app.py、Supersetsuperset/config.py与superset/security/manager.py、KeycloakUserInfoEndpoint.java与 admin-api 的 Protocol Mappers 参考;核查日期 2026-10-06,版本口径 Superset 5.x(flask-appbuilder>=5.2.3,<6.0.0,Authlib 1.x)、Keycloak 26.x。
适用 / 不适用
| 场景 | 是否适用 |
|---|---|
| 浏览器访问 Superset,用 Keycloak 做人类用户单点登录 + 按组映射 Superset 角色 | ✅ 本页主场景 |
| Superset 的 Alerts & Reports / 脚本调用,走 API key 或数据库账号 | ❌ 与本页无关,AUTH_OAUTH 只管交互式登录 |
| 想让 Keycloak 的组直接等于 Superset 的细粒度行级权限 | ⚠️ 只能映射到 FAB 角色(Admin / Alpha / Gamma / 自定义角色),行级权限仍在 Superset 的 RLS 规则里配 |
| 上游是其他 OIDC IdP(Entra ID / Okta / Authlib 通用 provider) | ⚠️ 同一套 OAUTH_PROVIDERS 骨架,但 get_oauth_user_info() 的 provider 分支不同,claim 来源也不同,不能照搬本页结论 |
| Superset 与 Keycloak 之间还有一层只做 TLS 卸载的反向代理 | ✅ 但回调地址由请求推导,必须配好 X-Forwarded-*,见第 5 节 |
1. 先认清:Keycloak 在 FAB 里是「内置 provider」,不是一个通用 OIDC 客户端
FAB 文档把 provider 分成两类:名字可以在内置名单里的(authentik、azure、github、google、keycloak、keycloak_before_17、linkedin、okta、openshift、twitter),以及需要你自己实现 oauth_user_info 的自定义 provider。名单内的走 BaseSecurityManager.get_oauth_user_info() 的内置分支,名单外的直接抛 OAuthProviderUnknown。
内置的 Keycloak 分支长这样(flask_appbuilder/security/manager.py):
if provider in ["keycloak", "keycloak_before_17"]:
me = self.appbuilder.sm.oauth_remotes[provider].get("openid-connect/userinfo")
me.raise_for_status()
data = me.json()
return {
"username": data.get("preferred_username", ""),
"first_name": data.get("given_name", ""),
"last_name": data.get("family_name", ""),
"email": data.get("email", ""),
"role_keys": data.get("groups", []), # 组 claim 名写死为 groups
}三件事从这段代码里直接读出来:
- 用户名取
preferred_username,不是sub。取不到时auth_user_oauth()会退回用email当用户名;两个都为空只打一行log.error然后返回None——外部表现就是「登录成功又被弹回登录页」,没有任何错误提示。 - 组 claim 名硬编码为
groups,且只从/userinfo响应里取。这一点是本页与 OpenSearch Dashboards、 MinIO 那些「必须勾 Add to ID token」的接入最大的区别:Superset 根本不看 ID token 里的组,mapper 的 Add to userinfo 开关才起作用。 oauth_user_info()抛出的任何异常都被views.py吞掉。oauth_authorized()里是except Exception as e: log.error("Error returning OAuth user info: %s", e),然后 flash 一句通用文案、回登录页。所以 Superset 侧的报错信息只在容器日志里,页面上永远只有Invalid login. Please try again.。
如果确实需要自定义 provider 名(比如同一套 Superset 接两个 IdP),走 Superset 文档里的
CustomSsoSecurityManager:继承SupersetSecurityManager覆写oauth_user_info(),用自己的逻辑拼出含role_keys的 dict。代价是 claim 解析从此由你维护,内置分支的升级收益也拿不到。
1.1 api_base_url 是必需的,路径少一段就 404
内置分支请求的是一个相对路径 openid-connect/userinfo。Authlib 只在配置了 api_base_url 时才做拼接(authlib/integrations/base_client/sync_app.py):
if self.api_base_url and not url.startswith(("https://", "http://")):
url = urlparse.urljoin(self.api_base_url, url)拼接用的是 urljoin 的「替换最后一段」语义,所以 api_base_url 结尾差一个斜杠,结果完全不同(下表为实测结果):
api_base_url 配置 | 实际请求的 userinfo 地址 | 结果 |
|---|---|---|
https://kc.example.com/realms/myrealm/protocol/openid-connect | https://kc.example.com/realms/myrealm/protocol/openid-connect/userinfo | ✅ 正确(FAB 文档示例就是这个形态) |
https://kc.example.com/realms/myrealm/protocol/ | https://kc.example.com/realms/myrealm/protocol/openid-connect/userinfo | ✅ 正确 |
https://kc.example.com/realms/myrealm/protocol | https://kc.example.com/realms/myrealm/openid-connect/userinfo | ❌ 404(protocol 被替换掉) |
https://kc.example.com/realms/myrealm/ | https://kc.example.com/realms/myrealm/openid-connect/userinfo | ❌ 404(缺 protocol) |
另外两点推论同样来自源码,值得先记住:
- 只配
server_metadata_url不够。Authlib 的load_server_metadata()只是把 discovery 文档合并进server_metadata,并不会回填api_base_url,相对路径因此不会被拼接,表现为MissingSchema: Invalid URL 'openid-connect/userinfo'。两者要一起配:server_metadata_url负责 authorize/token/jwks,api_base_url负责 userinfo。 token_key默认是oauth_token,而 Authlib 返回的 token 字典里的键是access_token。set_oauth_session()会去做oauth_response[token_key],漏配就抛KeyError,同样被吞进日志,外部仍是弹回登录页。
2. Keycloak 端配置
| 字段 | 值 | 说明 |
|---|---|---|
| Client ID | superset | 与 Superset 里 client_id 一致 |
| Client authentication | ON | Superset 是 confidential client,用 client secret 换 token |
| Standard flow | ON | 浏览器登录走授权码流程 |
| Direct access grants | OFF | 不需要 ROPC |
| Valid redirect URIs | https://superset.example.com/oauth-authorized/keycloak | 路径由 FAB 决定,keycloak 即 provider 名,见 5.1 |
| Client scopes | openid、email、profile + 自建组 scope | 见 2.1 / 2.2 |
2.1 openid 必须在 scope 里
Keycloak 的 /userinfo 端点会校验 access token 的 scope(UserInfoEndpoint.java):
if (!TokenUtil.hasScope(token.getScope(), OAuth2Constants.SCOPE_OPENID)) {
String errorMessage = "Missing openid scope";
...
throw error.insufficientScope(errorMessage);
}所以 Superset 侧必须写 client_kwargs: {"scope": "openid email profile"}。FAB 文档里 Keycloak 示例只写了 scope: "email profile",照抄到 Keycloak 上会在 userinfo 调用处拿到 insufficient_scope / Missing openid scope,然后被前文那段 except 吞掉——又是一次「登录成功后弹回登录页」。
email 和 profile 同样不能省:preferred_username、given_name、family_name 来自 profile,email 来自 email scope。少了 email,auth_user_oauth() 会用 f"{username}@email.notfound" 造一个假邮箱写进 Superset 用户表——Alerts & Reports 的收件人会变成这个不存在的地址,而且邮件失败不会在登录链路上报错,通常几周后才发现。
2.2 组 claim:建 mapper,claim 名必须是 groups
用户组不是 Keycloak 的默认 claim,要自己建。推荐做法是新建一个 client scope(例如 groups),在里面加一个 Group Membership(oidc-group-membership-mapper)mapper:
| 字段 | 值 | 依据 |
|---|---|---|
| Token Claim Name | groups | FAB 源码里 data.get("groups", []) 是硬编码的,改别的名字等于没配 |
| Full group path | true(默认) | 默认即 true,claim 值是 /superset_admins 这种完整路径 |
| Add to userinfo | ON | admin-api 文档里 userinfo.token.claim 默认值为 true,但开关必须实测确认,见下 |
| Add to ID token / access token | 按需 | Superset 只读 userinfo,这两个开不开都不影响本页场景 |
然后在 Clients → superset → Client scopes 把这个 scope 挂为 Default(或让 Superset 在 scope 参数里显式请求)。
不要用 microprofile-jwt scope 里的 groups:那个 mapper 是 User Realm Role 类型,claim 里装的是 realm 角色而不是用户组,值与 AUTH_ROLES_MAPPING 的直觉不一致(对比见
Jenkins 接入 Keycloak OIDC)。
关于
Add to userinfo有一个真实存在的坑:Keycloak 新版 Admin Console 曾经把 mapper 的开关显示成「已打开」但实际没落库(keycloak#16403 的记录是重新保存一次、或把开关关掉再打开才生效)。所以判断依据只有/userinfo的实际响应,不要只看控制台复选框。
3. Superset 端配置(superset_config.py)
from flask_appbuilder.security.manager import AUTH_OAUTH
AUTH_TYPE = AUTH_OAUTH
OAUTH_PROVIDERS = [
{
"name": "keycloak", # 必须是 FAB 内置名单里的名字,见 1
"icon": "fa-key",
"token_key": "access_token", # 默认 oauth_token,漏了会在 set_oauth_session 抛 KeyError
"remote_app": {
"client_id": "superset",
"client_secret": os.environ["SUPERSET_OIDC_CLIENT_SECRET"],
# discovery 负责 authorize/token/jwks
"server_metadata_url": "https://kc.example.com/realms/myrealm/.well-known/openid-configuration",
# userinfo 的相对路径要拼在这上面,必须以 .../protocol/openid-connect 结尾
"api_base_url": "https://kc.example.com/realms/myrealm/protocol/openid-connect",
"client_kwargs": {"scope": "openid email profile"},
},
}
]
AUTH_USER_REGISTRATION = True # 默认 False,不认识的新用户会被直接拒绝
AUTH_USER_REGISTRATION_ROLE = "Gamma" # 每次登录都会被重新加回,不要设成 Admin
AUTH_ROLES_MAPPING = {
"/superset_admins": ["Admin"], # key 是 userinfo 里 groups 的原始值
"/superset_users": ["Gamma", "Alpha"],
}
AUTH_ROLES_SYNC_AT_LOGIN = True # 默认 False,见 3.23.1 AUTH_ROLES_MAPPING 的 key 是 claim 值,不是角色名
FAB 文档对这两个 key 的定义是:AUTH_ROLES_MAPPING 是「从 userinfo["role_keys"] 的值到 FAB 角色名列表的映射」。取值的实现在 _oauth_calculate_user_roles() → get_roles_from_keys():
_role_keys = set(role_keys)
for role_key, fab_role_names in self.auth_roles_mapping.items():
if role_key in _role_keys:
for fab_role_name in fab_role_names:
fab_role = self.find_role(fab_role_name)
if fab_role:
_roles.add(fab_role)
else:
log.warning("Can't find role specified in AUTH_ROLES_MAPPING: %s", fab_role_name)两个直接结论:
- 匹配是逐字符的集合相等。
full.path默认为true,claim 值是/superset_admins,那么AUTH_ROLES_MAPPING的 key 就必须带前导斜杠;写成superset_admins不会报错,只是永远匹配不上(full.path的分层取舍与同名组歧义见 Argo CD 接入 Keycloak OIDC)。 - 右侧的 FAB 角色名必须已存在。Superset 内置
Admin/Alpha/Gamma/Public,自定义角色先在 Settings → List Roles 里建好;写错名字只有一行 warning,用户的角色就这么静默少了。
3.2 角色只在「按配置的时机」计算,默认不是每次登录
FAB 的默认值是 AUTH_ROLES_SYNC_AT_LOGIN = False。对应到 auth_user_oauth():
if user and self.auth_roles_sync_at_login:
user.roles = self._oauth_calculate_user_roles(userinfo)也就是说默认情况下角色只在用户首次注册时算一次,之后你在 Keycloak 里调整组,Superset 里不会有任何变化——这是本组合里最常见的「策略已经变更、权限却没变」投诉来源。要按组授权就必须显式打开,并接受每次登录覆盖一次角色(FAB 文档同时建议配合较短的 PERMANENT_SESSION_LIFETIME 让角色更快收敛)。
两点补充语义:
- 打开后是整体替换
user.roles,不是追加。所以「从管理员组移除」能立刻生效;但反过来,任何不在映射里的角色也会被清掉——包括你在 Superset 里手工加过的角色。 _oauth_calculate_user_roles()在AUTH_USER_REGISTRATION为真时会无条件把AUTH_USER_REGISTRATION_ROLE加回结果里(不只是注册那一刻)。Superset 文档里出现过的AUTH_USER_REGISTRATION_ROLE = "Admin"示例,等于给每个能通过 Keycloak 认证的用户永久授予 Admin,请勿在共享环境照抄。
3.3 注册开关与登出边界
AUTH_USER_REGISTRATION = False(默认)时,auth_user_oauth()对不存在的用户直接返回None:表现为「Keycloak 说认证成功,Superset 说登录失败」。要做 JIT 开通就必须显式打开。- FAB 的
AuthOAuthView只实现了login与oauth_authorized两个端点,没有登出端点。Superset 的登出只清本地会话,不会回调 Keycloak 的end_session_endpoint,用户在 Keycloak 侧的 SSO 会话仍然活着——这与「登出来又自动登回去」的现象是同一件事的两面,排查思路见 IAM 单点登出不彻底排错。
4. 验证顺序
顺序不要反,否则会拿下游现象去查上游配置。
- 先看 Keycloak 实际吐出来的 userinfo(Clients →
superset→ Client scopes → Evaluate → 选用户 → Generated user info;或浏览器登录后从 DevTools 取 access token 直接打端点):
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
https://kc.example.com/realms/myrealm/protocol/openid-connect/userinfo \
| jq '{preferred_username, email, groups}'期望:groups 是数组且形如 ["/superset_admins"]。这一步没通过,Superset 侧怎么改都是白改。
- 再确认 discovery 的 issuer 与 Superset 容器可达:
curl -s https://kc.example.com/realms/myrealm/.well-known/openid-configuration \
| jq '{issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri}'- 打开 Superset 的 DEBUG 日志,看 FAB 拿到了什么。
get_oauth_user_info()会把整个 userinfo 打成log.debug:
User info from Keycloak: {...}
Calculated new roles for user='alice' as: [...]
Can't find role specified in AUTH_ROLES_MAPPING: ...
Error returning OAuth user info: ...
OAUTH userinfo does not have username or email {...}前两行说明链路通了;第三行说明映射右侧的角色名不存在;第四、五行说明 oauth_user_info() 抛异常或返回的 dict 里既没有 username 也没有 email。页面上永远只有一句通用文案,这几行日志是唯一可用的证据。
登录后核对用户与角色:Settings → List Users 看用户是否落地、邮箱是否正常;Settings → List Roles →
<角色>→ List Users 看组映射是否按预期生效。负测试:把用户从
superset_admins组移除,重新登录,确认它确实丢掉 Admin,而不是「还留着」。没有这一步就无法区分「映射生效」与「角色来自注册默认值」。
5. 反向代理与回调地址
FAB 生成回调地址的代码是 url_for(".oauth_authorized", provider=provider, _external=True)——地址由当前请求推导,不来自配置文件。Superset 默认 ENABLE_PROXY_FIX = False、PROXY_FIX_CONFIG = {"x_for": 1, "x_proto": 1, "x_host": 1, "x_port": 1, "x_prefix": 1}。因此:
- 反代只做 TLS 卸载、没有把
X-Forwarded-Proto/X-Forwarded-Host交给 Superset,或没打开ENABLE_PROXY_FIX,送给 Keycloak 的redirect_uri就会是http://或内部主机名 → Keycloak 精确匹配失败,登录页直接报Invalid parameter: redirect_uri(这类「先怀疑 Keycloak」的误判在 oauth2-proxy 常见错误 里有同类模式)。 - 部署在子路径下(
APPLICATION_ROOT/SUPERSET_APP_ROOT)时,回调路径也会带前缀,Keycloak 的 Valid redirect URIs 必须同步写成https://host/<prefix>/oauth-authorized/keycloak。
6. 常见错误对照表
| 症状 / 报错 | 根因 | 处理 |
|---|---|---|
Keycloak 侧认证成功,回 Superset 只看到 Invalid login. Please try again.,日志 Error returning OAuth user info: OAuthProviderUnknown | name 不在 FAB 内置名单里 | 改名 keycloak,或者按 Superset 文档覆写 oauth_user_info() |
同上,日志 Error returning OAuth user info: 'oauth_token' | token_key 未配(默认 oauth_token,Authlib 返回的是 access_token) | 补 "token_key": "access_token" |
同上,日志 MissingSchema: Invalid URL 'openid-connect/userinfo' | 只配了 server_metadata_url,api_base_url 缺失,相对路径没被拼接 | 补 api_base_url |
同上,日志 404 指向 .../realms/<realm>/openid-connect/userinfo | api_base_url 少了 /protocol 或以 /protocol(无斜杠)结尾 | 按 1.1 的表改成 .../protocol/openid-connect |
userinfo 返回 401 / insufficient_scope / Missing openid scope | scope 里没有 openid(常见于照抄 FAB 文档的 email profile) | client_kwargs.scope 改为 openid email profile |
| 登录成功,但所有用户角色相同、没有任何组差异 | userinfo 里没有 groups:mapper 没建、claim 名不是 groups、或 Add to userinfo 未真正落库 | 用第 4 节第 1 步核对 userinfo;必要时重新保存 mapper(keycloak#16403) |
groups 有值但角色不生效 | AUTH_ROLES_MAPPING 的 key 与 claim 值不逐字符相等(full.path 默认 true 带前导斜杠);或右侧 FAB 角色不存在 | 对齐 key;日志里搜 Can't find role specified in AUTH_ROLES_MAPPING |
| 在 Keycloak 调整组后 Superset 权限不变 | AUTH_ROLES_SYNC_AT_LOGIN 默认 False,只在首次注册时算角色 | 设为 True,并缩短会话有效期让角色更快收敛 |
| 所有 SSO 用户都是 Admin | AUTH_USER_REGISTRATION_ROLE = "Admin",且该角色每次登录都会被加回 | 改成低权限角色,管理员身份走组映射 |
| 新用户登录被拒(无明确报错) | AUTH_USER_REGISTRATION 默认 False | 确认策略后显式打开 |
Alerts & Reports 收件人是 [email protected] | userinfo 没有 email:scope 少 email,或 Keycloak 用户未填邮箱 | 补 scope / 补用户邮箱,并回查已有的错误邮箱记录 |
Keycloak 登录页 Invalid parameter: redirect_uri | 回调地址由请求推导,反代未传 X-Forwarded-* 或未开 ENABLE_PROXY_FIX;子路径部署未同步前缀 | 见第 5 节 |
| 登出后立刻又被自动登回 | Superset 登出只清本地会话,不回 Keycloak end_session_endpoint | 见 3.3,单点登出需按应用侧方案单独设计 |
7. 回滚
顺序:先让 Superset 能登录,再动 Keycloak,最后才清理角色映射。
- Superset:把
AUTH_TYPE改回AUTH_DB(或注释掉OAUTH_PROVIDERS)并重启。本地管理员账号立刻可用;OIDC 段配置先留着,别删。 - Superset 用户数据不要动:SSO 建出来的用户与已同步的角色是幂等资产,回滚期间保留即可,重建成本远高于留着。
- Keycloak:只禁用 client,不删除。禁用一个动作就停掉新登录,redirect URI、scope、mapper 全部保留。
- 代理层:如果为了排查临时放宽过
ENABLE_PROXY_FIX或X-Forwarded-*处理,回滚时一并收回,不要留在生产上。 - 回滚验证清单:本地账号能登录、原有用户与角色未变、告警任务仍能正常发邮件、Keycloak 侧 client 处于 Disabled。
常见问题(IAM 单点登录)
Q1:为什么别的软件要勾 Add to ID token,Superset 却要勾 Add to userinfo?
因为取值路径不同。FAB 的内置 Keycloak 分支是 oauth_remotes[provider].get("openid-connect/userinfo"),用户与组都从 UserInfo 端点的 JSON 里读,ID token 的 claim 它压根不看。同一站内
OpenSearch Dashboards 用 ID token、
MinIO 也只认 ID token,判断依据永远是「目标软件读哪个 token/端点」,而不是「哪个开关看起来更常见」。
Q2:provider 名字能不能写成 keycloak-oidc 之类?
可以写,但不会命中内置逻辑:get_oauth_user_info() 对不在名单里的名字抛 OAuthProviderUnknown,登录会被弹回登录页且只有一行日志。除非你按 Superset 文档继承 SupersetSecurityManager 覆写 oauth_user_info(),否则就用 keycloak。
Q3:AUTH_ROLES_MAPPING 的 key 到底要不要前导斜杠?
以 userinfo 里 groups 的实际值为准。Group Membership mapper 的 full.path 默认为 true,claim 值形如 /superset_admins,所以 key 也要带斜杠。判断方法不是猜,而是先跑第 4 节第 1 步把真实值打出来。
Q4:改了 Keycloak 的组,为什么用户权限没变?
AUTH_ROLES_SYNC_AT_LOGIN 的默认值是 False,角色只在首次注册时计算一次。要按组持续授权就打开它;注意打开后每次登录都是整体替换角色集合。
Q5:Superset 登出会不会一起结束 Keycloak 的 SSO 会话?
不会。FAB 的 AuthOAuthView 只有 login 和 oauth_authorized 两个端点,没有调用 end_session_endpoint 的逻辑。要做到真正单点登出,需要在 Keycloak 侧配合会话上限或前置代理统一处理。
Q6:能不能直接用 Keycloak 的 realm roles 当组用?
技术上可行但不建议:内置分支读的是 groups claim,你要么把 realm 角色映射进 groups(例如用 microprofile-jwt scope,届时值与用户组的直觉不一致,见
Jenkins 篇),要么接受「组」和「角色」两套命名空间在 Superset 里重新抽象一次。共享一套 Keycloak 时,用用户组建专用 client scope 最不容易混淆。
参考来源
- Superset: Configuring Superset(
AUTH_TYPE/OAUTH_PROVIDERS结构、server_metadata_url与code_challenge_method、redirect URL 为https://<superset-webserver>/oauth-authorized/<provider-name>、AUTH_ROLES_MAPPING与AUTH_ROLES_SYNC_AT_LOGIN语义、CustomSsoSecurityManager覆写oauth_user_info的官方示例、ENABLE_PROXY_FIX与X-Forwarded-Proto说明) - Flask-AppBuilder docs: Security(oauth)(内置 provider 名单
'authentik','azure','github','google','keycloak','keycloak_before_17','linkedin','okta','openshift','twitter'、Keycloak 与 keycloak_before_17 的api_base_url示例、token_key默认oauth_token、AUTH_ROLES_MAPPING定义为userinfo["role_keys"]的映射、AUTH_USER_REGISTRATION_ROLE为「在映射之外额外授予」) - Flask-AppBuilder:
flask_appbuilder/security/manager.py(get_oauth_user_info()的 keycloak 分支与结尾的raise OAuthProviderUnknown()、auth_user_oauth()的用户名回退与f"{username}@email.notfound"、_oauth_calculate_user_roles()无条件加入注册角色、get_roles_from_keys()的find_role警告、AUTH_ROLES_MAPPING/AUTH_ROLES_SYNC_AT_LOGIN的setdefault默认值) - Flask-AppBuilder:
flask_appbuilder/security/views.py(oauth_authorized()中url_for(".oauth_authorized", provider=..., _external=True)、except Exception吞掉 userinfo 异常、AuthOAuthView只有login/oauth_authorized两个端点) - Authlib:
integrations/base_client/sync_app.py(_send_token_request()里urljoin(self.api_base_url, url)的拼接条件、load_server_metadata()不设置api_base_url、openid在 scope 中时生成 nonce 的 OIDC 分支) - Superset:
superset/config.py(ENABLE_PROXY_FIX = False与PROXY_FIX_CONFIG默认值、AUTH_USER_REGISTRATION/AUTH_USER_REGISTRATION_ROLE示例) - Keycloak: Protocol Mappers(admin-api 参考)(
oidc-group-membership-mapper的claim.name、full.path默认true、userinfo.token.claim默认值) - Keycloak:
UserInfoEndpoint.java(access token 缺openidscope 时抛insufficient_scope,错误信息为Missing openid scope) - keycloak/keycloak#16403 — Missing data in the userinfo response(新版 Admin Console 中
Add to userinfo开关显示与实际不落库不一致,需重新保存;结论必须以 userinfo 实际响应核对)
相关章节: Keycloak IAM 第三方软件集成指南、 OpenSearch Dashboards 接入 Keycloak OIDC、 MinIO 接入 Keycloak OIDC、 Jenkins 接入 Keycloak OIDC、 Argo CD 接入 Keycloak OIDC、 Keycloak 细粒度授权、 IAM 单点登出不彻底排错。