{"record":{"id":"e6aaf9425f42f16d","repo":"PrefectHQ/fastmcp","slug":"invalid-jwt-assertion","errorCode":null,"errorMessage":"Invalid JWT assertion","messagePattern":"Invalid JWT assertion","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":585,"sourceCode":"                    del self._verifier_cache[oldest_key]\n                self._verifier_cache[cache_key] = verifier\n        elif cimd_doc.jwks:\n            # Inline JWKS — no caching since the key is embedded\n            public_key = self._extract_public_key_from_jwks(assertion, cimd_doc.jwks)\n            verifier = _JWTVerifier(\n                public_key=public_key,\n                issuer=client_id,\n                audience=token_endpoint,\n            )\n        else:\n            raise ValueError(\n                \"CIMD document must have jwks_uri or jwks for private_key_jwt\"\n            )\n\n        # 2. Verify JWT using JWTVerifier (handles signature, exp, iss, aud)\n        access_token = await verifier.load_access_token(assertion)\n        if not access_token:\n            raise ValueError(\"Invalid JWT assertion\")\n\n        claims = access_token.claims\n\n        # 3. Validate assertion lifetime (exp and iat)\n        now = time.time()\n        exp = claims.get(\"exp\")\n        iat = claims.get(\"iat\")\n\n        if not exp:\n            raise ValueError(\"Assertion must include exp claim\")\n\n        # Validate exp is in the future (with small clock skew tolerance)\n        if exp < now - 30:  # 30 second clock skew tolerance\n            raise ValueError(\"Assertion has expired\")\n\n        # If iat is present, validate it and check assertion lifetime\n        if iat:\n            if iat > now + 30:  # 30 second clock skew tolerance","sourceCodeStart":567,"sourceCodeEnd":603,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L567-L603","documentation":"validate_assertion verifies the private_key_jwt client assertion's signature, expiration, issuer, and audience via JWTVerifier. If the assertion fails any of these checks, load_access_token returns None and a ValueError('Invalid JWT assertion') is raised — the client presented a malformed, wrongly signed, expired, or wrongly-addressed JWT.","triggerScenarios":"validate_private_key_jwt called with an assertion JWT that: is signed by a key not in the client's JWKS, has a mismatched 'iss' (must equal the client_id), has 'aud' not matching the token endpoint, is expired, or is structurally malformed.","commonSituations":"Client signing with a rotated-out key while the JWKS still lists (or no longer lists) the right key; 'aud' set to a resource server instead of the token endpoint URL; clock skew causing exp/nbf failures; 'iss' not exactly equal to the client_id URL; libraries issuing JWS instead of a proper JWT with claims.","solutions":["Sign the assertion with the private key corresponding to a key published in the document's jwks/jwks_uri","Set 'iss' to the client_id and 'aud' to the FastMCP token endpoint URL exactly","Check system clocks and keep assertion lifetime short (exp within a few minutes)","Decode the assertion locally (without verification) to compare iss/aud/exp claims against expectations","Catch ValueError and respond with OAuth invalid_client / invalid_grant per the token endpoint contract"],"exampleFix":"# before\nclaims = {\"iss\": \"https://app.example.com\", \"aud\": \"https://api.example.com\", ...}\n# after\nclaims = {\n    \"iss\": client_id,\n    \"aud\": \"https://mcp.example.com/token\",\n    \"sub\": client_id,\n    \"exp\": int(time.time()) + 300,\n    \"jti\": str(uuid4()),\n}","handlingStrategy":"try-catch","validationCode":"import time, jwt  # pre-check on the client before sending the assertion\nclaims = {\"iss\": client_id, \"sub\": client_id, \"aud\": token_endpoint,\n          \"exp\": int(time.time()) + 300, \"jti\": str(uuid4())}\nassertion = jwt.encode(claims, private_key, algorithm=\"RS256\", headers={\"kid\": kid})","typeGuard":"def assertion_shape_ok(token: str) -> bool:\n    parts = token.split(\".\")\n    return len(parts) == 3 and all(parts)","tryCatchPattern":"try:\n    await manager.validate_private_key_jwt(doc, assertion, token_endpoint)\nexcept ValueError:\n    raise InvalidClientError(\"invalid client assertion\") from None","preventionTips":["Set iss and sub to the client_id and aud to the token endpoint URL","Keep exp short and synchronize clocks (NTP) on the client","Rotate keys by adding new keys to the JWKS before removing old ones","Sign with the key whose kid is published in the client's jwks/jwks_uri"],"tags":["oauth","cimd","jwt","private-key-jwt","authentication"],"backgroundTag":"jwt-verification-failed","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}