一言一诺 發表於 2026-4-23 10:24:00

keycloak~实现OAuth 2.0 Token Exchange

<ul>
<li>https://datatracker.ietf.org/doc/html/rfc8693</li>
<li>https://www.keycloak.org/securing-apps/token-exchange</li>
</ul>
<h1 id="keycloak-的令牌交换功能如下">Keycloak 的令牌交换功能如下:</h1>
<ul>
<li>在同一个领域中,客户端可以将为特定客户端创建的现有 Keycloak 令牌交换为针对不同客户端的新令牌。</li>
<li>客户可以将现有的 Keycloak 令牌兑换为外部令牌,例如关联的 Facebook 账户令牌。</li>
<li>客户端可以将外部令牌兑换为 Keycloak 令牌。</li>
<li>客户可以冒充用户。</li>
<li></li>
</ul>
<p>在Keycloak 14.0.0版本中,即使启用了<code>-Dkeycloak.profile.feature.token_exchange=enabled</code>预览功能,<strong>客户端配置页面中确实不会直接显示"exchange token"选项</strong>。这是因为旧版令牌交换(V1)需要额外的细粒度权限配置。</p>
<h2 id="keycloak14中开启交换token功能">keycloak14中开启交换Token功能</h2>
<h3 id="keycloak-1400需要同时启用两个预览功能需要使用下划线的名字">Keycloak 14.0.0需要同时启用两个预览功能,需要使用下划线的名字:</h3>
<pre><code class="language-bash">-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled
-Dkeycloak.profile.feature.token_exchange=enabled
</code></pre>
<p>重启Keycloak服务使配置生效。</p>
<h3 id="为客户端开启token-exchnage能力">为客户端开启token exchnage能力</h3>
<p><img src="https://img2024.cnblogs.com/blog/118538/202604/118538-20260423084510804-1119451565.png" alt="image" loading="lazy"></p>
<h3 id="添加一个wso2的idp认证服务">添加一个wso2的IDP认证服务</h3>
<p><img src="https://img2024.cnblogs.com/blog/118538/202604/118538-20260423100524942-318896791.png" alt="image" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202604/118538-20260423100559816-1781399888.png" alt="image" loading="lazy"></p>
<h3 id="idp中permission的配置">IDP中Permission的配置</h3>
<p><img src="https://img2024.cnblogs.com/blog/118538/202604/118538-20260423100253314-1330839166.png" alt="image" loading="lazy"></p>
<h3 id="测试步骤">测试步骤</h3>
<p>1 用户在keycloak平台登录</p>
<pre><code>curl -X POST 'https://kc.com/auth/realms/demoo/protocol/openid-connect/token' -H 'Accept: application/json'--data-urlencode 'grant_type=password' --data-urlencode 'username=test' --data-urlencode 'password=123456' --data-urlencode 'client_id=wso2' --data-urlencode 'client_secret=abf99c64-db24-43fb-b63b-c60213b8052f' --data-urlencode 'scope=openid'
</code></pre>
<p>2 用户在wso2平台通过urn:ietf:params:oauth:grant-type:jwt-bearer方式生成对应的token</p>
<pre><code>curl -X POST 'https://wso2.com/oauth2/token' -H 'Content-Type: application/json' -H 'Content-Type: application/json' -u '3N5OKJnQozVc8pPKWHSf1CTLgwQa:HEj3QGF3bPxLIJbTpEmanW5mIjEa' --basic -d '{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "kc-access-token",
"scope": "openid apim:subscribe"
}'
</code></pre>
<p>3 用户在keycloak平台根据wso2的token来校验成keycloak的token,注意wso2中的客户端生成的token类型可以是<code>jwt类型</code>和<code>default</code>类型,但scope中必须包含openid的,否则无法获取oauth2/userinfo接口。</p>
<pre><code>curl -X POST 'https://kc.com/auth/realms/fabao/protocol/openid-connect/token' -H 'Content-Type: application/x-www-form-urlencoded'-u 'wso2:abf99c64-db24-43fb-b63b-c60213b8052f' --basic --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' --data-urlencode 'subject_token=wso2-jwt-token' --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' --data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:access_token' --data-urlencode 'scope=openid' --data-urlencode 'subject_issuer=wso2'
</code></pre>
<h2 id="一点优化的空间">一点优化的空间</h2>
<ul>
<li>目前wso2中,api接口使用的token是用户的应用的token,它是一种客户端认证</li>
<li>目前客户端认证不支持scope的openid,导致无法获取当前应用对应的用户/oauth2/userinfo接口,导致这个token不支持token exchange</li>
<li>但是,如果你手动向<code>idn_oauth2_access_token_scope</code>表添加openid的scope,它就可以调用/oauth2/userinfo接口了,有几秒缓存。<br>
![](<img src="https://img2024.cnblogs.com/blog/118538/202604/118538-20260427143241194-1570983421.png" alt="image" loading="lazy"></li>
</ul>
<h3 id="优化已经找到方法">优化已经找到方法</h3>
<p>为sp应用添加openid的scope,在acp的deployment.toml中,添加相关配置,以开启openid的功能</p>
<pre><code>
allowed_scopes = ["openid", "default", "am_application_scope"]


enable_openid_scope = true
</code></pre>
<p>重启服务后,进行sp的客户端认证,返回值会有openid和default,并有id_token的内容</p>
<pre><code>{
"access_token": "3baba7f7-3437-345e-89e3-ee9ea9ae6c05",
"scope": "default openid",
"id_token": "eyJ4NXQiOiJNekF6TVRGak9EUTFNRE5qT1RVMVpEQTROR1E1TURrell6RTNNV0k0TW1SbFpHVTNZelpqWWprNFpHUmtNMlJoTW1Jd01qQXhZekpsTUdKak5qZG1OdyIsImtpZCI6Ik16QXpNVEZqT0RRMU1ETmpPVFUxWkRBNE5HUTVNRGt6WXpFM01XSTRNbVJsWkdVM1l6WmpZams0WkdSa00yUmhNbUl3TWpBeFl6SmxNR0pqTmpkbU53X1JTMjU2IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoiWEIwUWxaWTZ0TVZTWC05U2VrYmpyUSIsImF1ZCI6InNIaXN4ajIzc0JxN2ZZMDcwd2hWTzdwd1VCd2EiLCJzdWIiOiJiMzk4MjcyYi0zYTZjLTQ0MGYtOTQ5NS04OGMzYzE3NThhMzkiLCJuYmYiOjE3NzcyODA0MzksImF6cCI6InNIaXN4ajIzc0JxN2ZZMDcwd2hWTzdwd1VCd2EiLCJhbXIiOlsiY2xpZW50X2NyZWRlbnRpYWxzIl0sImlzcyI6Imh0dHBzOi8vdGVzdC1hcGltLnBrdWxhdy5jb20vb2F1dGgyL3Rva2VuIiwiZXhwIjoxNzgwODgwNDM5LCJpYXQiOjE3NzcyODA0MzksImp0aSI6IjdiYzYyY2U1LTFjODktNDliOS1iNGVmLTY2YmY5NzUxMTEzZSJ9.fPlMbjUvSw9Y59RHe-cIW1MA0DHTnOJ_0-S5Bxoc6yqF6VBa8xZAyjjFtaKxXULMqpcH0f-qKsxoT9X7LnEwGgUWhBHVKmuxK0WHDwsHIyEo1yscVc-Ap6HpfRf_cu8nX0taW0wPdeAdUCgPVGUas2d8YDnz5bKVSlydk9xVxwqvvyJvA_GcpOJ2W14Zk34bjlqOWN6jcaXkUfwrAZbheZudnKVWdI_hYOq0bd94pkRF7-af0b_h7rk4BuAatu_Z8qPhClvWZLpg1hMFRWwobOvBbuijmjMPkcsy0-JSLhc-i5_YXQ7q-U_uuDi1AiEijVlfHb7Uij5zQ98S_jF1Mw",
"token_type": "Bearer",
"expires_in": 3600000
}
</code></pre>
<h2 id="交换token配置步骤详解">交换token配置步骤详解</h2>
<h3 id="1-创建令牌交换策略">1. 创建令牌交换策略</h3>
<p>在客户端的Permissions页面中,找到<code>token-exchange</code>权限,点击进入配置:</p>
<ol>
<li>
<p><strong>创建策略</strong>:</p>
<ul>
<li>点击<code>Create Policy</code>按钮</li>
<li>选择策略类型为<strong>Client</strong></li>
<li>策略名称:例如<code>wso2-token-exchange-policy</code></li>
</ul>
</li>
<li>
<p><strong>配置策略</strong>:</p>
<ul>
<li>在策略配置页面,找到<strong>Clients</strong>选项</li>
<li>添加允许进行令牌交换的客户端(即WSO2 APIM的客户端ID)</li>
<li>保存策略</li>
</ul>
</li>
<li>
<p><strong>绑定策略</strong>:</p>
<ul>
<li>返回到<code>token-exchange</code>权限页面</li>
<li>将刚创建的策略绑定到该权限</li>
<li>确保权限状态为<strong>Enabled</strong></li>
</ul>
</li>
</ol>
<h3 id="2-配置身份提供者关键步骤">2. 配置身份提供者(关键步骤)</h3>
<p>由于WSO2 APIM是外部令牌颁发者,需要在Keycloak中将其配置为受信任的Identity Provider:</p>
<ol>
<li>进入<strong>Identity Providers</strong>菜单</li>
<li>点击<strong>Add provider</strong>,选择<strong>OpenID Connect v1.0</strong></li>
<li>配置信息:
<ul>
<li><strong>Alias</strong>: <code>wso2-apim</code>(自定义名称)</li>
<li><strong>Display Name</strong>: <code>WSO2 APIM</code></li>
<li><strong>Authorization URL</strong>: <code>https://test-apim.pkulaw.com/oauth2/authorize</code></li>
<li><strong>Token URL</strong>: <code>https://test-apim.pkulaw.com/oauth2/token</code></li>
<li><strong>Client ID</strong>: WSO2 APIM的客户端ID</li>
<li><strong>Client Secret</strong>: WSO2 APIM的客户端密钥</li>
<li><strong>Issuer</strong>: <code>https://test-apim.pkulaw.com</code>(WSO2 APIM的颁发者URL)</li>
</ul>
</li>
</ol>
<h3 id="3-配置客户端映射">3. 配置客户端映射</h3>
<p>在WSO2 APIM的客户端配置中,确保:</p>
<ul>
<li>客户端类型为<strong>Confidential</strong></li>
<li>启用了<strong>Service Accounts Enabled</strong></li>
<li>配置了正确的重定向URI</li>
</ul>
<h2 id="发送令牌交换请求">发送令牌交换请求</h2>
<p>配置完成后,使用以下请求格式交换令牌:</p>
<pre><code class="language-bash">curl -X POST 'https://your-keycloak-host/auth/realms/{realm}/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Authorization: Basic {base64 kc_client_id:kc_client_secret)}' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&amp;
      subject_token={WSO2_APIM_TOKEN}&amp;
      subject_token_type=urn:ietf:params:oauth:token-type:access_token&amp;
      requested_token_type=urn:ietf:params:oauth:token-type:access_token&amp;
      subject_issuer=wso2&amp;
      scope=openid profile email'
</code></pre>
<h2 id="关键参数说明">关键参数说明</h2>
<ul>
<li><strong>subject_token</strong>: WSO2 APIM的访问令牌(经过我的测试,使用jwt和default方式的token都是可以的)</li>
<li><strong>subject_issuer</strong>: 这是keycloak中定义的idp的Alias</li>
<li><strong>audience</strong>: keycloak目标客户端ID(您希望获得的Keycloak令牌的受众),可以省略</li>
<li><strong>scope</strong>: 请求的权限范围,必须包含openid,否则无法获取用户信息</li>
</ul>
<h2 id="常见问题处理">常见问题处理</h2>
<ol>
<li>
<p><strong>"Client not allowed to exchange"</strong>:</p>
<ul>
<li>确认客户端已添加到token-exchange策略中</li>
<li>检查客户端是否为Confidential类型</li>
</ul>
</li>
<li>
<p><strong>"Invalid subject token"</strong>:</p>
<ul>
<li>验证WSO2 APIM令牌格式</li>
<li>确认<code>subject_issuer</code>参数与令牌中的<code>iss</code>一致</li>
<li>检查令牌是否过期</li>
</ul>
</li>
<li>
<p><strong>"Subject token issuer not trusted"</strong>:</p>
<ul>
<li>确认Identity Provider配置正确</li>
<li>检查颁发者URL是否完全匹配</li>
</ul>
</li>
<li>
<p>** “User already exists”**:</p>
<ul>
<li>请通过wso2生成的token来访问wso2的用户端点是否正常,一般地址为<code>oauth2/userinfo</code></li>
</ul>
</li>
</ol>
<h2 id="注意事项">注意事项</h2>
<ol>
<li><strong>性能考虑</strong>:令牌交换涉及额外的网络调用和验证,可能影响性能</li>
<li><strong>安全性</strong>:确保只在受信任的客户端间进行令牌交换</li>
<li><strong>版本限制</strong>:Keycloak 14.0.0的令牌交换功能可能不如新版本完善</li>
</ol>
<p>如果配置后仍遇到问题,建议提供具体的错误信息以便进一步排查。</p>
<h2 id="未解决的问题2026-04-24">未解决的问题2026-04-24</h2>
<p>当keycloak中的本地用户,它的用户名与wso2的token中sub(类型于uuid)同名时,你的wso2交换token将无法完成,因为在交换token时,它会将wso2中sub作为用户名写到keycloak本地用户表,当写时有重复时,会出现错误,如下</p>
<pre><code>{
"error": "invalid_token",
"error_description": "User already exists"
}
</code></pre>
<p>由于交换token是后端完成的,所以是无交互的动作,这时你在idp中定义的<code>first login flow</code>是没有执行的,如果找一个方法解决让它执行,我们就可以把它和kc本地用户做映射了,问题也就解决了。</p>
<h3 id="解决方法">解决方法</h3>
<p><strong>问题原因</strong><br>
外部 token exchange(TokenEndpoint.importUserFromExternalIdentity / exchangeExternalToken)在 getUserByFederatedIdentity 查不到 (IdP, sub) 时,会按 email 或 username 去建用户;若本地已有同名用户,就直接抛 invalid_token + "User already exists"。<br>
浏览器里的 first broker login 则会让用户确认后把 IdP 绑到已有账号,而 token exchange 没有这条路径,所以会出现你描述的行为。</p>
<p><strong>实现说明</strong><br>
在 TokenEndpoint.java 中做了这些事:</p>
<ol>
<li>新增 autolinkLocalUserForExternalTokenExchange</li>
</ol>
<ul>
<li>在本地已存在用户ID或者 email(在不允许重复 email 的 realm 下)或 username 冲突的用户时,不再抛 “User already exists”。</li>
<li>为该用户 addFederatedIdentity,把当前 token 的 sub 与 IdP 关联起来(等价于“静默完成首次绑定”的一次性效果)。</li>
<li>若该用户 已经 绑了同一 IdP,但 联邦里的 userId(即 sub)与当前 token 不一致,则仍拒绝,并返回更明确的<br>
"User already linked to this identity provider with a different account",避免把两个不同 IdP 身份错误绑到同一账号。</li>
</ul>
<ol start="2">
<li>重构 importUserFromExternalIdentity</li>
</ol>
<ul>
<li>用 registerNew 区分 新注册 与 已存在(含通过 lookup 或 autolink 得到)。</li>
<li>非新注册时统一走 updateBrokeredUser + mapper 的 delegateUpdateBrokeredUser(与原来“已按联邦找到用户”的分支一致),保证属性、token、mapper 与登录 broker 后更新一致。</li>
</ul>
<p>这样:当 IdP 的 sub 在 Keycloak 里还没有对应联邦记录,但 计算出的 username(或 email)与本地用户冲突 时,会 复用该本地用户并建立关联,而不再报 "User already exists"。</p>
<p>子类如需定制策略,可 override autolinkLocalUserForExternalTokenExchange。</p>
<h2 id="错误总结">错误总结</h2>
<h3 id="1-认证类型不支持这是没开启预览版的token-exchange">1 认证类型不支持,这是没开启预览版的token exchange</h3>
<pre><code>-Dkeycloak.profile.feature.token_exchange=enabled
-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled
</code></pre>
<h3 id="2-客户端配置了token-exchange但在idp中没有配置">2 客户端配置了token exchange,但在IDP中没有配置</h3>
<pre><code>Client not allowed to exchange
</code></pre>
<h3 id="3-wso2-idp用户endpoint错误这是生成token时scope中缺少openid">3 wso2-idp用户endpoint错误,这是生成token时,scope中缺少openid</h3>
<pre><code>user info call failure
</code></pre>
<h3 id="4-获取token时audience的值写成了wso2客户端id这个应该省略或者用keycloak的client_id但一般这个值是在basic认证中写的">4 获取token时,audience的值写成了wso2客户端ID,这个应该省略,或者用keycloak的client_id,但一般这个值是在basic认证中写的</h3>
<pre><code>{
"error": "invalid_client",
"error_description": "Audience not found"
}
</code></pre>
<h3 id="5-交换token时没有使用basic认证将kc的client_idclientsecret进行传递">5 交换token时,没有使用basic认证,将kc的client_id:clientsecret进行传递</h3>
<pre><code>{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}
</code></pre>


</div>
<div id="MySignature" role="contentinfo">
    <p></p>
<div class="navgood">
<p>作者:仓储大叔,张占岭,<br>
荣誉:微软MVP<br>QQ:853066980</p>

<p><strong>支付宝扫一扫,为大叔打赏!</strong>
<br><img src="https://images.cnblogs.com/cnblogs_com/lori/237884/o_IMG_7144.JPG"></p>
</div><br><br>
来源:https://www.cnblogs.com/lori/p/19913491
頁: [1]
查看完整版本: keycloak~实现OAuth 2.0 Token Exchange