{"record":{"id":"c3d8b22244928323","repo":"cloudflare/cloudflared","slug":"metadata-jwt-verification-failed-w","errorCode":null,"errorMessage":"metadata JWT verification failed: %w","messagePattern":"metadata JWT verification failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"token/token.go","lineNumber":445,"sourceCode":"\t\treturn nil, err\n\t}\n\n\t// Decode without verification to extract auth_domain for JWKS lookup.\n\tunverified, err := decodeMetadataUnverified(rawJWT)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse auth_domain into the canonical hostname used for JWKS lookup.\n\tauthDomain, err := parseAuthDomain(unverified.AuthDomain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"metadata JWT auth_domain validation failed: %w\", err)\n\t}\n\n\t// Verify the JWT signature against the JWKS (with disk cache + retry).\n\tclaims, err := verifyMetadataWithRetry(rawJWT, authDomain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"metadata JWT verification failed: %w\", err)\n\t}\n\n\t// Verify the hostname in the JWT matches the URL we actually requested.\n\tif !strings.EqualFold(claims.Hostname, reqURL.Hostname()) {\n\t\treturn nil, fmt.Errorf(\"metadata JWT hostname %q does not match request host %q\", claims.Hostname, reqURL.Hostname())\n\t}\n\tif claims.Type != metadataMatchType {\n\t\treturn nil, fmt.Errorf(\"metadata JWT type %q is not match\", claims.Type)\n\t}\n\tif claims.AUD == \"\" {\n\t\treturn nil, errors.New(\"metadata JWT aud is empty\")\n\t}\n\tif err := validateMetadataIssuedAt(claims.IAT, time.Now()); err != nil {\n\t\treturn nil, err\n\t}\n\n\tappHostname := claims.AppHostname\n\tif appHostname == \"\" {","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/cloudflare/cloudflared/blob/2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f/token/token.go#L427-L463","documentation":"GetAppInfo wraps verifyMetadataWithRetry errors: the metadata JWT's signature could not be verified against the JWKS published by the auth domain (including disk cache and retry attempts). This prevents an attacker-controlled server from spoofing app identity — only JWTs signed by the account's keys are accepted.","triggerScenarios":"Calling GetAppInfo when the JWT's kid is absent from the fetched JWKS, the JWKS endpoint is unreachable or returns errors, the JWT is malformed/tampered, keys were rotated and the disk cache is stale, or the auth domain resolved to the wrong Cloudflare account.","commonSituations":"Cloudflare key rotation while a cached stale JWKS is on disk, DNS/hosts file pointing the auth domain elsewhere, an intercepting TLS proxy (Zscaler/Netskope) breaking JWKS retrieval, firewalls blocking the team domain's /cdn-cgi/access/certs endpoint, or clock skew affecting token claims.","solutions":["Clear the cached JWKS file used by the disk cache so fresh keys are fetched, then retry.","Confirm the JWKS endpoint is reachable: curl https://<team-domain>/cdn-cgi/access/certs.","Remove any TLS-intercepting proxy or add its CA properly so JWKS fetches are not tampered with.","Check /etc/hosts and DNS for the auth domain — it must resolve to Cloudflare.","Retry after a few seconds (transient edge issues) and update cloudflared if the edge switched signing algorithms."],"exampleFix":"// before: stale key cache causes verification failure\ninfo, err := token.GetAppInfo(appURL)\n// after: verify JWKS reachability and clear stale cache\nresp, err := http.Get(\"https://\" + authDomain + \"/cdn-cgi/access/certs\")\nif err != nil || resp.StatusCode != 200 {\n    return fmt.Errorf(\"JWKS unreachable for %s\", authDomain)\n}\nos.Remove(jwksCachePath)\ninfo, err := token.GetAppInfo(appURL)","handlingStrategy":"retry","validationCode":"resp, err := http.Get(\"https://\" + authDomain + \"/cdn-cgi/access/certs\")\nif err != nil || resp.StatusCode != http.StatusOK {\n    return fmt.Errorf(\"JWKS endpoint unreachable for auth domain %s\", authDomain)\n}","typeGuard":null,"tryCatchPattern":"var info *token.AppInfo\nvar err error\nfor i := 0; i < 3; i++ {\n    info, err = token.GetAppInfo(appURL)\n    if err == nil || !strings.Contains(err.Error(), \"metadata JWT verification failed\") {\n        break\n    }\n    os.Remove(jwksCachePath) // drop possibly stale cached keys\n    time.Sleep(time.Duration(1<<i) * time.Second)\n}\nif err != nil {\n    return err\n}","preventionTips":["Keep the JWKS disk cache writable and clear it after Cloudflare key rotations.","Ensure the team domain's /cdn-cgi/access/certs endpoint is reachable from the network.","Avoid TLS-intercepting proxies on the auth domain, or trust their CA correctly.","Keep NTP/clock sync so time-based claims validate.","Retry with backoff — the library already retries, but stale caches need manual clearing."],"tags":["go","jwt","security","signature-verification","cloudflare-access"],"backgroundTag":"jwt-signature-verification-failed","analyzedSha":"2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f","analyzedAt":"2026-09-06T04:14:33.757Z","contentChangedAt":"2026-09-06T04:14:33.757Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}