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

  1. Refresh the access token using the refresh token (or re-run the client-credentials flow) and retry the request.
  2. For clock-drift scenarios, increase the allowed leeway if the scheme exposes it (leeway/clock_skew_seconds), or fix NTP on both hosts.
  3. In tests, mint fresh tokens per test instead of hardcoding fixture JWTs.
  4. 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

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


AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15). Data as JSON: /api/errors/e04b4388d4b375dc. Report an issue: GitHub.