ory/hydra · error
ErrUnableToFindKeyID
ErrUnableToFindKeyID
Error message
specified JWK kid can not be found in the JWK sets
What it means
ErrUnableToFindKeyID is the sentinel error returned by ResolveKeyFromLocations when a JWK matching the requested kid cannot be found in any fetched JWK set. If forceKID is set and jwk.Set.LookupKeyID fails, or the set is empty so even Get(0) fails, this error (wrapped with a stack trace) is returned. It signals a key-selection failure, not a network failure.
Source
Thrown at oryx/jwksx/fetcher_v2.go:28
"github.com/ory/herodot"
"github.com/hashicorp/go-retryablehttp"
"github.com/ory/x/fetcher"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/ory/x/otelx"
"github.com/dgraph-io/ristretto/v2"
"github.com/lestrrat-go/jwx/jwk"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
var ErrUnableToFindKeyID = errors.New("specified JWK kid can not be found in the JWK sets")
type (
fetcherNextOptions struct {
forceKID string
cacheTTL time.Duration
useCache bool
httpClient *retryablehttp.Client
schemes []string
}
// FetcherNext is a JWK fetcher that can be used to fetch JWKs from multiple locations.
FetcherNext struct {
cache *ristretto.Cache[[]byte, jwk.Set]
}
// FetcherNextOption is a functional option for the FetcherNext.
FetcherNextOption func(*fetcherNextOptions)
)
// NewFetcherNext returns a new FetcherNext instance.View on GitHub (pinned to 4174065ffb)
Solutions
- Log/inspect the fetched JWK set and compare its kids with the token's kid; fix the kid value or the JWKS URL.
- Bypass/reduce the cache TTL so a freshly rotated key is re-fetched from the issuer.
- Ensure the issuer publishes the signing key (check the JWKS endpoint directly with curl) and wait for rotation propagation.
- If forceKID isn't required, drop it and fall back to the first key only when the set is known to contain exactly one key.
Example fix
// before
key, err := fetcher.ResolveKeyFromLocations(ctx, []jwksx.Fetcher{f}, &jwksx.NextOptions{ForceKID: "old-kid"})
// after
kid := tokenFromHeader("kid") // read from the actual token
key, err := fetcher.ResolveKeyFromLocations(ctx, []jwksx.Fetcher{f}, &jwksx.NextOptions{ForceKID: kid}) Defensive patterns
Strategy: validation
Validate before calling
// before resolving, verify the kid exists in the fetched set
set, err := f.Fetch(ctx)
if err != nil { return err }
if opts.ForceKID != "" {
if _, found := set.LookupKeyID(opts.ForceKID); !found {
return fmt.Errorf("kid %q not in JWKS (available kids: %v)", opts.ForceKID, kidsOf(set))
}
} else if set.Len() == 0 {
return fmt.Errorf("JWKS at %s contains no keys", jwksURL)
} Try / catch
key, err := fetcher.ResolveKeyFromLocations(ctx, locs, opts)
if err != nil {
if errors.Is(err, jwksx.ErrUnableToFindKeyID) {
// refresh cache / re-fetch JWKS once, then retry the resolution
return resolveAfterRefresh(ctx, locs, opts)
}
return err
} Prevention
- Always take the kid from the token header rather than hardcoding forceKID
- Keep JWKS cache TTL well below key rotation intervals and implement refresh-on-miss
- Monitor issuer key rotation and ensure old keys stay published during overlap windows
- Alert on empty JWKS responses from the issuer
When it happens
Trigger: Calling ResolveKeyFromLocations with opts.forceKID set to a kid absent from the fetched JWKS, or with forceKID empty on a JWK set that contains zero keys (set.Get(0) not found).
Common situations: Tokens signed with a kid rotated out of the JWKS before old tokens expire, typos/mismatched kid between issuer and fetcher config, misconfigured JWKS URL returning an empty or wrong-tenant key set, cache serving a stale empty set.
Related errors
- jwt from authorization HTTP header is expecting string value
- unable to find JSON Web Key with ID: %s
- header, body and signature must all be set
- Token is expired
- Token used before issued
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/19286a4a37286f69.
Report an issue: GitHub.