Keycloak 21.0.0调用外部JWK API验签时提示公钥未找到
问题背景
在Keycloak 21.0.0自定义认证流程中,调用外部JWK API验签时出现公钥未找到的错误,日志提示:
WARN [org.keycloak.keys.infinispan.InfinispanPublicKeyStorageProvider]PublicKey wasn't found in the storage. Requested kid:'sig-2024-10-07-DB'. Available kids: '[]'
WARN [org.keycloak.services]KC-SERVICES0097: Invalid request: java.lang.RuntimeException: Failed to verify signature on 'request' object
核心问题排查与修复
1. 验签逻辑致命错误:直接使用JWT Header的Base64编码作为kid
在verifySignature方法中,代码错误地将jwtParts[0](JWT Header的Base64URL编码串)当作kid去查找JWK,完全不符合JWT规范。正确做法是解码Header部分,提取其中的kid字段。
错误代码片段:
JWK jwk = euroInfoJwkSet.getKeyByKeyId(jwtParts[0]);
修复后的代码片段:
// 解码JWT Header,提取kid String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0]), StandardCharsets.UTF_8); JsonObject header = JsonParser.parseString(headerJson).getAsJsonObject(); String kid = header.get("kid").getAsString(); JWK jwk = euroInfoJwkSet.getKeyByKeyId(kid);
2. 检查外部JWK API返回内容
- 直接调用
https://request/jwk_request.cgi?client_id=5g4h4r8r,确认返回的JWK Set中是否包含日志提到的sig-2024-10-07-DB - 验证返回JSON格式是否符合JWK Set标准(顶级key为
keys,值为JWK对象数组)
3. 增加JWK空值判断逻辑
当前代码未处理getKeyByKeyId返回null的情况,会直接触发空指针异常。需补充判空:
if (jwk == null) { throw new RuntimeException("Public key not found for kid: " + kid); }
4. 规避Keycloak内置公钥存储干扰
日志中提到的InfinispanPublicKeyStorageProvider是Keycloak内置公钥存储,若完全使用自定义外部JWK验签,需确认自定义流程未误触发内置验签逻辑,确保所有验签操作通过自定义代码完成。
5. 优化JWK获取性能(可选)
每次认证都调用外部API获取JWK Set会影响性能,还可能触发API限流。可基于cache-control响应头或定时策略,将JWK Set缓存到本地或Keycloak内置缓存中。
完整修复后的verifySignature方法示例
private boolean verifySignature(String jwt, JWKSet euroInfoJwkSet) throws Exception { String[] jwtParts = jwt.split("\\."); if (jwtParts.length != 3) { throw new IllegalArgumentException("Invalid JWT format"); } // 解码Header提取kid String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0]), StandardCharsets.UTF_8); JsonObject header = JsonParser.parseString(headerJson).getAsJsonObject(); if (!header.has("kid")) { throw new IllegalArgumentException("JWT Header missing kid field"); } String kid = header.get("kid").getAsString(); // 获取对应JWK并验证 JWK jwk = euroInfoJwkSet.getKeyByKeyId(kid); if (jwk == null) { throw new RuntimeException("Public key not found for kid: " + kid); } if (!(jwk instanceof RSAKey)) { throw new IllegalArgumentException("Unsupported key type: " + jwk.getKeyType()); } PublicKey publicKey = ((RSAKey) jwk).toPublicKey(); Signature sig = Signature.getInstance("SHA256withRSA"); sig.initVerify(publicKey); sig.update((jwtParts[0] + "." + jwtParts[1]).getBytes(StandardCharsets.UTF_8)); byte[] signatureBytes = Base64.getUrlDecoder().decode(jwtParts[2]); return sig.verify(signatureBytes); }
内容的提问来源于stack exchange,提问作者mohamed amine salah

