{"record":{"id":"e04b4388d4b375dc","repo":"crewAIInc/crewAI","slug":"token-has-expired-e04b43","errorCode":null,"errorMessage":"Token has expired","messagePattern":"Token has expired","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"lib/crewai/src/crewai/a2a/auth/server_schemes.py","lineNumber":306,"sourceCode":"                issuer=str(self.issuer).rstrip(\"/\"),\n                leeway=self.clock_skew_seconds,\n                options={\n                    \"require\": self.required_claims,\n                },\n            )\n\n            return AuthenticatedUser(\n                token=token,\n                scheme=\"oidc\",\n                claims=claims,\n            )\n\n        except jwt.ExpiredSignatureError:\n            logger.debug(\n                \"OIDC authentication failed\",\n                extra={\"reason\": \"token_expired\", \"scheme\": \"oidc\"},\n            )\n            raise HTTPException(\n                status_code=HTTP_401_UNAUTHORIZED,\n                detail=\"Token has expired\",\n            ) from None\n        except jwt.InvalidAudienceError:\n            logger.debug(\n                \"OIDC authentication failed\",\n                extra={\"reason\": \"invalid_audience\", \"scheme\": \"oidc\"},\n            )\n            raise HTTPException(\n                status_code=HTTP_401_UNAUTHORIZED,\n                detail=\"Invalid token audience\",\n            ) from None\n        except jwt.InvalidIssuerError:\n            logger.debug(\n                \"OIDC authentication failed\",\n                extra={\"reason\": \"invalid_issuer\", \"scheme\": \"oidc\"},\n            )\n            raise HTTPException(","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/crewAIInc/crewAI/blob/754d7323beb2fd042e33444a115ea2d5a47193f0/lib/crewai/src/crewai/a2a/auth/server_schemes.py#L288-L324","documentation":"Raised by OIDCAuth.authenticate() when PyJWT's jwt.decode() throws ExpiredSignatureError, i.e. the presented JWT's `exp` claim (plus allowed clock skew) is in the past. It maps to HTTP 401 with detail 'Token has expired' and logs reason='token_expired' at debug level. The token itself may be perfectly valid otherwise; only its lifetime is exhausted.","triggerScenarios":"Client presents a JWT whose exp claim is older than now (minus leeway); long-running sessions that cached a token past its lifetime; tokens with very short TTL (e.g. 5-minute access tokens) used after expiry; server clock ahead of the issuer's clock.","commonSituations":"Access tokens not refreshed before expiry; cached tokens in a client SDK surviving beyond TTL; clock drift between the CrewAI server and the identity provider; test fixtures generated hours before the test run.","solutions":["Refresh the access token using the refresh token (or re-run the client-credentials flow) and retry the request.","For clock-drift scenarios, increase the allowed leeway if the scheme exposes it (leeway/clock_skew_seconds), or fix NTP on both hosts.","In tests, mint fresh tokens per test instead of hardcoding fixture JWTs.","Schedule token refresh at ~80% of the token TTL rather than waiting for a 401."],"exampleFix":"# before\nresp = await agent_client.send(message)  # 401 Token has expired\n\n# after\nfrom datetime import datetime, timezone\nimport time\n\ndef needs_refresh(claims: dict) -> bool:\n    return claims[\"exp\"] - time.time() < 30\n\nif needs_refresh(jwt.decode(access_token, options={\"verify_signature\": False})):\n    access_token = await oauth_client.refresh(refresh_token)\nresp = await agent_client.send(message)","handlingStrategy":"retry","validationCode":"import time\nimport jwt\n\ndef token_expired(token: str, skew: float = 30.0) -> bool:\n    claims = jwt.decode(token, options={\"verify_signature\": False})\n    return claims.get(\"exp\", 0) - skew <= time.time()","typeGuard":null,"tryCatchPattern":"try:\n    resp = await client.send(msg)\nexcept HTTPException as e:\n    if e.status_code == 401 and e.detail == \"Token has expired\":\n        access_token = await refresh_flow()  # refresh token / client credentials\n        resp = await client.send(msg)  # retry once with fresh token\n    else:\n        raise","preventionTips":["Refresh tokens at ~80% of their TTL instead of waiting for a 401.","Sync clocks with NTP on both client and server hosts.","Generate fresh tokens in tests, never hardcode expired fixtures."],"tags":["a2a","oidc","jwt","token-expiry","http-401"],"backgroundTag":null,"analyzedSha":"754d7323beb2fd042e33444a115ea2d5a47193f0","analyzedAt":"2026-08-15T04:06:56.746Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}