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
- Pass the key-ID modifier when resolving from multiple locations, e.g. ResolveKeyFromLocations(ctx, urls, jwksx.WithKeyID("my-kid"))
- Reduce the locations slice to a single JWKS URL if no specific kid is needed
- 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
- Always pair multi-location JWKS configs with an explicit kid
- Centralize JWKS resolution in one helper that enforces the kid rule
- Validate configuration at startup: N jwks_urls implies a configured kid
- Keep the locations list to one entry when keys are not disambiguated by kid
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
- cookiex: purpose must be non-empty and must not contain a pi
- the provided region is not a valid Ory region
- jwksx: "%s" does not support arbitrary key length
- cookiex: at least one secret is required
- cookiex: max age must not be negative
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/690f0a907e2fe259.
Report an issue: GitHub.