Claude Skill

jwt-inspector

Decode and security-audit a JSON Web Token — flag alg=none, missing/excessive expiry, symmetric-alg confusion risk, missing claims — and attempt an offline HMAC secret crack against a wordlist to detect weak signing keys. Use when the user asks to "decode this JWT", "is this toke

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download novacode37-claude-security-skills-skills_jwt-inspector-dd4e44f.zip · 6 KB
Part of novacode37/claude-security-skills — 5 skills

Install

skills CLI npx skills add https://github.com/NovaCode37/claude-security-skills/tree/main/skills/jwt-inspector
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install novacode37-claude-security-skills@llmmart
Git git clone https://github.com/NovaCode37/claude-security-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole novacode37/claude-security-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

JWT Inspector

Decode and audit JSON Web Tokens with no third-party dependencies. It splits the token, decodes header + payload, evaluates them against a set of security checks, and (for HMAC tokens) tries a fast offline crack of the signing secret against a wordlist.

When to use this skill

  • "Decode / inspect this JWT."
  • "Is this token configured securely?"
  • "Does this JWT use a weak/guessable secret?"
  • Auditing auth tokens during a security review.

Checks performed

  • alg=none (critical) — unsigned, forgeable token.
  • Symmetric alg (HS)* — HMAC verification key == signing secret; HS/RS confusion and brute-force risk.
  • Missing exp / token never expires; excessively long lifetime.
  • iat in the future, missing nbf, missing iss/aud/sub.
  • Weak HMAC secret (critical) — cracked from a built-in or supplied wordlist.

How to run it

# Decode + audit
python skills/jwt-inspector/inspector.py "<token>"

# Read token from stdin
echo "<token>" | python skills/jwt-inspector/inspector.py -

# Try cracking the HMAC secret with a custom wordlist
python skills/jwt-inspector/inspector.py "<token>" --secret-list rockyou.txt

# JSON output
python skills/jwt-inspector/inspector.py "<token>" --json

# Only fail CI on high/critical (claim-hygiene notes are LOW)
python skills/jwt-inspector/inspector.py "<token>" --min-severity high

Exit codes: 0 clean · 1 issues reported · 2 malformed input. Every reported issue fails the build. The default reports everything down to info (including exp-past); raise --min-severity to low/medium/high to filter advisory notes out of both the report and the exit code.

Recommended workflow for Claude

  1. Run the inspector and read the decoded payload to understand the token.
  2. Report findings ordered by severity; explain the impact of each.
  3. If a secret was cracked, stress that the key is compromised — rotate it and move to an asymmetric algorithm (RS256/ES256) where feasible.
  4. Never treat a decoded payload as trusted: decoding ≠ verifying. Remind the user that signature verification with the correct key is what matters.

Note

Cracking only runs for HMAC algorithms and only against the provided wordlist — it is a weak-key detector, not a brute-forcer. Only inspect tokens you are authorized to handle.

