crewAIInc/crewAI · error · HTTPException
Token has expired
Error message
Token has expired
What it means
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.
Source
Thrown at lib/crewai/src/crewai/a2a/auth/server_schemes.py:306
issuer=str(self.issuer).rstrip("/"),
leeway=self.clock_skew_seconds,
options={
"require": self.required_claims,
},
)
return AuthenticatedUser(
token=token,
scheme="oidc",
claims=claims,
)
except jwt.ExpiredSignatureError:
logger.debug(
"OIDC authentication failed",
extra={"reason": "token_expired", "scheme": "oidc"},
)
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Token has expired",
) from None
except jwt.InvalidAudienceError:
logger.debug(
"OIDC authentication failed",
extra={"reason": "invalid_audience", "scheme": "oidc"},
)
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid token audience",
) from None
except jwt.InvalidIssuerError:
logger.debug(
"OIDC authentication failed",
extra={"reason": "invalid_issuer", "scheme": "oidc"},
)
raise HTTPException(View on GitHub (pinned to 754d7323be)
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.
Example fix
# before
resp = await agent_client.send(message) # 401 Token has expired
# after
from datetime import datetime, timezone
import time
def needs_refresh(claims: dict) -> bool:
return claims["exp"] - time.time() < 30
if needs_refresh(jwt.decode(access_token, options={"verify_signature": False})):
access_token = await oauth_client.refresh(refresh_token)
resp = await agent_client.send(message) Defensive patterns
Strategy: retry
Validate before calling
import time
import jwt
def token_expired(token: str, skew: float = 30.0) -> bool:
claims = jwt.decode(token, options={"verify_signature": False})
return claims.get("exp", 0) - skew <= time.time() Try / catch
try:
resp = await client.send(msg)
except HTTPException as e:
if e.status_code == 401 and e.detail == "Token has expired":
access_token = await refresh_flow() # refresh token / client credentials
resp = await client.send(msg) # retry once with fresh token
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid token audience
- Invalid token issuer
- Missing required claim: {e.claim}
- Error: {e}
- Project name cannot be empty or contain only whitespace
AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15).
Data as JSON: /api/errors/e04b4388d4b375dc.
Report an issue: GitHub.