cloudflare/cloudflared · warning

failed to decode verified metadata JWT claims

Error message

failed to decode verified metadata JWT claims

What it means

Once the metadata JWT signature verifies, verifyMetadataJWT json.Unmarshal's the verified payload into metadataClaims. This error means the signature was valid but the payload is not JSON matching the claims schema — very rare, since Cloudflare always emits the documented claim set. It usually indicates a non-metadata JWT that happens to carry a valid signature from the same key set.

Source

Thrown at token/jwks.go:74

	return &claims, nil
}

// verifyMetadataJWT verifies the metadata JWT signature against the provided
// JWKS and returns the decoded claims.
func verifyMetadataJWT(rawJWT string, keySet *jose.JSONWebKeySet) (*metadataClaims, error) {
	jws, err := jose.ParseSigned(rawJWT, signatureAlgs)
	if err != nil {
		return nil, errors.Wrap(err, "failed to parse metadata JWT")
	}

	payload, err := jws.Verify(keySet)
	if err != nil {
		return nil, errors.Wrap(err, "failed to verify metadata JWT signature")
	}

	var claims metadataClaims
	if err := json.Unmarshal(payload, &claims); err != nil {
		return nil, errors.Wrap(err, "failed to decode verified metadata JWT claims")
	}
	return &claims, nil
}

// parseAuthDomain extracts the canonical hostname used for JWKS requests and
// cache paths from the auth_domain claim.
func parseAuthDomain(authDomain string) (url.URL, error) {
	parsed, err := url.Parse(httpsScheme + "://" + authDomain)
	if err != nil {
		return url.URL{}, fmt.Errorf("failed to parse auth_domain %q: %w", authDomain, err)
	}
	hostname := strings.ToLower(parsed.Hostname())
	if !strings.HasSuffix(hostname, accessDomainSuffix) {
		return url.URL{}, fmt.Errorf("auth_domain %q does not end with %q", authDomain, accessDomainSuffix)
	}
	return url.URL{Scheme: httpsScheme, Host: hostname}, nil
}

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Confirm the token came from the metadata endpoint (CF-Access-Metadata-Request: true) and not another Access surface.
  2. Decode the payload offline and compare claim types against metadataClaims (type, hostname, auth_domain, aud, app_hostname, iat).
  3. Update cloudflared if Cloudflare changed the metadata claim schema — the struct may lag a new format.
  4. File an upstream issue with the (redacted) header/claim names if a current token still fails.
Defensive patterns

Strategy: type-guard

Validate before calling

var _ = json.Valid // payload sanity check before trusting claims
func payloadIsValidJSON(token string) bool {
    parts := strings.Split(strings.TrimSpace(token), ".")
    if len(parts) < 2 { return false }
    payload, err := base64.RawURLEncoding.DecodeString(parts[1])
    return err == nil && json.Valid(payload)
}

Type guard

func isMetadataClaims(m map[string]any) bool {
    _, hasAud := m["aud"].(string)
    _, hasDomain := m["auth_domain"].(string)
    return hasAud && hasDomain
}

Try / catch

claims, err := verifyMetadataJWT(rawJWT, keySet)
if err != nil && strings.Contains(err.Error(), "failed to decode verified metadata JWT claims") {
    return fmt.Errorf("signature valid but payload is not metadata claims — wrong token type or schema change: %w", err)
}

Prevention

When it happens

Trigger: verifyMetadataJWT with a signed token whose payload is empty, non-JSON, or has type-incompatible claims (e.g. aud as an array of strings instead of a string) after successful jws.Verify.

Common situations: Feeding a different kind of Cloudflare-signed token (e.g. an application session token) through the metadata verification path; an edge API change producing an unexpected claim schema in newer tokens.

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/eaec722b312d7e69. Report an issue: GitHub.