Files (claude-security-skills)
  • tests
    • test_inspector.py 6.5 KB
      import json
      import os
      import sys
      import time
      
      import pytest
      
      sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
      
      import inspector
      
      
      def _b64(obj):
          return inspector.b64url_encode(json.dumps(obj).encode())
      
      
      def test_b64url_roundtrip():
          data = b"hello world!?"
          assert inspector.b64url_decode(inspector.b64url_encode(data)) == data
      
      
      def test_decode_valid():
          token = inspector.sign_hs256({"alg": "HS256", "typ": "JWT"},
                                       {"sub": "1", "exp": int(time.time()) + 60},
                                       "secret")
          jwt = inspector.decode(token)
          assert jwt.header["alg"] == "HS256"
          assert jwt.payload["sub"] == "1"
      
      
      def test_decode_rejects_malformed():
          with pytest.raises(ValueError):
              inspector.decode("not.a.valid.jwt.token")
          with pytest.raises(ValueError):
              inspector.decode("only-one-part")
      
      
      def test_alg_none_is_critical():
          token = f'{_b64({"alg": "none", "typ": "JWT"})}.{_b64({"sub": "1"})}.'
          jwt = inspector.decode(token)
          issues = inspector.audit(jwt)
          assert any(i.id == "alg-none" and i.severity == "critical" for i in issues)
      
      
      def test_missing_exp_flagged():
          token = inspector.sign_hs256({"alg": "HS256"}, {"sub": "1"}, "x")
          issues = inspector.audit(inspector.decode(token))
          assert any(i.id == "exp-missing" for i in issues)
      
      
      def test_expired_token_noted():
          token = inspector.sign_hs256({"alg": "HS256"},
                                       {"exp": int(time.time()) - 100}, "x")
          issues = inspector.audit(inspector.decode(token))
          assert any(i.id == "exp-past" for i in issues)
      
      
      def test_excessive_lifetime_flagged():
          token = inspector.sign_hs256(
              {"alg": "HS256"}, {"exp": int(time.time()) + 60 * 60 * 24 * 800}, "x")
          issues = inspector.audit(inspector.decode(token))
          assert any(i.id == "exp-far" for i in issues)
      
      
      def test_symmetric_alg_flagged():
          token = inspector.sign_hs256({"alg": "HS256"},
                                       {"exp": int(time.time()) + 60}, "x")
          issues = inspector.audit(inspector.decode(token))
          assert any(i.id == "alg-symmetric" for i in issues)
      
      
      def test_crack_weak_secret():
          token = inspector.sign_hs256({"alg": "HS256"},
                                       {"sub": "1", "exp": int(time.time()) + 60},
                                       "secret")
          result = inspector.inspect(token)
          assert result["cracked_secret"] == "secret"
          assert any(i["id"] == "weak-secret" for i in result["issues"])
      
      
      @pytest.mark.parametrize("secret", ["password123", "P@ssw0rd", "welcome"])
      def test_crack_extended_weak_secret_candidates(secret):
          token = inspector.sign_hs256({"alg": "HS256"},
                                       {"sub": "1", "exp": int(time.time()) + 60},
                                       secret)
          result = inspector.inspect(token)
          assert result["cracked_secret"] == secret
          assert any(issue["id"] == "weak-secret" for issue in result["issues"])
      
      
      def test_strong_secret_not_cracked():
          token = inspector.sign_hs256(
              {"alg": "HS256"}, {"sub": "1", "exp": int(time.time()) + 60},
              "f3Kd9Lm2Qx8Zp1Rt7Vw4Bn6Cs0Hj5-not-in-wordlist")
          result = inspector.inspect(token)
          assert result["cracked_secret"] is None
      
      
      def test_inspect_returns_decoded():
          token = inspector.sign_hs256({"alg": "HS256"},
                                       {"sub": "abc", "exp": int(time.time()) + 60},
                                       "secret")
          result = inspector.inspect(token)
          assert result["payload"]["sub"] == "abc"
      
      
      def test_cli_exit_code_high_issue(capsys):
          token = f'{_b64({"alg": "none"})}.{_b64({"sub": "1"})}.'
          assert inspector.main([token]) == 1
      
      
      def test_cli_bad_input():
          assert inspector.main(["garbage"]) == 2
      
      
      def test_cli_json(capsys):
          token = inspector.sign_hs256({"alg": "HS256"},
                                       {"sub": "1", "exp": int(time.time()) + 60},
                                       "secret")
          inspector.main([token, "--json"])
          data = json.loads(capsys.readouterr().out)
          assert "header" in data and "issues" in data
      
      
      def _low_only_token():
          """RS256 token whose only findings are LOW claim-hygiene issues."""
          now = int(time.time())
          header = {"alg": "RS256", "typ": "JWT"}
          payload = {"sub": "user-1", "iat": now - 60, "exp": now + 3600}
          return "{}.{}.{}".format(_b64(header), _b64(payload),
                                  inspector.b64url_encode(b"sig"))
      
      
      def test_cli_low_only_issues_exit_one(capsys):
          """Reported LOW issues fail the build too (issue #46)."""
          rc = inspector.main([_low_only_token(), "--json"])
          data = json.loads(capsys.readouterr().out)
          assert data["issues"] and all(i["severity"] == "low" for i in data["issues"])
          assert rc == 1
      
      
      def test_cli_min_severity_filters_report_and_exit(capsys):
          rc = inspector.main([_low_only_token(), "--min-severity", "medium",
                               "--json"])
          data = json.loads(capsys.readouterr().out)
          assert data["issues"] == []
          assert rc == 0
      
      
      def test_inspect_min_severity():
          token = f'{_b64({"alg": "none"})}.{_b64({"sub": "1"})}.'
          all_issues = inspector.inspect(token, min_severity="info")
          high_only = inspector.inspect(token, min_severity="high")
          assert len(high_only["issues"]) < len(all_issues["issues"])
          assert all(i["severity"] in ("critical", "high")
                     for i in high_only["issues"])
      
      
      def _info_only_token():
          """RS256 token with every claim present but expired: one INFO issue."""
          now = int(time.time())
          header = {"alg": "RS256", "typ": "JWT"}
          payload = {"iss": "https://issuer.example", "aud": "api", "sub": "user-1",
                     "iat": now - 7200, "nbf": now - 7200, "exp": now - 3600}
          return "{}.{}.{}".format(_b64(header), _b64(payload),
                                  inspector.b64url_encode(b"sig"))
      
      
      def test_cli_info_issue_reported_by_default(capsys):
          """INFO issues stay in the default report and still fail the build."""
          rc = inspector.main([_info_only_token(), "--json"])
          data = json.loads(capsys.readouterr().out)
          assert [i["id"] for i in data["issues"]] == ["exp-past"]
          assert [i["severity"] for i in data["issues"]] == ["info"]
          assert rc == 1
      
      
      def test_cli_min_severity_low_hides_info(capsys):
          rc = inspector.main([_info_only_token(), "--min-severity", "low", "--json"])
          data = json.loads(capsys.readouterr().out)
          assert data["issues"] == []
          assert rc == 0
      
      
      def test_inspect_reports_info_by_default():
          ids = {i["id"] for i in inspector.inspect(_info_only_token())["issues"]}
          assert "exp-past" in ids
      
  • inspector.py 8.3 KB
    from __future__ import annotations
    
    import argparse
    import base64
    import hashlib
    import hmac
    import json
    import sys
    import time
    from dataclasses import dataclass, asdict
    
    def b64url_decode(segment: str) -> bytes:
        pad = "=" * (-len(segment) % 4)
        return base64.urlsafe_b64decode(segment + pad)
    
    def b64url_encode(data: bytes) -> str:
        return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
    
    @dataclass
    class DecodedJWT:
        header: dict
        payload: dict
        signature_b64: str
        signing_input: bytes
    
    def decode(token: str) -> DecodedJWT:
        token = token.strip()
        parts = token.split(".")
        if len(parts) != 3:
            raise ValueError(
                f"Not a well-formed JWT: expected 3 dot-separated parts, got "
                f"{len(parts)}.")
        h_b64, p_b64, s_b64 = parts
        try:
            header = json.loads(b64url_decode(h_b64))
            payload = json.loads(b64url_decode(p_b64))
        except (ValueError, UnicodeDecodeError) as exc:
            raise ValueError(f"Could not decode header/payload: {exc}") from exc
        if not isinstance(header, dict) or not isinstance(payload, dict):
            raise ValueError("Header and payload must be JSON objects.")
        return DecodedJWT(header, payload, s_b64,
                          f"{h_b64}.{p_b64}".encode("ascii"))
    
    @dataclass
    class Issue:
        id: str
        severity: str
        message: str
    
        def to_dict(self) -> dict:
            return asdict(self)
    
    WEAK_ALGS = {"none", "hs256", "hs384", "hs512"}
    
    def audit(jwt: DecodedJWT) -> list[Issue]:
        issues: list[Issue] = []
        alg = str(jwt.header.get("alg", "")).strip()
        alg_lower = alg.lower()
    
        if alg_lower == "none":
            issues.append(Issue("alg-none", "critical",
                                "alg=none: token is unsigned and trivially "
                                "forgeable if the server accepts it."))
        elif not alg:
            issues.append(Issue("alg-missing", "high",
                                "No 'alg' in header — ambiguous verification."))
    
        if "typ" not in jwt.header:
            issues.append(Issue("typ-missing", "low",
                                "Header has no 'typ' field."))
    
        if alg_lower.startswith("hs"):
            issues.append(Issue("alg-symmetric", "medium",
                                f"{alg} is symmetric (HMAC): the verification key "
                                "is the signing secret. Risk of HS/RS confusion "
                                "and brute-forceable weak secrets."))
    
        payload = jwt.payload
        now = int(time.time())
    
        if "exp" not in payload:
            issues.append(Issue("exp-missing", "high",
                                "No 'exp' claim: token never expires."))
        else:
            try:
                exp = int(payload["exp"])
                if exp < now:
                    issues.append(Issue("exp-past", "info",
                                        f"Token expired at {_ts(exp)}."))
                elif exp - now > 60 * 60 * 24 * 365:
                    issues.append(Issue("exp-far", "medium",
                                        "Token lifetime exceeds 1 year — overly "
                                        "long-lived."))
            except (TypeError, ValueError):
                issues.append(Issue("exp-malformed", "medium",
                                    "'exp' is not a numeric timestamp."))
    
        if "iat" in payload:
            try:
                if int(payload["iat"]) > now + 300:
                    issues.append(Issue("iat-future", "medium",
                                        "'iat' is in the future (clock skew or "
                                        "forged token)."))
            except (TypeError, ValueError):
                pass
    
        if "nbf" not in payload:
            issues.append(Issue("nbf-missing", "low",
                                "No 'nbf' (not-before) claim."))
    
        for claim in ("iss", "aud", "sub"):
            if claim not in payload:
                issues.append(Issue(f"{claim}-missing", "low",
                                    f"No '{claim}' claim — weakens validation."))
    
        return issues
    
    def _ts(epoch: int) -> str:
        return time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime(epoch))
    
    DEFAULT_WEAK_SECRETS = [
        "secret", "password", "123456", "changeme", "admin", "jwt", "token",
        "secretkey", "supersecret", "key", "your-256-bit-secret", "test",
        "qwerty", "letmein", "default", "root", "private", "s3cr3t",
        "password123", "secret123", "jwtsecret", "mysecret", "P@ssw0rd", "welcome",
    ]
    
    _HASH_BY_ALG = {"HS256": hashlib.sha256, "HS384": hashlib.sha384,
                    "HS512": hashlib.sha512}
    
    def crack_hmac_secret(jwt: DecodedJWT, candidates) -> str | None:
        alg = str(jwt.header.get("alg", "")).upper()
        hashfn = _HASH_BY_ALG.get(alg)
        if not hashfn:
            return None
        try:
            expected = b64url_decode(jwt.signature_b64)
        except Exception:
            return None
        for cand in candidates:
            key = cand.encode("utf-8") if isinstance(cand, str) else cand
            sig = hmac.new(key, jwt.signing_input, hashfn).digest()
            if hmac.compare_digest(sig, expected):
                return cand if isinstance(cand, str) else cand.decode("utf-8", "ignore")
        return None
    
    def sign_hs256(header: dict, payload: dict, secret: str) -> str:
        h = b64url_encode(json.dumps(header, separators=(",", ":")).encode())
        p = b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
        signing_input = f"{h}.{p}".encode()
        sig = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
        return f"{h}.{p}.{b64url_encode(sig)}"
    
    SEV_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
    
    def filter_by_severity(issues: list[Issue],
                           min_severity: str = "info") -> list[Issue]:
        threshold = SEV_RANK.get(min_severity, SEV_RANK["info"])
        return [i for i in issues if SEV_RANK.get(i.severity, 3) <= threshold]
    
    def inspect(token: str, secret_candidates=None,
                min_severity: str = "info") -> dict:
        jwt = decode(token)
        issues = audit(jwt)
        cracked = None
        if str(jwt.header.get("alg", "")).upper() in _HASH_BY_ALG:
            cands = list(secret_candidates) if secret_candidates else DEFAULT_WEAK_SECRETS
            cracked = crack_hmac_secret(jwt, cands)
            if cracked is not None:
                issues.append(Issue("weak-secret", "critical",
                                    f"HMAC secret cracked from wordlist: '{cracked}'. "
                                    "Anyone can forge valid tokens."))
        issues.sort(key=lambda i: SEV_RANK.get(i.severity, 9))
        issues = filter_by_severity(issues, min_severity)
        return {
            "header": jwt.header,
            "payload": jwt.payload,
            "issues": [i.to_dict() for i in issues],
            "cracked_secret": cracked,
        }
    
    def main(argv: list[str] | None = None) -> int:
        p = argparse.ArgumentParser(
            prog="jwt-inspector",
            description="Decode and audit a JWT; detect weak HMAC secrets.")
        p.add_argument("token", help="the JWT, or '-' to read from stdin")
        p.add_argument("--secret-list", help="wordlist file for HMAC cracking")
        p.add_argument("--json", action="store_true", help="emit JSON")
        p.add_argument("--min-severity", default="info", choices=list(SEV_RANK),
                       help="report issues at or above this severity "
                            "(default: info, i.e. report everything)")
        args = p.parse_args(argv)
    
        token = sys.stdin.read() if args.token == "-" else args.token
        cands = None
        if args.secret_list:
            try:
                with open(args.secret_list, "r", encoding="utf-8", errors="ignore") as fh:
                    cands = [ln.strip() for ln in fh if ln.strip()]
            except OSError as exc:
                print(f"error: {exc}", file=sys.stderr)
                return 2
    
        try:
            result = inspect(token, cands, args.min_severity)
        except ValueError as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 2
    
        if args.json:
            print(json.dumps(result, indent=2))
        else:
            print("== Header ==");  print(json.dumps(result["header"], indent=2))
            print("\n== Payload ==");  print(json.dumps(result["payload"], indent=2))
            print("\n== Findings ==")
            if not result["issues"]:
                print("  No issues found. [OK]")
            for i in result["issues"]:
                print(f"  [{i['severity'].upper():<8}] {i['id']}: {i['message']}")
    
        return 1 if result["issues"] else 0
    
    if __name__ == "__main__":
        try:
            sys.stdout.reconfigure(encoding="utf-8")
        except Exception:
            pass
        raise SystemExit(main())
    
  • SKILL.md 2.7 KB
    ---
    name: jwt-inspector
    description: >-
      Decode and security-audit a JSON Web Token — flag alg=none, missing/excessive
      expiry, symmetric-alg confusion risk, missing claims — and attempt an offline
      HMAC secret crack against a wordlist to detect weak signing keys. Use when the
      user asks to "decode this JWT", "is this token secure?", "audit a JWT", or
      "check if this token uses a weak secret".
    license: MIT
    ---
    
    # JWT Inspector
    
    Decode and audit JSON Web Tokens with **no third-party dependencies**. It
    splits the token, decodes header + payload, evaluates them against a set of
    security checks, and (for HMAC tokens) tries a fast offline crack of the
    signing secret against a wordlist.
    
    ## When to use this skill
    
    - "Decode / inspect this JWT."
    - "Is this token configured securely?"
    - "Does this JWT use a weak/guessable secret?"
    - Auditing auth tokens during a security review.
    
    ## Checks performed
    
    - **alg=none** (critical) — unsigned, forgeable token.
    - **Symmetric alg (HS*)** — HMAC verification key == signing secret; HS/RS
      confusion and brute-force risk.
    - **Missing `exp`** / token never expires; **excessively long** lifetime.
    - **`iat` in the future**, missing `nbf`, missing `iss`/`aud`/`sub`.
    - **Weak HMAC secret** (critical) — cracked from a built-in or supplied wordlist.
    
    ## How to run it
    
    ```bash
    # Decode + audit
    python skills/jwt-inspector/inspector.py "<token>"
    
    # Read token from stdin
    echo "<token>" | python skills/jwt-inspector/inspector.py -
    
    # Try cracking the HMAC secret with a custom wordlist
    python skills/jwt-inspector/inspector.py "<token>" --secret-list rockyou.txt
    
    # JSON output
    python skills/jwt-inspector/inspector.py "<token>" --json
    
    # Only fail CI on high/critical (claim-hygiene notes are LOW)
    python skills/jwt-inspector/inspector.py "<token>" --min-severity high
    ```
    
    **Exit codes:** `0` clean · `1` issues reported · `2` malformed input.
    Every reported issue fails the build. The default reports everything down to
    `info` (including `exp-past`); raise `--min-severity` to `low`/`medium`/`high`
    to filter advisory notes out of both the report and the exit code.
    
    ## Recommended workflow for Claude
    
    1. Run the inspector and read the decoded payload to understand the token.
    2. Report findings ordered by severity; explain the impact of each.
    3. If a secret was cracked, stress that the key is compromised — rotate it and
       move to an asymmetric algorithm (RS256/ES256) where feasible.
    4. Never treat a decoded payload as trusted: decoding ≠ verifying. Remind the
       user that signature verification with the correct key is what matters.
    
    ## Note
    
    Cracking only runs for HMAC algorithms and only against the provided
    wordlist — it is a weak-key *detector*, not a brute-forcer. Only inspect
    tokens you are authorized to handle.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related