Guard Jwt

JWT identifier-safety guard — validates user-supplied JWT compact-serialization strings against the canonical CVE-class refuse list BEFORE hand-off to a signature verifier. KIND is identifier; the gate consumes ctx.identifier (or ctx.token / ctx.jwt). Pair with b.auth.jwt.verifyExternal for cryptographic verification — this layer is the shape / header / claims contract that runs before any HMAC or signature work.

Algorithm-confusion defense: alg=none is universally refused at every profile (RFC 7518 §3.6 explicit-no-signature, the canonical CVE-2015-9235 jsonwebtoken alg:none / CVE-2018-0114 Cisco node-jose embedded-JWK confusion class). The operator-supplied allowedAlgs allowlist defaults to the framework's PQC-first set (ML-DSA-87 / ML-DSA-65 / ML-DSA-44 / SLH-DSA-SHAKE-256{f,s} / SLH-DSA-SHA2-256{f,s} / EdDSA / ES* / RS* / PS*) so HS256-against-RSA-public-key forgery is blocked before the verifier sees the token.

kid path-traversal defense: the gate refuses any header kid that contains .., /, \, or percent-encoded variants — operators that resolve kid to a filesystem path can't escape the keystore directory. The standalone b.guardJwt.kidSafe(kid) helper throws on the same indicators and is the contract every keyResolver implementation must enforce before reading a key file.

Bounded shape: header / payload / signature segments each have their own byte cap (maxHeaderBytes / maxPayloadBytes / maxSignatureBytes) and the total token is bounded by maxBytes. Decompression-bomb-shaped tokens fail at the cap check before any base64url decode. Header JSON is parsed through b.safeJson.parse({ rejectProto: true }) so prototype pollution can't ride a forged header.

Claim sanity: exp in the past, nbf more than nbfFutureSlackMs in the future, and iat more than iatFutureSlackMs in the future all surface as issues — replay / clock-skew detection that doesn't require pulling in a verifier. Required-claims (iss / exp / iat at strict; iss / exp at balanced) are enforced before the verifier so missing-claim refusals fail fast.

typ confusion: any typ outside jwt / jws / at+jwt / id_token flags as suspect — non-JWT tokens coerced into a JWT slot are refused under strict, audited under balanced.

crit discipline: RFC 7515 §4.1.11 mandates refusing tokens that carry crit headers the verifier doesn't understand. The gate's knownCrit allowlist is empty by default — every crit field is unknown unless the operator opts a name in.

Audience verification is the operator's responsibility (the verifier handles it); the guard's required-claims list ensures the operator can't forget to populate aud in their verifier config because the claim must be present at validate time.

Profiles: strict / balanced / permissive. Compliance postures: hipaa / pci-dss / gdpr / soc2. BIDI / null / control / zero-width universal-refuse applies on the raw input string at every profile so trojan-source codepoints can't ride inside a base64url segment.

b.guardJwt.validate(input, opts?) #

stable0.7.49hipaapci-dssgdprsoc2
{
  profile:              "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  allowedAlgs:          string[],
  requiredClaims:       string[],
  knownCrit:            string[],
  algNonePolicy:        "reject"|"audit"|"allow",
  algAllowlistPolicy:   "reject"|"audit"|"allow",
  kidTraversalPolicy:   "reject"|"audit"|"allow",
  typConfusionPolicy:   "reject"|"audit"|"allow",
  expSanityPolicy:      "reject"|"audit"|"allow",
  nbfSanityPolicy:      "reject"|"audit"|"allow",
  iatSanityPolicy:      "reject"|"audit"|"allow",
  critUnknownPolicy:    "reject"|"audit"|"allow",
  nbfFutureSlackMs:     number,
  iatFutureSlackMs:     number,
  maxHeaderBytes:       number,
  maxPayloadBytes:      number,
  maxSignatureBytes:    number,
  maxBytes:             number,
}

Apply the full guard-jwt threat catalog to a JWT compact- serialization string. Returns { ok, issues } per gateContract.aggregateIssues. Detected classes include alg-none (always critical), kid-traversal (always critical), alg-not-allowed, typ-confusion, crit-unknown, exp-past, nbf-far-future, iat-far-future, claim-missing, plus the shape (jwt-shape) / segment-cap (header-cap / payload-cap / signature-cap) / total-cap (jwt-cap) / codepoint-class issues. Header JSON is decoded through b.safeJson.parse({ rejectProto: true }) so prototype-pollution keys are refused before any policy check runs. Operator-supplied opts are bounds-checked; bad opts throw GuardJwtError("jwt.bad-opt").

