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.

Solutions

  1. Make the JWTIssuer's issuer configuration identical to the value stamped into tokens at mint time (exact string match)
  2. Check env-specific config — trailing slashes, scheme (http/https), and host must match exactly
  3. In multi-tenant setups, ensure the request is routed to the verifier configured for the token's tenant/issuer
  4. 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

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

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/ac96df2082209151. Report an issue: GitHub.

Appendix: 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)