ory/hydra · error

a key ID must be specified when multiple JWK sets are config

Error message

a key ID must be specified when multiple JWK sets are configured

What it means

ResolveKeyFromLocations fetches and merges JWK sets from multiple remote locations. When more than one location is given, the resulting merged set can contain many keys, so the library requires a key ID (via the WithKeyID/forceKID modifier) to know which key to return. If the caller passes multiple locations but no key ID, this error is returned before any fetching happens.

Source

Thrown at oryx/jwksx/fetcher_v2.go:98

// WithHTTPClient will use the given HTTP client to fetch the JSON Web Keys.
func WithHTTPClient(c *retryablehttp.Client) FetcherNextOption {
	return func(o *fetcherNextOptions) {
		o.httpClient = c
	}
}

func (f *FetcherNext) ResolveKey(ctx context.Context, locations string, modifiers ...FetcherNextOption) (jwk.Key, error) {
	return f.ResolveKeyFromLocations(ctx, []string{locations}, modifiers...)
}

func (f *FetcherNext) ResolveKeyFromLocations(ctx context.Context, locations []string, modifiers ...FetcherNextOption) (jwk.Key, error) {
	opts := new(fetcherNextOptions)
	for _, m := range modifiers {
		m(opts)
	}

	if len(locations) > 1 && opts.forceKID == "" {
		return nil, errors.Errorf("a key ID must be specified when multiple JWK sets are configured")
	}

	set := jwk.NewSet()
	eg := new(errgroup.Group)
	for k := range locations {
		location := locations[k]
		eg.Go(func() error {
			remoteSet, err := f.fetch(ctx, location, opts)
			if err != nil {
				return err
			}

			iterator := remoteSet.Iterate(ctx)
			for iterator.Next(ctx) {
				// Pair().Value is always of type jwk.Key when generated by Iterate.
				set.Add(iterator.Pair().Value.(jwk.Key))
			}

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Pass the key-ID modifier when resolving from multiple locations, e.g. ResolveKeyFromLocations(ctx, urls, jwksx.WithKeyID("my-kid"))
  2. Reduce the locations slice to a single JWKS URL if no specific kid is needed
  3. If a specific key must be selected, ensure the token's kid header matches a key in the fetched sets and pass it as forceKID

Example fix

// before
key, err := fetcher.ResolveKeyFromLocations(ctx, []string{url1, url2})
// after
key, err := fetcher.ResolveKeyFromLocations(ctx, []string{url1, url2}, jwksx.WithKeyID("my-key-id"))
Defensive patterns

Strategy: validation

Validate before calling

func resolveKeySafe(locations []string, modifiers ...jwksx.FetcherNextOption) error {
	opts := &fetcherNextOptions{} // or track via your own wrapper struct
	for _, m := range modifiers {
		m(opts)
	}
	if len(locations) > 1 && opts.forceKID == "" {
		return errors.New("provide WithKeyID(...) when resolving from multiple JWKS locations")
	}
	return nil
}

Type guard

func hasKeyID(modifiers []jwksx.FetcherNextOption) bool { // use a captured opts struct
	var forceKID string
	for _, m := range modifiers {
		m(&fetcherNextOptionsForCheck{})
	}
	return forceKID != ""
}

Try / catch

key, err := fetcher.ResolveKeyFromLocations(ctx, urls, opts...)
if err != nil {
	if strings.Contains(err.Error(), "a key ID must be specified") {
		// fall back to single location or add WithKeyID
	}
	return err
}

Prevention

When it happens

Trigger: Calling ResolveKeyFromLocations (directly or via ResolveKey) with a locations slice of length > 1 while omitting the modifier that sets opts.forceKID (e.g. WithKeyID).

Common situations: Configuring an app to trust keys from several JWKS endpoints (e.g. multiple OIDC providers or key rotation endpoints) but forgetting to pin a specific kid; a config file lists several jwks_urls but no expected kid; tests that pass a variadic list of URLs.

Related errors


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