fix(sso): decode JWT header manually to extract kid
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (java-kotlin) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
CodeQL Advanced / Analyze (python) (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Gateway Docker Image CI / Build and Push Gateway Docker Image (push) Canceled after 0s

jjwt 0.12 的无密钥 parse() 对已签名 JWT 抛 UnsupportedJwtException,
改为直接 base64url 解码 header 段取 kid。
This commit is contained in:
apescale
2026-08-31 11:51:12 +08:00
parent 9b2ce37ceb
commit 09b04a77fb
@@ -1,6 +1,8 @@
package com.deepdata.gateway.infrastructure.casdoor;
import com.deepdata.gateway.infrastructure.casdoor.dto.JwksResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
@@ -12,6 +14,7 @@ import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
@@ -47,6 +50,8 @@ public class CasdoorIdTokenVerifier {
/** JWKS 缓存(kid → RSAPublicKey),证书轮转时靠 TTL / kid 未命中强制刷新 */
private volatile CacheEntry cache;
private static final ObjectMapper MAPPER = new ObjectMapper();
private record CacheEntry(long fetchedAtMillis, Map<String, RSAPublicKey> keys) {}
public CasdoorIdTokenVerifier(CasdoorProperties props) {
@@ -127,7 +132,7 @@ public class CasdoorIdTokenVerifier {
}
private Jws<Claims> parseWithKid(String idToken, Map<String, RSAPublicKey> keys) {
String kid = (String) Jwts.parser().build().parse(idToken).getHeader().get("kid");
String kid = headerKid(idToken);
RSAPublicKey key = keys.get(kid);
if (key == null) {
log.warn("casdoor jwks: kid [{}] not cached, size={}", kid, keys.size());
@@ -145,6 +150,28 @@ public class CasdoorIdTokenVerifier {
}
}
/**
* 直接 base64url 解码 JWT header 段取 {@code kid}。
* 不能用 {@code Jwts.parser().build().parse(...)}:jjwt 0.12 对已签名 JWT
* 在未提供验签密钥时会抛 {@code UnsupportedJwtException},无法仅取 header。
*/
private static String headerKid(String idToken) {
int firstDot = idToken.indexOf('.');
if (firstDot <= 0) {
throw new CasdoorIdTokenException("id_token missing JWS header segment");
}
try {
String headerJson = new String(
Base64.getUrlDecoder().decode(idToken.substring(0, firstDot)),
StandardCharsets.UTF_8);
return MAPPER.readTree(headerJson).path("kid").asText(null);
} catch (CasdoorIdTokenException e) {
throw e;
} catch (Exception e) {
throw new CasdoorIdTokenException("id_token header unparsable: " + e.getMessage(), e);
}
}
private Mono<Map<String, RSAPublicKey>> loadKeys(boolean forced) {
CacheEntry current = cache;
long ttlMillis = props.getJwksCacheSeconds() * 1000L;