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

Validate JWT

Verify a token's signature, issuer, audience, algorithm, time, and required claims.

POST /v1/validate/jwt Verify a token's signature, issuer, audience, algorithm, time, and required claims.

The full validation path. Verifies the signature, then runs claim-level checks against either an inline policy or a registered issuer profile. Always returns HTTP 200; check valid and findings[] to determine the outcome.

Trust source — exactly one

Either policy (inline) or issuer_profile_id (a profile registered via the ISSUER_PROFILES_JSON environment variable on the server). Supplying both, or neither, returns HTTP 422.

Request

body — application/json
Field Type Required Description
token string yes The JWT to validate. Minimum length 1.
policy VerifyPolicy no Inline policy. See VerifyPolicy fields below. Mutually exclusive with issuer_profile_id.
issuer_profile_id string no ID of a profile registered upstream. Mutually exclusive with policy. Minimum length 1.

VerifyPolicy

The trust source you would otherwise hand to a verification library, expressed as a single JSON object.

policy fields
Field Type Required Description
secret string no HMAC shared secret for HS* algorithms. Mutually exclusive with public_key. Minimum length 1.
public_key string (PEM) no Public key for asymmetric algorithms (RS*, ES*, EdDSA). PEM-encoded SubjectPublicKeyInfo. Mutually exclusive with secret.
issuer string yes Expected iss claim. Compared exactly (case-sensitive, trailing slashes matter).
audiences string[] yes Allowed aud values. At least one must match the token's aud claim (string or array).
allowed_algs string[] yes Algorithm allowlist. alg=none is always rejected regardless. Tokens whose alg is not in this list fail with ALGORITHM_INVALID before signature work runs.
required_claims string[] default [] Claims that must be present in the payload. Failures surface as REQUIRED_CLAIM_MISSING.
required_scopes string[] default [] Scopes that must be present in the scope claim (space-delimited).
required_custom_claims object default {} Key-value assertions on arbitrary payload claims.
max_ttl_seconds integer no Maximum acceptable lifetime (exp - iat). Tokens issued with longer lifetimes fail.
clock_skew_seconds integer default 0 Leeway applied to exp, nbf, and iat comparisons.
token_type string no Expected typ header value (e.g. "JWT", "at+jwt"). When set, mismatches fail.

Response — 200

Always HTTP 200. Read valid to determine the outcome.

body — application/json
Field Type Required Description
valid boolean yes True only when every status passes.
statuses.signature "pass" | "fail" yes Cryptographic signature verification.
statuses.issuer "pass" | "fail" yes iss claim match.
statuses.audience "pass" | "fail" yes aud claim match.
statuses.algorithm "pass" | "fail" yes alg in allowlist.
statuses.time "pass" | "fail" yes exp/nbf/iat valid (with clock_skew).
statuses.required_claims "pass" | "fail" yes All required_claims/required_scopes/required_custom_claims present and matched.
findings Finding[] yes Structured failure entries — empty when valid: true. See Finding shape below.
summary string yes One-sentence outcome.
claim_diff object no Present when claim assertions failed; diff of expected vs actual claim values.
metadata object default {} Operational metadata (kid used, JWKS cache state, request_id when audit middleware is active).

Finding

findings[i]
Field Type Required Description
code string yes Stable error code. See Errors.
severity "error" | "warning" yes "error" causes valid: false. "warning" is informational and does not affect valid.
message string yes Human-readable explanation.
evidence object no Structured detail — varies per code. Includes the offending claim values, configured policy values, etc.
remediation string no Suggested fix when one applies cleanly to the cause.
Severity rule

JWTShield uses a fail-closed model: any severity: "error" finding marks the result invalid. Use severity: "warning" findings (e.g. lint-style suggestions) for telemetry, not authorization.

Examples

Pass

200 valid token — every status passes, findings empty
{
  "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.",
  "metadata": {}
}

Fail (audience mismatch)

200 valid: false with structured finding and remediation
{
  "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.",
  "metadata": {}
}

Errors

ChannelCodeCause
400MALFORMED_TOKENToken is not a parseable JWT.
200, in findingsSIGNATURE_INVALIDSignature verification failed.
200, in findingsALGORITHM_INVALIDalg not in allowed_algs.
200, in findingsISSUER_MISMATCHiss doesn’t match policy.
200, in findingsAUDIENCE_MISMATCHNo allowed audience matched.
200, in findingsTOKEN_EXPIREDexp < now.
200, in findingsTOKEN_NOT_YET_VALIDnbf > now.
200, in findingsREQUIRED_CLAIM_MISSINGA required_claims entry isn’t in the payload.
200, in findingsPROFILE_NOT_FOUNDissuer_profile_id is unregistered.
422Request body invalid; both / neither of policy and issuer_profile_id.

Examples (full request)

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

const r = await fetch(`${BASE_URL}/v1/validate/jwt`, {
method: "POST",
headers: {
  "Authorization": `Bearer ${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}`);
}
}
validate_jwt.py
KEY = os.environ["JWTSHIELD_KEY"]
r = httpx.post(
  f"{BASE_URL}/v1/validate/jwt",
  headers={"Authorization": f"Bearer {KEY}"},
  json={
      "token": token,
      "issuer_profile_id": "acme-auth0",
  },
  timeout=10.0,
)
r.raise_for_status()
body = r.json()
print(body["valid"], body["summary"])