dgraph-io/dgraph · error
Invalid kid
Error message
Invalid kid
What it means
The token carried a `kid` header, but no key in the fetched JWK set for the current URL matches that key ID (jwkSet[i].Key(kid) returned zero keys). The library cannot find the public key needed to verify the signature, so validation fails. Often means the IdP rotated keys and the cached/local JWK set is stale, or the token comes from a different issuer.
Source
Thrown at graphql/authorization/auth.go:364
if a.isExpired(i) {
err = a.refreshJWK(i)
if err != nil {
return nil, errors.Wrap(err, "while refreshing JWK from the URL")
}
}
token, err = jwt.ParseWithClaims(
jwtStr,
&CustomClaims{authMeta: a},
func(token *jwt.Token) (interface{}, error) {
kid := token.Header["kid"]
if kid == nil {
return nil, errors.Errorf("kid not present in JWT")
}
signingKeys := a.jwkSet[i].Key(kid.(string))
if len(signingKeys) == 0 {
return nil, errors.Errorf("Invalid kid")
}
return signingKeys[0].Key, nil
},
)
if err == nil {
return token, nil
}
}
return nil, err
}
func (a *AuthMeta) validateJWTCustomClaims(jwtStr string) (*CustomClaims, error) {
var token *jwt.Token
var err error
// Verification through JWKUrl
if len(a.JWKUrls) != 0 {
token, err = a.validateThroughJWKUrl(jwtStr)View on GitHub (pinned to 759e242be6)
Solutions
- Force a JWK refresh (restart or call FetchJWKs/refreshJWK) so the cache contains the IdP's current keys.
- Confirm the JWKUrls point to the same issuer/tenant that mints the tokens.
- Compare the token's kid against the kids in the fetched JWKS JSON to spot the mismatch.
- If a rotation just happened, wait for or trigger a cache expiry so isExpired triggers refreshJWK.
- Support multiple JWK URLs (one per issuer/region) so all valid tokens find their key.
Example fix
// before: stale cache causes mismatch
// (cached jwkSet missing new kid)
// after: refresh keys when kid is unknown
signingKeys := a.jwkSet[i].Key(kid.(string))
if len(signingKeys) == 0 {
if err := a.refreshJWK(i); err == nil {
signingKeys = a.jwkSet[i].Key(kid.(string))
}
}
if len(signingKeys) == 0 {
return nil, errors.Errorf("Invalid kid: %s", kid)
} Defensive patterns
Strategy: retry
Validate before calling
// verify kid is present in current JWKS before validation
resp, _ := http.Get(jwksURL)
b, _ := io.ReadAll(resp.Body)
var set struct{ Keys []struct{ Kid string `json:"kid"` } `json:"keys"` }
json.Unmarshal(b, &set)
for _, k := range set.Keys {
if k.Kid == tokenKid { return nil }
}
return fmt.Errorf("kid %s not in JWKS; refresh keys", tokenKid) Try / catch
if err != nil && strings.Contains(err.Error(), "Invalid kid") {
if rerr := auth.FetchJWKs(); rerr == nil {
claims, err = auth.ExtractCustomClaims(ctx, jwtStr) // retry once with fresh keys
}
} Prevention
- Refresh JWK sets on unknown kid instead of failing outright
- Keep TTL for JWK cache shorter than the IdP rotation interval
- Subscribe to IdP rotation events/webhooks where available
- Verify staging and prod use matching JWKS URLs per environment
When it happens
Trigger: Inside the ParseWithClaims keyfunc after kid is present: a.jwkSet[i].Key(kid.(string)) returns an empty slice — kid not present in the key set fetched from JWKUrls[i]. Loop then tries the next JWK URL and, if all fail, validation fails.
Common situations: IdP rotated signing keys while the service cached an old JWK set; token issued by a different environment/tenant (e.g. staging token validated against prod JWKS URL); wrong JWKS URL configured; multi-region IdPs serving different keys.
Related errors
- kid not present in JWT
- while refreshing JWK from the URL
- Authorize guardian of the galaxy, extracting jwt token, erro
- expecting either JWKUrl or JWKUrls, both were given
- expecting either JWKUrl/JWKUrls or (VerificationKey, Algo),
AI-assisted analysis of dgraph-io/dgraph@759e242be6 (2026-09-01).
Data as JSON: /api/errors/f40889f2277013e6.
Report an issue: GitHub.