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
| 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.
| 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.
| 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
| 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. |
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
{
"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)
{
"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
| Channel | Code | Cause |
|---|---|---|
| 400 | MALFORMED_TOKEN | Token is not a parseable JWT. |
| 200, in findings | SIGNATURE_INVALID | Signature verification failed. |
| 200, in findings | ALGORITHM_INVALID | alg not in allowed_algs. |
| 200, in findings | ISSUER_MISMATCH | iss doesn’t match policy. |
| 200, in findings | AUDIENCE_MISMATCH | No allowed audience matched. |
| 200, in findings | TOKEN_EXPIRED | exp < now. |
| 200, in findings | TOKEN_NOT_YET_VALID | nbf > now. |
| 200, in findings | REQUIRED_CLAIM_MISSING | A required_claims entry isn’t in the payload. |
| 200, in findings | PROFILE_NOT_FOUND | issuer_profile_id is unregistered. |
| 422 | — | Request body invalid; both / neither of policy and issuer_profile_id. |
Examples (full request)
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"]
}
}'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}`);
}
}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"])