jwt_inspect
Decode and audit a JWT token. Detects algorithm issues, missing claims, suspicious kid values, and checks for weak secrets.
Instructions
Decode and audit a JWT.
Reports algorithm issues (none, weak HS*), expiry, missing standard
claims (exp, iat, iss, aud), suspicious kid values that look
like path traversal or SQL, and (optionally) checks the signature
against a small dictionary of common weak HS256/384/512 secrets.
Args: token: The JWT string (three dot-separated base64url segments). check_weak_secrets: If True, attempt a small dictionary of common secrets against the signature for HS* algorithms. Default True.
Returns: Structured inspection report (see JwtInspection schema).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| token | Yes | ||
| check_weak_secrets | No |
Implementation Reference
- The main handler function 'jwt_inspect' that decodes and audits a JWT token. It validates structure, checks algorithm (flags 'none', unknown), inspects claims (exp, iat, iss, aud, nbf), detects suspicious kid/jku/x5u headers, and optionally checks for weak HS256/384/512 secrets against a built-in dictionary.
def jwt_inspect(token: str, check_weak_secrets: bool = True) -> dict: """Decode and audit a JWT. Reports algorithm issues (`none`, weak HS*), expiry, missing standard claims (`exp`, `iat`, `iss`, `aud`), suspicious `kid` values that look like path traversal or SQL, and (optionally) checks the signature against a small dictionary of common weak HS256/384/512 secrets. Args: token: The JWT string (three dot-separated base64url segments). check_weak_secrets: If True, attempt a small dictionary of common secrets against the signature for HS* algorithms. Default True. Returns: Structured inspection report (see JwtInspection schema). """ result = JwtInspection(valid_structure=False) parts = token.split(".") if len(parts) != 3: result.findings.append( JwtFinding( category="malformed", severity="high", message=f"expected 3 segments, got {len(parts)}", ) ) return result.model_dump() try: header = json.loads(_b64url_decode(parts[0])) payload = json.loads(_b64url_decode(parts[1])) except (ValueError, json.JSONDecodeError) as e: result.findings.append( JwtFinding(category="malformed", severity="high", message=f"decode error: {e}") ) return result.model_dump() result.valid_structure = True result.header = header result.payload = payload result.signature_b64 = parts[2] alg = str(header.get("alg", "")).upper() if alg in {"NONE", ""}: result.findings.append( JwtFinding( category="alg-none", severity="high", message="alg is 'none' or missing — token is unsigned, trivially forgeable", ) ) elif alg not in HS_ALGS and not alg.startswith(("RS", "ES", "PS", "ED")): result.findings.append( JwtFinding( category="unknown-alg", severity="medium", message=f"unknown algorithm `{alg}`", ) ) if "kid" in header: kid = str(header["kid"]) if any(s in kid for s in ("../", "..\\", "/", "\\", "'", '"', ";", "--")): result.findings.append( JwtFinding( category="suspicious-kid", severity="medium", message=f"`kid` contains characters suggestive of path traversal or injection: {kid!r}", ) ) if "jku" in header or "x5u" in header: result.findings.append( JwtFinding( category="external-key-url", severity="medium", message="header references external key URL (jku/x5u) — verify allow-list", ) ) now = int(time.time()) exp = payload.get("exp") if exp is None: result.findings.append( JwtFinding( category="missing-claim", severity="medium", message="no `exp` claim — token never expires", ) ) elif isinstance(exp, int | float) and exp < now: result.findings.append( JwtFinding( category="expired", severity="info", message=f"token expired {now - int(exp)}s ago", ) ) for claim, sev in (("iat", "low"), ("iss", "low"), ("aud", "low")): if claim not in payload: result.findings.append( JwtFinding( category="missing-claim", severity=sev, message=f"no `{claim}` claim", ) ) nbf = payload.get("nbf") if isinstance(nbf, int | float) and nbf > now: result.findings.append( JwtFinding( category="not-yet-valid", severity="info", message=f"token not valid for another {int(nbf) - now}s", ) ) if check_weak_secrets and alg in HS_ALGS: result.weak_secret_check_performed = True weak = _try_weak_secret(token, alg, DEFAULT_WEAK_SECRETS) if weak: result.weak_secret = weak result.findings.append( JwtFinding( category="weak-secret", severity="high", message=f"signature verifies with common weak secret: {weak!r}", ) ) return result.model_dump() - Pydantic models JwtFinding and JwtInspection defining the output schema. JwtInspection includes fields: valid_structure, header, payload, signature_b64, findings (list of JwtFinding), weak_secret, weak_secret_check_performed, and weak_secret_check_scope.
class JwtFinding(BaseModel): category: str severity: Severity message: str class JwtInspection(BaseModel): valid_structure: bool header: dict | None = None payload: dict | None = None signature_b64: str | None = None findings: list[JwtFinding] = Field(default_factory=list) weak_secret: str | None = None weak_secret_check_performed: bool = False weak_secret_check_scope: str = ( f"small_builtin_dictionary ({len(DEFAULT_WEAK_SECRETS)} entries) — " "absence of finding is NOT proof of strong secret" ) - src/mcp_security_toolkit/server.py:25-25 (registration)Registration of jwt_inspect.jwt_inspect as an MCP tool via mcp.tool() decorator in the FastMCP server.
mcp.tool()(jwt_inspect.jwt_inspect) - src/mcp_security_toolkit/server.py:9-9 (registration)Import of the jwt_inspect module in server.py for registration.
jwt_inspect, - Helper function '_try_weak_secret' that attempts HMAC verification of JWT signature against a list of common weak secrets for HS256/384/512 algorithms.
def _try_weak_secret(token: str, alg: str, secrets: list[str]) -> str | None: if alg not in HS_ALGS: return None try: signing_input, signature = token.rsplit(".", 1) except ValueError: return None sig = _b64url_decode(signature) hashfn = HS_ALGS[alg] for s in secrets: mac = hmac.new(s.encode("utf-8"), signing_input.encode("ascii"), hashfn).digest() if hmac.compare_digest(mac, sig): return s