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

  1. Log/inspect the fetched JWK set and compare its kids with the token's kid; fix the kid value or the JWKS URL.
  2. Bypass/reduce the cache TTL so a freshly rotated key is re-fetched from the issuer.
  3. Ensure the issuer publishes the signing key (check the JWKS endpoint directly with curl) and wait for rotation propagation.
  4. 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

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


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/19286a4a37286f69. Report an issue: GitHub.