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

  1. Force a JWK refresh (restart or call FetchJWKs/refreshJWK) so the cache contains the IdP's current keys.
  2. Confirm the JWKUrls point to the same issuer/tenant that mints the tokens.
  3. Compare the token's kid against the kids in the fetched JWKS JSON to spot the mismatch.
  4. If a rotation just happened, wait for or trigger a cache expiry so isExpired triggers refreshJWK.
  5. 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

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


AI-assisted analysis of dgraph-io/dgraph@759e242be6 (2026-09-01). Data as JSON: /api/errors/f40889f2277013e6. Report an issue: GitHub.