cloudflare/cloudflared · error

failed to parse JWKS

Error message

failed to parse JWKS

What it means

This error is returned by fetchJWKS when the JWKS endpoint returned HTTP 200 and a body within the size limit, but the body is not valid JSON or not shaped like a jose.JSONWebKeySet. json.Unmarshal failed, so the wrap preserves the JSON syntax error. It guards token verification against malformed or hijacked key-discovery responses.

Source

Thrown at token/jwks.go:124

		return nil, errors.Wrapf(err, "failed to fetch JWKS from %s", jwksURL.String())
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("JWKS endpoint %s returned status %d", jwksURL.String(), resp.StatusCode)
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxJWKSResponseSize+1))
	if err != nil {
		return nil, errors.Wrap(err, "failed to read JWKS response body")
	}
	if len(body) > maxJWKSResponseSize {
		return nil, fmt.Errorf("JWKS response body exceeds %d bytes", maxJWKSResponseSize)
	}

	var keySet jose.JSONWebKeySet
	if err := json.Unmarshal(body, &keySet); err != nil {
		return nil, errors.Wrap(err, "failed to parse JWKS")
	}
	return &keySet, nil
}

// jwksCachePath returns the on-disk path for cached JWKS for the given auth domain.
func jwksCachePath(authDomain url.URL) (string, error) {
	configPath, err := getConfigPath()
	if err != nil {
		return "", err
	}
	name := authDomain.Hostname() + jwksCacheSuffix
	return filepath.Join(configPath, name), nil
}

// getCachedJWKS loads JWKS and its modification time from the disk cache.
// A missing cache file is returned as a cache miss without an error.
func getCachedJWKS(authDomain url.URL) (*jose.JSONWebKeySet, time.Time, error) {
	path, err := jwksCachePath(authDomain)

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Verify the JWKS URL serves valid JSON: curl it and validate the body parses as JSON with a 'keys' array
  2. Confirm the auth domain / JWKS path configuration is correct (no redirect to an HTML page)
  3. Test connectivity without captive portal / proxy interference
  4. If you control the IdP, check server logs for why a non-JWKS payload was returned

Example fix

// before: trusting any 200 body
resp, _ := client.Get(jwksURL)
keySet, err := fetchJWKS(resp)
// after: validating content-type before parsing
resp, _ := client.Get(jwksURL)
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") {
	return fmt.Errorf("unexpected JWKS content-type: %s", ct)
}
keySet, err := fetchJWKS(resp)
Defensive patterns

Strategy: validation

Validate before calling

// verify the endpoint returns JSON before using it
resp, err := http.Get(jwksURL)
if err == nil {
	ct := resp.Header.Get("Content-Type")
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	resp.Body.Close()
	if !strings.Contains(ct, "json") || !json.Valid(body) {
		return fmt.Errorf("JWKS URL does not serve valid JSON (content-type %s)", ct)
	}
}

Try / catch

keySet, err := fetchJWKS(url)
if err != nil && strings.Contains(err.Error(), "failed to parse JWKS") {
	// log raw body for diagnosis; fail fast — retrying won't fix a bad URL
	log.Error().Err(err).Str("url", jwksURL).Msg("JWKS payload malformed; check auth domain config")
}

Prevention

When it happens

Trigger: fetchJWKS receives a 200 response whose body cannot be unmarshaled into jose.JSONWebKeySet: HTML error pages served with 200, truncated JSON, wrong Content-Type served by a captive portal, or a proxy injecting content.

Common situations: Misconfigured auth domain pointing to a login page instead of the JWKS endpoint; captive portals on public Wi-Fi returning HTML with 200; load balancer serving a maintenance page; wrong path configured for the OIDC discovery/JWKS route.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/609467c6ce79c362. Report an issue: GitHub.