PrefectHQ/fastmcp · error · JoseError
Invalid token issuer
Error message
Invalid token issuer
What it means
verify_token() validates the 'iss' (issuer) claim against the issuer value the JWTIssuer was constructed with. A token signed with the correct key but issued by a different issuer (different deployment, tenant, or environment) is rejected with JoseError('Invalid token issuer') to prevent tokens minted by one system being accepted by another.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/jwt_issuer.py:282
"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)
raise
View on GitHub (pinned to 1f02114297)
Solutions
- Make the JWTIssuer's issuer configuration identical to the value stamped into tokens at mint time (exact string match)
- Check env-specific config — trailing slashes, scheme (http/https), and host must match exactly
- In multi-tenant setups, ensure the request is routed to the verifier configured for the token's tenant/issuer
- Decode the token locally and compare payload['iss'] with your configured issuer to see the exact mismatch
Example fix
// before JWTIssuer(issuer="https://api.example.com/") # token has no trailing slash // after JWTIssuer(issuer="https://api.example.com") # match mint-time iss exactly
Defensive patterns
Strategy: validation
Validate before calling
claims = jwt.decode(token, options={"verify_signature": False})
expected_issuer = os.environ["FASTMCP_JWT_ISSUER"]
if claims.get("iss") != expected_issuer:
raise ValueError(f"Token iss {claims.get('iss')!r} != configured {expected_issuer!r}") Type guard
def has_expected_issuer(claims: dict, expected: str) -> bool:
return claims.get("iss") == expected Try / catch
try:
payload = issuer.verify_token(token)
except JoseError as e:
if "issuer" in str(e).lower():
log_untrusted_token_rejected(token) # do not retry; investigate config
raise
raise Prevention
- Use one shared config source for issuer on both minting and verifying sides
- Compare exact strings — beware trailing slashes and http vs https
- In multi-tenant setups, derive expected iss from the request tenant
When it happens
Trigger: Calling verify_token() on a token whose payload['iss'] != self.issuer (jwt_issuer.py:282): e.g. a token from staging used against production, a misconfigured issuer base URL, or a multi-tenant setup routing tokens to the wrong tenant verifier.
Common situations: Environment variable for the issuer differs between issuer and verifier deployments (trailing slash, http vs https, localhost vs domain); pointing a dev client at prod; copying tokens between environments in tests; tenant ID omitted from the configured issuer URL.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid token audience
- JWKS URI not configured
- No keys found in JWKS
- OAuth server rejected the static client credentials. Verify
- MultiAuth requires at least a server or one verifier
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/ac96df2082209151.
Report an issue: GitHub.