Keycloak v26如何从X509证书UPN中提取用户名?
Keycloak v26 从客户端证书UPN提取用户名解决方案
问题背景
配置Keycloak(v26)X509认证流程时,需从客户端证书OtherName字段(UPN + RFC822格式,值如username@active.directory.domain)提取仅含username的部分用于用户认证。当前认证流程中的「X509/Validate Username Form config」步骤,其「nom principal」字段使用完整UPN值,与系统中仅存username的用户账号不匹配,导致认证失败。
已知限制:
- Keycloak界面提供的正则选项对「Subject's Alternative Name otherName (UPN)」类型的身份源无效
- 证书DN中不包含目标用户名,UPN是唯一存储该信息的位置
已执行排查步骤
- 开启TRACE日志,确认
BCUserIdentityExtractorProvider读取到完整UPN值,用户因user_not_found错误被拒绝 - 测试多种正则表达式,均无法对UPN类型身份源生效
- 尝试通过X509字段映射用户名属性,未找到可行配置
可行解决方案
方法1:自定义UserIdentityExtractorProvider扩展
Keycloak默认的BCUserIdentityExtractorProvider不支持对UPN应用正则提取,需自定义扩展实现提取逻辑:
- 创建Java类继承
AbstractUserIdentityExtractorProvider,重写extract方法,从X509Certificate中解析UPN值,通过字符串分割或正则提取用户名部分 - 将打包后的JAR文件放入Keycloak的
providers目录 - 在Keycloak管理界面中,进入「Authentication」→「Bindings」→「X509 User Identity Extractor」,选择自定义的提取器
核心代码片段示例:
@Override public UserIdentity extract(X509Certificate certificate, Map<String, String> config) { String upn = extractUPNFromCertificate(certificate); if (upn != null && upn.contains("@")) { String username = upn.split("@")[0]; return UserIdentity.builder() .username(username) .build(); } return super.extract(certificate, config); } private String extractUPNFromCertificate(X509Certificate cert) { // 借助BouncyCastle库解析证书OtherName字段中的UPN值 // 实现ASN.1格式解析逻辑,提取对应OID(1.3.6.1.4.1.311.20.2.3)的UPN内容 }
方法2:使用X509认证流程的脚本扩展
通过Keycloak的脚本功能,在认证流程中添加「Script Authenticator」步骤,覆盖用户名提取逻辑:
- 在Keycloak管理界面中,进入「Authentication」→「Flows」,复制默认的「X509/Validate Username Form」流程
- 在复制后的流程中,添加「Script Authenticator」步骤,置于「X509/Validate Username Form config」之前
- 编写Groovy脚本,从认证上下文获取客户端证书,解析UPN并提取用户名,存入认证上下文的用户名字段
示例脚本:
import org.keycloak.authentication.AuthenticationFlowContext import java.security.cert.X509Certificate import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.DERUTF8String import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralNames def context = context as AuthenticationFlowContext X509Certificate cert = context.getHttpRequest().getAttribute("javax.servlet.request.X509Certificate") as X509Certificate if (cert != null) { byte[] sanBytes = cert.getExtensionValue("2.5.29.17") if (sanBytes != null) { ASN1InputStream asn1InputStream = new ASN1InputStream(sanBytes) def san = GeneralNames.getInstance(asn1InputStream.readObject()) for (GeneralName name : san.getNames()) { if (name.getTagNo() == GeneralName.otherName) { def otherName = name.getName() def oid = ASN1ObjectIdentifier.getInstance(otherName.getObjectAt(0)) if (oid.getId().equals("1.3.6.1.4.1.311.20.2.3")) { // UPN对应的OID def upnValue = DERUTF8String.getInstance(otherName.getObjectAt(1)).getString() if (upnValue.contains("@")) { String username = upnValue.split("@")[0] def targetUser = context.session.users().getUserByUsername(username, context.realm()) if (targetUser != null) { context.setUser(targetUser) context.success() return } } } } } } } context.failure(context.errorEvent("user_not_found"))
方法3:修改Keycloak默认实现(不推荐)
若无法自定义扩展,可修改Keycloak默认的BCUserIdentityExtractorProvider代码,添加UPN正则提取逻辑后重新编译打包。但此方法会导致后续版本升级困难,仅作为临时应急方案。
内容的提问来源于stack exchange,提问作者Paul Evans
相关产品推荐
相关产品推荐

