Guard Oauth

OAuth 2.x / OIDC authorization-code-flow shape guard — validates user-supplied parameter bundles BEFORE the framework's b.auth.oauth client exchanges them with the IdP. KIND is oauth-flow; the gate consumes ctx.oauthFlow (or ctx.flow) shape { response_type, redirect_uri, state, code_challenge, code_challenge_method, scope, code, iss, _isCallback }. The guard runs the spec-mandated refuse list so misconfigured callers can't downgrade the flow.

PKCE enforcement: strict requires S256 (RFC 7636 + OAuth 2.1; the plain method is a downgrade-attack class). balanced accepts S256 or plain. permissive audits without enforcing. Missing code_verifier AND missing code_challenge always surfaces as oauth.pkce-missing because OAuth 2.1 mandates PKCE for every client class.

state enforcement: required at strict / balanced. Without state-binding the authorization callback is open to CSRF (RFC 6749 §10.12). The guard refuses missing state; operator-side replay defense (rotating + comparing) is the responsibility of the caller's session layer.

nonce is OIDC-specific replay defense — the guard's required- claims parity is enforced via the operator's b.auth.jwt.verifyExternal config, not in the flow shape, so nonce is documented here but checked by the verifier.

redirect_uri exact-match: when the operator supplies allowedRedirectUris, every callback must be a byte-for-byte match. RFC 6749 §3.1.2 + OAuth 2.1 forbid prefix, wildcard, or scheme drift — the canonical CVE-class for this is the "redirect_uri loose-match" account-takeover bug. When no allowlist is configured the gate skips the check (operator-side misconfiguration warning lives in the startup audit, not in per-request issue lists).

response_type allowlist: strict allows only code. balanced adds code id_token. permissive skips. Implicit- flow token and id_token outside OIDC are deprecated in OAuth 2.1 and refused under the strict / balanced allowlists.

Scope-token discipline: every space-separated scope must conform to the RFC 6749 §3.3 charset (%x21 / %x23-5B / %x5D-7E). Whitespace-other-than-space, control bytes, and non-printable bytes in scope tokens are refused under strict / balanced and audited under permissive.

RFC 9207 issuer-on-callback: when the request bundle is marked _isCallback: true, the iss parameter MUST be present at strict — defeats the IdP-mix-up attack class. balanced audits, permissive skips.

Token-introspection bounds: maxParamBytes (default 2 KiB at strict / balanced) and maxBytes (default 8 KiB) cap each parameter and the total flow JSON. Decompression-bomb-shaped clients can't push the introspection / metadata layer past these bounds.

Code-reuse defense: when the operator wires a seenCodeStore with hasSeen(code), the guard refuses any authorization code already exchanged (RFC 6749 §10.5). The store implementation is the operator's responsibility — typically a short-TTL b.cache entry.

Profiles: strict / balanced / permissive. Compliance postures: hipaa / pci-dss / gdpr / soc2. BIDI / null / control / zero-width universal-refuse applies on every string- valued top-level parameter at every profile so trojan-source codepoints can't ride a state or scope value.

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

stable0.7.49hipaapci-dssgdprsoc2
{
  profile:                 "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  pkcePolicy:              "require-s256"|"require-any"|"audit"|"allow",
  statePolicy:             "require"|"audit"|"allow",
  redirectUriPolicy:       "require-exact-allowlist"|"audit"|"allow",
  responseTypePolicy:      "require-allowlist"|"audit"|"allow",
  scopeTamperingPolicy:    "reject"|"audit"|"allow",
  issuerOnCallbackPolicy:  "require"|"audit"|"allow",
  codeReusePolicy:         "reject"|"allow",
  allowedRedirectUris:     string[],
  allowedResponseTypes:    string[],
  seenCodeStore:           { hasSeen: function(code): boolean },
  maxParamBytes:           number,
  maxBytes:                number,
}

Apply the full guard-oauth threat catalog to a flow bundle. Returns { ok, issues } per gateContract.aggregateIssues. Detected classes include pkce-missing, pkce-method (e.g. plain under require-s256), state-missing, redirect-uri-not-allowed, response-type-not-allowed, scope-token-shape, issuer-missing, code-reused (always critical), plus per- parameter param-cap and total-flow flow-cap bounds and codepoint-class issues on every string parameter. Operator- supplied opts are bounds-checked; bad opts throw GuardOauthError("oauth.bad-opt").

var hostile = {
  response_type: "code",
  redirect_uri:  "https://attacker.example/callback",
  scope:         "openid",
};
var rv = b.guardOauth.validate(hostile, { profile: "strict" });
rv.ok;                                              // → false
rv.issues[0].ruleId;                                // → "oauth.pkce-missing"

var benign = {
  response_type: "code",
  redirect_uri:  "https://app.example.com/callback",
  state:         "csrf-rand-1",
  scope:         "openid profile",
  code_challenge: "abc123def456ghi789jkl012mno345pqr678",
  code_challenge_method: "S256",
};
var ok = b.guardOauth.validate(benign, {
  profile: "strict",
  allowedRedirectUris: ["https://app.example.com/callback"],
});
ok.ok;                                              // → true

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

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

Pass-through-or-throw form of validate. OAuth flow bundles can't be partially repaired — a missing state or wrong redirect_uri is a refuse-class outcome, not something the guard can patch up safely. Returns the input unchanged when the issue list contains no critical / high entries; throws GuardOauthError carrying the offending ruleId otherwise.

try {
  b.guardOauth.sanitize({
    response_type: "code",
    redirect_uri:  "https://app.example.com/callback",
    scope:         "openid",
  }, { profile: "strict" });
} catch (e) {
  e.code;                                           // → "oauth.pkce-missing"
}

b.guardOauth.gate(opts?) #

stable0.7.49hipaapci-dssgdprsoc2
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  name:       string,            // gate label for audit trails
  ...:        every guardOauth.validate opt is honored,
}

Build a gateContract.buildGuardGate-shaped gate that pulls ctx.oauthFlow (or ctx.flow) and dispatches to validate. Returns { ok: true, action: "serve" } when the issue list is empty, { ok: true, action: "audit-only", issues } when only low-severity issues fire, and { ok: false, action: "refuse", issues } on any critical / high issue. Compose into the authorization-callback handler before exchanging the code with the IdP — refusal on a hostile callback prevents the token exchange entirely.

var oauthGate = b.guardOauth.gate({
  profile: "strict",
  allowedRedirectUris: ["https://app.example.com/callback"],
});
var rv = await oauthGate.check({
  oauthFlow: {
    response_type: "code",
    redirect_uri:  "https://attacker.example/callback",
    state:         "csrf-rand-1",
    scope:         "openid",
    code_challenge: "abc123def456ghi789jkl012mno345pqr678",
    code_challenge_method: "S256",
  },
});
rv.action;                                          // → "refuse"

b.guardOauth.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 GuardOauthError with code "oauth.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.guardOauth.compliancePosture("hipaa");
posture;                                             // → overlay clone (mutable)

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

b.guardOauth.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.guardOauth.buildProfile({ extends: "strict" });
custom;                                              // → composed profile object

b.guardOauth.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 GuardOauthError with code "oauth.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.guardOauth.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id;                                             // → "tenant-policy"

b.guardOauth.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 GuardOauthError with code "oauth.bad-opt" / "oauth.bad-posture" on an unknown profile or posture name.

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

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