v0.0.1 · system status
Open APIOpenAPI 3.1Zero token storage

Quickstart

From a JWT and a base URL to a fully validated request in five minutes.

Prerequisites

You need:

1. Set the base URL and API key

Every example honours two env vars: JWTSHIELD_URL (defaults to the hosted API at https://api.jwtshield.com) and JWTSHIELD_KEY (the API key from your dashboard). Set them once in your shell:

export JWTSHIELD_URL=https://api.jwtshield.com
export JWTSHIELD_KEY=jws_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Self-hosted

A self-hosted instance does not require JWTSHIELD_KEY. Drop the Authorization header from the examples below and point JWTSHIELD_URL at your deployment (e.g. http://localhost:8000).

2. Decode a token

POST /v1/inspect/token parses the three JWT segments and returns the decoded header, payload, and signature_segment plus a list of suspicious_warnings. It does not verify the signature or any claims.

inspect_token.sh
curl -sS -X POST "$JWTSHIELD_URL/v1/inspect/token" \
-H "Authorization: Bearer $JWTSHIELD_KEY" \
-H "Content-Type: application/json" \
-d '{"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfMSJ9.x"}' | jq .
inspect_token.js
const BASE_URL = process.env.JWTSHIELD_URL || "https://api.jwtshield.com";
const KEY = process.env.JWTSHIELD_KEY;

const r = await fetch(`${BASE_URL}/v1/inspect/token`, {
method: "POST",
headers: {
  "Authorization": `Bearer ${KEY}`,
  "Content-Type": "application/json",
},
body: JSON.stringify({ token: process.argv[2] }),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const body = await r.json();
console.log(body.decoded.header);
console.log(body.decoded.payload);
console.log("suspicious:", body.suspicious_warnings);
inspect_token.py
import os, sys, httpx

BASE_URL = os.environ.get("JWTSHIELD_URL", "https://api.jwtshield.com")
KEY = os.environ["JWTSHIELD_KEY"]
r = httpx.post(
  f"{BASE_URL}/v1/inspect/token",
  headers={"Authorization": f"Bearer {KEY}"},
  json={"token": sys.argv[1]},
  timeout=10.0,
)
r.raise_for_status()
body = r.json()
print(body["decoded"]["header"])
print(body["decoded"]["payload"])
print("suspicious:", body["suspicious_warnings"])

A successful response looks like:

{
  "decoded": {
    "header": { "alg": "HS256", "typ": "JWT" },
    "payload": { "sub": "usr_1" },
    "signature_segment": "x"
  },
  "validated": false,
  "warning": "Token was decoded only. Decoded does not mean validated: signature, issuer, audience, and claim policy were not checked.",
  "suspicious_warnings": ["KID_MISSING", "EXP_MISSING"]
}
Decoded ≠ validated

The validated field on /v1/inspect/token is always false. Treating decoded payload claims as authorization input is the JWT bug class JWTShield exists to prevent. Use /v1/validate/jwt for any decision that affects state.

3. Validate the token

POST /v1/validate/jwt runs the full check: signature verification, algorithm allowlist, issuer match, audience match, expiry/not-before, required claims. Pass either policy (inline) or issuer_profile_id (registered upstream) — exactly one is required.

validate_jwt.sh
curl -sS -X POST "$JWTSHIELD_URL/v1/validate/jwt" \
-H "Authorization: Bearer $JWTSHIELD_KEY" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfMSJ9.x",
"policy": {
  "secret": "your-256-bit-secret",
  "issuer": "https://issuer.example.com",
  "audiences": ["api://backend"],
  "allowed_algs": ["HS256"]
}
}
JSON
validate_jwt.js
const policy = {
secret: "your-256-bit-secret",
issuer: "https://issuer.example.com",
audiences: ["api://backend"],
allowed_algs: ["HS256"],
};

const r = await fetch(`${process.env.JWTSHIELD_URL}/v1/validate/jwt`, {
method: "POST",
headers: {
  "Authorization": `Bearer ${process.env.JWTSHIELD_KEY}`,
  "Content-Type": "application/json",
},
body: JSON.stringify({ token, policy }),
});
const body = await r.json();
if (!body.valid) {
for (const f of body.findings) {
  console.error(`${f.code} (${f.severity}): ${f.message}`);
}
process.exit(1);
}
validate_jwt.py
policy = {
  "secret": "your-256-bit-secret",
  "issuer": "https://issuer.example.com",
  "audiences": ["api://backend"],
  "allowed_algs": ["HS256"],
}
r = httpx.post(
  f"{BASE_URL}/v1/validate/jwt",
  headers={"Authorization": f"Bearer {KEY}"},
  json={"token": token, "policy": policy},
  timeout=10.0,
)
r.raise_for_status()
body = r.json()
if not body["valid"]:
  for f in body["findings"]:
      print(f"{f['code']} ({f['severity']}): {f['message']}")
  raise SystemExit(1)

A successful response:

{
  "valid": true,
  "statuses": {
    "signature": "pass",
    "issuer": "pass",
    "audience": "pass",
    "algorithm": "pass",
    "time": "pass",
    "required_claims": "pass"
  },
  "findings": [],
  "summary": "Token is valid: signature verified, issuer/audience/time/required-claims all passed."
}

4. Handle invalid tokens

Validation failures still return HTTP 200 with valid: false. Inspect findings[] to handle each failure case explicitly. Never short-circuit on statuses.signature == "pass" alone — check valid.

{
  "valid": false,
  "statuses": {
    "signature": "pass",
    "issuer": "pass",
    "audience": "fail",
    "algorithm": "pass",
    "time": "pass",
    "required_claims": "pass"
  },
  "findings": [
    {
      "code": "AUDIENCE_MISMATCH",
      "severity": "error",
      "message": "Token aud claim does not match any allowed audience.",
      "evidence": { "token_aud": "api://other", "allowed_audiences": ["api://backend"] },
      "remediation": "Issue tokens with aud=\"api://backend\" or add \"api://other\" to your policy."
    }
  ],
  "summary": "Token is NOT valid: audience mismatch."
}

The full list of codes is in Errors.

Next steps