<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Oauth2 on IDaaS Book</title><link>https://idaas.xlabs.club/tags/oauth2/</link><description>Recent content in Oauth2 on IDaaS Book</description><generator>Hugo</generator><language>zh-CN</language><copyright>Copyright (c) 2020-2024 xlabs.club</copyright><lastBuildDate>Fri, 25 Sep 2026 23:10:06 +0800</lastBuildDate><atom:link href="https://idaas.xlabs.club/tags/oauth2/index.xml" rel="self" type="application/rss+xml"/><item><title>Keycloak 客户端认证与 IAM 凭据轮换：client secret、private_key_jwt、mTLS | IDaaS Book</title><link>https://idaas.xlabs.club/docs/solution-blogs/keycloak-client-authentication-credentials/</link><pubDate>Mon, 21 Sep 2026 20:50:00 +0800</pubDate><guid>https://idaas.xlabs.club/docs/solution-blogs/keycloak-client-authentication-credentials/</guid><description>&lt;h2 id="场景"&gt;场景&lt;/h2&gt;&#10;&lt;p&gt;服务间调用、定时任务、第三方系统对接——这类 confidential client 的凭据通常就是一个 &lt;code&gt;client_secret&lt;/code&gt;，写在 Deployment 的环境变量或 ConfigMap 里，从上线那天起没换过。Keycloak 里可以用的客户端认证器有四种（另有由 IdP 代签断言的一种），它们对「谁能签发凭据、允许什么算法、断言多久有效、怎么轮换」的约束各不相同；配错之后报错几乎都是同一个 &lt;code&gt;invalid_client&lt;/code&gt;，日志只有一句 &lt;code&gt;Client authentication with signed JWT failed&lt;/code&gt;，看不出到底是密钥不对还是 &lt;code&gt;aud&lt;/code&gt; 不对。&lt;/p&gt;&#10;&lt;p&gt;这篇文章解决三件事：四种认证器的硬边界、&lt;code&gt;private_key_jwt&lt;/code&gt; 断言的完整校验链（哪些 &lt;code&gt;invalid_client&lt;/code&gt; 其实与密钥无关）、以及 client secret 不停机轮换怎么做和它的前置条件。&lt;/p&gt;&#10;&lt;p&gt;只讨论 token endpoint 上的&lt;strong&gt;客户端认证&lt;/strong&gt;，不涉及用户登录流程；用户侧的 MFA/Passkey 落地见 &#13;&#10;&#13;&#10;&lt;a class="link link--text" href="https://idaas.xlabs.club/docs/solution-blogs/keycloak-passkey-webauthn/"&gt;Passkey / WebAuthn / FIDO2 IAM 企业落地指南&lt;/a&gt;。下文源码结论取自 Keycloak &lt;strong&gt;26.7.4&lt;/strong&gt; tag 的 &lt;code&gt;services/src/main/java/org/keycloak/authentication/authenticators/client/&lt;/code&gt;，错误文案为该版本源码中的原文；文档与实现不一致时以实现为准。&lt;/p&gt;&#10;&lt;h2 id="四种认证器与它们的硬边界"&gt;四种认证器与它们的硬边界&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Client Authenticator（控制台）&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;provider id&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;凭据形态&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;算法边界（源码强制）&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client Id and Secret&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;client-secret&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;共享密钥，&lt;code&gt;client_secret_basic&lt;/code&gt; 或 &lt;code&gt;client_secret_post&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;无签名&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Signed JWT&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;client-jwt&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;客户端私钥签名断言（= &lt;code&gt;private_key_jwt&lt;/code&gt;）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;只接受&lt;strong&gt;非对称&lt;/strong&gt;算法，否则 &lt;code&gt;Algorithm is not asymmetric&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Signed JWT with Client Secret&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;client-secret-jwt&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;用 secret 做 HMAC 签名断言（= &lt;code&gt;client_secret_jwt&lt;/code&gt;）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;只接受&lt;strong&gt;对称&lt;/strong&gt;算法，否则 &lt;code&gt;Algorithm is not symmetric&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;X.509 Certificate&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;client-x509&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;TLS 客户端证书&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;校验 Subject DN 与根 CA Subject DN&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;再加一种由 IdP 代签断言的 Signed JWT（SPIFFE JWT SVID、Kubernetes service account 这类跨信任域凭据），它的 &lt;code&gt;iss&lt;/code&gt; 不等于 &lt;code&gt;sub&lt;/code&gt;，处理逻辑独立，本文不展开——这类凭据属于工作负载身份，签发、轮换与校验方式见 &#13;&#10;&#13;&#10;&lt;a class="link link--text" href="https://idaas.xlabs.club/docs/solution-blogs/spiffe-spire-workload-identity/"&gt;SPIFFE/SPIRE 工作负载身份 IAM 落地&lt;/a&gt;。顺带说明一个常见误用：JWT-SVID 不能直接当 client assertion 用，它的 &lt;code&gt;sub&lt;/code&gt; 是签发方标识的 SPIFFE ID，不是提出请求的客户端自己。&lt;/p&gt;</description></item><item><title>Keycloak 作为 MCP 授权服务器：IAM audience 绑定与 resource indicators 落地 | IDaaS Book</title><link>https://idaas.xlabs.club/docs/solution-blogs/keycloak-mcp-authorization-server/</link><pubDate>Mon, 21 Sep 2026 23:00:00 +0800</pubDate><guid>https://idaas.xlabs.club/docs/solution-blogs/keycloak-mcp-authorization-server/</guid><description>&lt;h2 id="场景"&gt;场景&lt;/h2&gt;&#10;&lt;p&gt;MCP server 用 Keycloak 做授权服务器之后，接入方报的错几乎都是同一个：token 能签发、能验签、&lt;code&gt;iss&lt;/code&gt; 也对，但 MCP server 返回 &lt;code&gt;401 invalid_token&lt;/code&gt;，描述是「token 的 audience 不是给我的」。原因在 MCP 授权规范的一条 MUST——客户端必须在&lt;strong&gt;授权请求和 token 请求&lt;/strong&gt;里带 &lt;code&gt;resource&lt;/code&gt; 参数（&#13;&#10;&#13;&#10;&lt;a class="link link--text" href="https://datatracker.ietf.org/doc/html/rfc8707" rel="external"&gt;RFC 8707&lt;/a&gt;），MCP server 必须校验 token 是专门为它签发的；而 Keycloak 的官方文档在 26.7.4 明确写着「Keycloak cannot recognize &lt;code&gt;resource&lt;/code&gt; parameter」，MCP 2025-06-18 / 2025-11-25 / 2026-07-28 三个版本都被标为 &lt;em&gt;Partially Supported without Resource Indicators&lt;/em&gt;。&lt;/p&gt;&#10;&lt;p&gt;也就是说：客户端一定会带 &lt;code&gt;resource&lt;/code&gt;，Keycloak 默认&lt;strong&gt;静默忽略&lt;/strong&gt;它，既不报错也不按它设置 &lt;code&gt;aud&lt;/code&gt;。这一篇文章讲清两件事——怎么在官方推荐路径下把 &lt;code&gt;aud&lt;/code&gt; 绑对，以及 26.6.0 起已进入发布版、但仍属实验特性的 resource indicators，在源码层面到底做了什么、什么情况下会把已经能跑的客户端打断。&lt;/p&gt;&#10;&lt;p&gt;结论先说：&lt;strong&gt;Keycloak 的实验实现是「过滤」aud，不是「添加」aud。&lt;/strong&gt; 如果 token 里本来没有目标资源的标识，&lt;code&gt;resource&lt;/code&gt; 不会帮你补上，而是直接返回 &lt;code&gt;invalid_target&lt;/code&gt;。理解这一点，能省掉一轮线上排错。&lt;/p&gt;&#10;&lt;h2 id="适用与不适用"&gt;适用与不适用&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;场景&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;是否适用&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;说明&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MCP server 放在 Keycloak 后面，客户端是 VS Code / Claude Code / MCP Inspector&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;需要 CIMD（&lt;code&gt;client_id&lt;/code&gt; 是 URL）或 DCR 注册客户端&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;一个 realm 里有多台 MCP server，同一个 agent 客户端要分别取 token&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;aud 收敛为单值是这套机制的核心价值，跨 server 重放会被拒&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;只需要给单台 server 发 token，且不想开实验特性&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅（走官方 scope 路径）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;见下节路径 A，稳定、无需实验特性&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MCP 2025-03-26 及更早版本&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;❌&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;该版本不要求 &lt;code&gt;resource&lt;/code&gt; 与 aud 绑定，Keycloak 文档写明 &lt;em&gt;No special setup is required&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;本地 stdio 传输的 MCP server&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;❌&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;没有 HTTP 授权层，与本文无关&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;生产环境无法接受「实验特性可能破坏性变更」&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;⚠️&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;路径 A 即可满足 MCP 的 MUST/SHOULD 集合，路径 B 是增益不是前提&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="两条落地路径"&gt;两条落地路径&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;路径 A：scope 驱动（官方文档路径）&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;路径 B：resource 驱动（实验特性）&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;机制&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;用 &lt;code&gt;scope&lt;/code&gt; 触发 Audience mapper，把 MCP server 的标识&lt;strong&gt;追加&lt;/strong&gt;进 &lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;解析 &lt;code&gt;resource&lt;/code&gt; 参数，把 &lt;code&gt;aud&lt;/code&gt; &lt;strong&gt;替换&lt;/strong&gt;成该参数对应的资源&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;前置配置&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;client scope + Audience mapper&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;在 A 的基础上，额外给 MCP server 的客户端配 &lt;code&gt;resource_url&lt;/code&gt; 属性，并启用 &lt;code&gt;resource-indicators&lt;/code&gt; 特性&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;特性开关&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;不需要&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--features=resource-indicators&lt;/code&gt;（26.7.4 为 EXPERIMENTAL）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;token 的 &lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;取决于客户端请求了哪些 scope，可能是多个值&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;只剩 &lt;code&gt;resource&lt;/code&gt; 对应的单值（多值会被覆盖）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;失败模式&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;客户端没请求 scope → &lt;code&gt;aud&lt;/code&gt; 缺失 → RS 401&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;resource&lt;/code&gt; 解析不到客户端 → &lt;code&gt;invalid_target&lt;/code&gt;（token 根本不签发）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;合规状态&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;官方文档认可，但 RFC 8707 那一行仍是 Not supported&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;实现已合入（&#13;&#10;&#13;&#10;&lt;a class="link link--text" href="https://github.com/keycloak/keycloak/pull/46763" rel="external"&gt;PR #46763&lt;/a&gt;），文档未发布（&#13;&#10;&#13;&#10;&lt;a class="link link--text" href="https://github.com/keycloak/keycloak/issues/47127" rel="external"&gt;#47127&lt;/a&gt; 仍 open）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;官方文档给出的做法是路径 A：为 MCP server 声明的每个 scope 建一个 client scope，每个 scope 里放一个 Audience mapper，&lt;em&gt;Included Custom Audience&lt;/em&gt; 填 MCP server 的 URL（例如 &lt;code&gt;https://example.com/mcp&lt;/code&gt;）。客户端请求哪些 scope，&lt;code&gt;aud&lt;/code&gt; 里就有对应值。这条路能跑通，因为 MCP 客户端会从 RFC 9728 Protected Resource Metadata 里读取 &lt;code&gt;scopes_supported&lt;/code&gt; 并请求这些 scope——&lt;strong&gt;如果 MCP server 没在元数据里声明 scope，客户端就不会请求，mapper 也就不会触发&lt;/strong&gt;，这是路径 A 最常见的哑火原因。&lt;/p&gt;</description></item></channel></rss>