{"record":{"id":"d7e2952a33084bf4","repo":"PrefectHQ/fastmcp","slug":"jwks-document-contains-no-keys","errorCode":null,"errorMessage":"JWKS document contains no keys","messagePattern":"JWKS document contains no keys","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":678,"sourceCode":"        Returns:\n            PEM-encoded public key\n\n        Raises:\n            ValueError: If key cannot be found or extracted\n        \"\"\"\n        # Extract kid from token header\n        try:\n            header_b64 = token.split(\".\")[0]\n            header_b64 += \"=\" * (4 - len(header_b64) % 4)  # Add padding\n            header = json.loads(base64.urlsafe_b64decode(header_b64))\n            kid = header.get(\"kid\")\n        except (IndexError, ValueError, json.JSONDecodeError) as e:\n            raise ValueError(f\"Failed to extract key ID from token: {e}\") from e\n\n        # Find matching key in JWKS\n        keys = jwks.get(\"keys\", [])\n        if not keys:\n            raise ValueError(\"JWKS document contains no keys\")\n\n        matching_key = None\n        for key in keys:\n            if kid and key.get(\"kid\") == kid:\n                matching_key = key\n                break\n\n        if not matching_key:\n            # If no kid match, try first key as fallback\n            if len(keys) == 1:\n                matching_key = keys[0]\n                self.logger.warning(\n                    \"No matching kid in JWKS, using single available key\"\n                )\n            else:\n                raise ValueError(f\"No matching key found for kid={kid} in JWKS\")\n\n        # Convert JWK to PEM","sourceCodeStart":660,"sourceCodeEnd":696,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L660-L696","documentation":"This error means the JWKS document fetched (or supplied) for a CIMD client contains an empty `keys` array, so no public key can be extracted to verify a client assertion (private_key_jwt). The library raises it early rather than failing later with a confusing 'no matching key' error. It is a ValueError wrapping the key-selection step of assertion validation.","triggerScenarios":"`_extract_public_key_from_jwks` is called via `validate_assertion` with a JWKS document (from the client's CIMD metadata, e.g. `jwks_uri` or embedded `jwks`) whose parsed JSON has no `keys` member or `keys: []`.","commonSituations":"Authorization server publishes a malformed/placeholder JWKS at the jwks_uri in the CIMD document; a static/mocked JWKS file used in testing is empty; the JWKS fetch succeeded but the issuer rotated keys and returned an empty set; misconfigured jwks_uri pointing to a non-JWKS JSON doc that happens to parse.","solutions":["Inspect the JWKS document at the CIMD client's jwks_uri and confirm it contains a non-empty `keys` array with the signing key","Fix the issuer's JWKS publishing endpoint so it serves the public keys (e.g. re-export the key set)","If the JWKS is embedded in the CIMD document, update the metadata to include the `keys` array","Re-fetch after the issuer rotates keys — the empty set may be transient during rotation"],"exampleFix":"// before (bad JWKS served by issuer)\n{\"keys\": []}\n// after\n{\"keys\": [{\"kty\": \"RSA\", \"kid\": \"key-1\", \"n\": \"...\", \"e\": \"AQAB\"}]}","handlingStrategy":"validation","validationCode":"import json, urllib.request\njwks = json.load(urllib.request.urlopen(client_meta[\"jwks_uri\"]))\nassert isinstance(jwks.get(\"keys\"), list) and len(jwks[\"keys\"]) > 0, \"JWKS has no keys\"","typeGuard":"def has_keys(jwks: dict) -> bool:\n    keys = jwks.get(\"keys\")\n    return isinstance(keys, list) and len(keys) > 0","tryCatchPattern":"try:\n    key = extract_public_key(jwks, kid)\nexcept ValueError as e:\n    if \"contains no keys\" in str(e):\n        logger.error(\"Issuer JWKS is empty; check jwks_uri %s\", jwks_uri)\n    raise","preventionTips":["Verify the issuer's jwks_uri serves a valid non-empty JWKS before registering the CIMD client","Monitor the JWKS endpoint for availability and content changes","Refresh/re-fetch the JWKS after issuer key rotation","Validate the JWKS document with a schema check during onboarding of new issuers"],"tags":["oauth","jwks","private-key-jwt","cimd"],"backgroundTag":"jwks-empty-or-missing-keys","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}