在Keycloak请求服务账户Token时传递自定义参数到JWT
在Keycloak Client Credentials模式下添加自定义请求参数到JWT Claim
需求场景
通过client_credentials模式请求Access Token时,传递自定义表单参数(如foo=bar),使该参数成为JWT中的Claim:
$ http --form --auth myclient:mysecret POST http://localhost:7070/realms/test/protocol/openid-connect/token \ grant_type=client_credentials \ foo=bar
期望JWT包含:
{ "iss": "http://localhost:7070/auth/realms/test", ... "clientId": "myclient", "foo": "bar" }
已尝试但无效的方案
- 自定义请求表单参数被Keycloak直接忽略
- 自定义Scope返回"Invalid scope"错误
- 启用实验性Dynamic Scopes并配置
foo:*未生效 - 基于
AbstractOIDCProtocolMapper的Java扩展无法直接获取请求的表单/查询参数
可行解决方案:自定义Grant Handler + Protocol Mapper
Keycloak默认不会保留token端点的自定义参数,需通过扩展Grant Handler捕获参数并存储,再通过Protocol Mapper将存储的参数注入JWT。
步骤1:实现自定义Client Credentials Grant Handler
该Handler会拦截client_credentials类型的token请求,提取自定义参数并存储到Client Session的notes中:
import org.keycloak.OAuth2Constants; import org.keycloak.models.ClientSession; import org.keycloak.models.ClientSessionContext; import org.keycloak.protocol.oidc.grants.AbstractOAuth2GrantHandler; import org.keycloak.protocol.oidc.grants.OAuth2Request; import org.keycloak.services.resources.Response; import javax.ws.rs.core.MultivaluedMap; public class CustomClientCredentialsGrant extends AbstractOAuth2GrantHandler { @Override public Response createAccessToken(OAuth2Request request) { // 获取请求中的自定义表单参数 MultivaluedMap<String, String> formParams = request.getFormParameters(); String fooValue = formParams.getFirst("foo"); // 获取当前客户端会话 ClientSessionContext clientSessionCtx = request.getClientSessionContext(); ClientSession clientSession = clientSessionCtx.getClientSession(); // 将自定义参数存入会话note(可支持多个参数,此处以foo为例) if (fooValue != null) { clientSession.setNote("custom_foo", fooValue); } // 调用父类逻辑生成Token return super.createAccessToken(request); } @Override public String getGrantType() { // 覆盖默认的client_credentials grant type return OAuth2Constants.CLIENT_CREDENTIALS; } }
步骤2:实现自定义Protocol Mapper
该Mapper从Client Session的notes中读取参数,添加到JWT的Claim中:
import org.keycloak.models.ClientSession; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSession; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCToken; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.protocol.oidc.mappers.ProtocolMapper; import java.util.ArrayList; import java.util.List; public class CustomSessionNoteClaimMapper extends AbstractOIDCProtocolMapper implements ProtocolMapper { private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>(); static { // 配置项:Claim名称 ProviderConfigProperty claimNameProp = new ProviderConfigProperty(); claimNameProp.setName("claim_name"); claimNameProp.setLabel("Claim 名称"); claimNameProp.setType(ProviderConfigProperty.STRING_TYPE); claimNameProp.setHelpText("要添加到Token中的Claim名称"); CONFIG_PROPERTIES.add(claimNameProp); // 配置项:Session Note的Key ProviderConfigProperty noteKeyProp = new ProviderConfigProperty(); noteKeyProp.setName("note_key"); noteKeyProp.setLabel("客户端会话Note的Key"); noteKeyProp.setType(ProviderConfigProperty.STRING_TYPE); noteKeyProp.setHelpText("存储自定义参数的Session Note键名"); CONFIG_PROPERTIES.add(noteKeyProp); } @Override public String getDisplayCategory() { return TOKEN_MAPPER_CATEGORY; } @Override public String getDisplayType() { return "自定义客户端会话Note Claim"; } @Override public String getHelpText() { return "将客户端会话Note中的值作为Claim添加到Token中"; } @Override public List<ProviderConfigProperty> getConfigProperties() { return CONFIG_PROPERTIES; } @Override public String getId() { return "custom-session-note-claim-mapper"; } @Override protected void setClaim(OIDCToken token, ProtocolMapperModel mappingModel, UserSession userSession, org.keycloak.models.KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { String claimName = mappingModel.getConfig().get("claim_name"); String noteKey = mappingModel.getConfig().get("note_key"); if (claimName == null || noteKey == null) { return; } ClientSession clientSession = clientSessionCtx.getClientSession(); String claimValue = clientSession.getNote(noteKey); if (claimValue != null) { token.getOtherClaims().put(claimName, claimValue); } } }
步骤3:打包与部署
- 创建Maven项目,添加Keycloak对应版本的依赖(以16.1.1为例):
<dependencies> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-core</artifactId> <version>16.1.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-server-spi</artifactId> <version>16.1.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-server-spi-private</artifactId> <version>16.1.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-services</artifactId> <version>16.1.1</version> <scope>provided</scope> </dependency> </dependencies>
- 打包成jar文件,放入Keycloak的
providers目录。 - 重启Keycloak:
- Keycloak 16:执行
./standalone.sh(或对应环境的启动脚本) - Keycloak 20+:执行
./kc.sh build && ./kc.sh start
- Keycloak 16:执行
步骤4:客户端配置
- 进入目标客户端的Mappers页面,点击Create。
- 在Mapper Type中选择刚才创建的
自定义客户端会话Note Claim。 - 配置参数:
- Claim 名称:填写
foo - 客户端会话Note的Key:填写
custom_foo
- Claim 名称:填写
- 保存配置,确保客户端已启用
client_credentials授权类型。
验证
重新发送测试请求,解析返回的Access Token,即可看到foo: "bar"已加入JWT的Claims中。
内容的提问来源于stack exchange,提问作者lathspell
相关产品推荐
相关产品推荐

