PrefectHQ/fastmcp · error · JoseError
Token has expired
Error message
Token has expired
What it means
verify_token() checks the 'exp' (expiration, NumericDate) claim against the current time after verifying the signature. If exp exists and is in the past, the token is rejected with JoseError('Token has expired'). JWTs are short-lived by design; the verifier will not accept an expired token even with a valid signature.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/jwt_issuer.py:277
# Validate token type
token_use = payload.get("token_use", "access")
if token_use != expected_token_use:
logger.debug(
"Token type mismatch: expected %s, got %s",
expected_token_use,
token_use,
)
raise JoseError(
f"Token type mismatch: expected {expected_token_use}, "
f"got {token_use}"
)
# Validate expiration
exp = payload.get("exp")
if exp is not None and exp < time.time():
logger.debug("Token expired")
raise JoseError("Token has expired")
# Validate issuer
if payload.get("iss") != self.issuer:
logger.debug("Token has invalid issuer")
raise JoseError("Invalid token issuer")
# Validate audience
if payload.get("aud") != self.audience:
logger.debug("Token has invalid audience")
raise JoseError("Invalid token audience")
logger.debug(
"Token verified successfully for subject=%s", payload.get("sub")
)
return payload
except JoseError as e:
logger.debug("Token validation failed: %s", e)View on GitHub (pinned to 1f02114297)
Solutions
- Refresh the token before it expires (use the refresh token to mint a new one) and retry the request
- If tokens keep expiring prematurely, check clock sync (NTP) between issuing and verifying servers
- Fix client token caching to honor exp and refresh proactively (e.g. at 80% of lifetime)
- Ensure a long enough lifetime at issuance if the workload legitimately needs longer-lived tokens
Example fix
// before
use(token) # stale cached token
// after
if time.time() >= decoded["exp"]:
token = refresh_access_token()
use(token) Defensive patterns
Strategy: try-catch
Validate before calling
claims = jwt.decode(token, options={"verify_signature": False})
if (exp := claims.get("exp")) is not None and exp < time.time():
token = refresh_access_token() # refresh before calling verify_token Type guard
def is_token_expired(claims: dict) -> bool:
exp = claims.get("exp")
return exp is not None and exp < time.time() Try / catch
try:
payload = issuer.verify_token(token)
except JoseError as e:
if "expired" in str(e).lower():
token = refresh_access_token()
payload = issuer.verify_token(token)
else:
raise Prevention
- Refresh tokens proactively (e.g. at 80% of lifetime) rather than on failure
- Keep token lifetimes appropriate to the operation duration
- Synchronize clocks across servers with NTP
When it happens
Trigger: Calling verify_token() with a JWT whose claims['exp'] < time.time() (jwt_issuer.py:277) — e.g. a cached token past its TTL, a long-lived test token, or a client that never refreshes.
Common situations: Client cached an access token and its TTL lapsed; server/client clock skew making a still-valid token look expired; replaying a captured/sample token from documentation or a test fixture; a refresh flow that fails so the app keeps sending the old token.
Related errors
- Assertion has expired
- Assertion must include exp claim
- Assertion is not yet valid (nbf in future)
- Assertion iat is in the future
- Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIM
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/9ac62e2040505fff.
Report an issue: GitHub.