var algNoneToken =
  "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0." +
  "eyJzdWIiOiJhdHRhY2tlciJ9.";
var rv = b.guardJwt.validate(algNoneToken, { profile: "strict" });
rv.ok;                                              // → false
rv.issues[0].ruleId;                                // → "jwt.alg-none"

var benign =
  "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9." +
  "eyJpc3MiOiJleGFtcGxlIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjE3MDAwMDAwMDB9." +
  "sig";
var ok = b.guardJwt.validate(benign, { profile: "strict" });
ok.ok;                                              // → true

b.guardJwt.sanitize(input, opts?) #

stable0.7.49
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  ...:        every guardJwt.validate opt is honored,
}

Pass-through-or-throw form of validate. JWT compact serialization can't be repaired (every byte feeds the signature) so sanitize either returns the input unchanged when the issue list contains no critical / high entries, or throws GuardJwtError carrying the offending ruleId. Use this when the caller wants a single try/catch boundary instead of an issue-list switch.

var algNoneToken =
  "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0." +
  "eyJzdWIiOiJhdHRhY2tlciJ9.";
try {
  b.guardJwt.sanitize(algNoneToken, { profile: "strict" });
} catch (e) {
  e.code;                                           // → "jwt.alg-none"
}

b.guardJwt.kidSafe(kid) #

stable0.7.49hipaapci-dssgdprsoc2

Throw on any kid value that contains path-traversal indicators (.., /, \, percent-encoded variants) or non-printable control bytes. Returns the input unchanged on success. This is the contract every operator keyResolver MUST run before resolving kid to a filesystem path or KMS key handle — without it, a forged token's kid can escape the keystore directory.

b.guardJwt.kidSafe("tenant-1-2026-05");             // → "tenant-1-2026-05"

try {
  b.guardJwt.kidSafe("../../etc/passwd");
} catch (e) {
  e.code;                                           // → "jwt.kid-traversal"
}

b.guardJwt.compliancePosture(name) #

stable0.7.49hipaapci-dssgdprsoc2

Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardJwtError with code "jwt.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.

var posture = b.guardJwt.compliancePosture("hipaa");
posture;                                             // → overlay clone (mutable)

try {
  b.guardJwt.compliancePosture("not-a-regime");
} catch (e) {
  e.code;                                            // → "jwt.bad-posture"
}

b.guardJwt.buildProfile(opts) #

stable0.7.49
{
  extends:   string|string[],   // base profile name(s) to compose
  ...:       any guard key,      // inline override of resolved keys
}

Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.

var custom = b.guardJwt.buildProfile({ extends: "strict" });
custom;                                              // → composed profile object

b.guardJwt.loadRulePack(pack) #

stable0.7.49

Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardJwtError with code "jwt.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.

var pack = b.guardJwt.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id;                                             // → "tenant-policy"

b.guardJwt.gate(opts?) #

stable0.7.49
{
  profile:           string,    // one of PROFILES; default this guard's default
  compliancePosture: string,    // overlay one of hipaa/pci-dss/gdpr/soc2
  mode:              string,    // one of gateContract MODES; default "enforce"
}

Build the guard's request-boundary gate — a contract-shaped object exposing check(ctx) that host primitives call at their byte moment. This is the factory default chain: serve when no issue, audit-only for info / warn issues, and refuse for any high / critical issue, dispatched to the right ctx field by the guard's KIND. Wired by gateContract.defineGuard through gateContract.buildGuardGate; a guard whose gate diverges (a bespoke sanitize-and-reserialize chain, for example) ships its own gate block instead of this template.

var gate = b.guardJwt.gate({ profile: "strict" });
var decision = await gate.check({ bytes: Buffer.from("...") });
decision.action;                                     // → "serve" | "refuse" | …

b.guardJwt.resolveOpts(opts?) #

stable0.7.49
{
  profile:           string,    // one of PROFILES; default this guard's default
  compliancePosture: string,    // overlay one of hipaa/pci-dss/gdpr/soc2
}

Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardJwtError with code "jwt.bad-opt" / "jwt.bad-posture" on an unknown profile or posture name.

var resolved = b.guardJwt.resolveOpts({ profile: "strict" });
resolved.profile;                                    // → "strict"

Last updated 2026-08-08T16:39:49.652Z by seeder.