shadimage
Shadcn UI Auth
authlibpolicysource: ok · hit

OIDC Policy Contract

The OidcPolicy rule type, its matching semantics, validation, and the match/explain functions. No dependencies — the foundation every other oidc-ui item builds on.

npx shadcn add "https://ui.shadcn.com/oidc/r/policy.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Files1 · source cached 10/15/2026, 11:40:16 AM

registry/oidc/lib/policy.ts
/**
 * The rule contract for trusted OIDC callers. Matching semantics:
 *
 * - `issuer` equals the token's `iss` exactly.
 * - Every listed claim must be present with a value in the accepted list;
 *   exact string equality, arrays match on intersection.
 * - Unlisted claims are not checked. A policy with no claim rules never
 *   matches, and validation requires an `aud` rule.
 * - Accepted values must be arrays of plain strings; any other shape fails
 *   closed.
 */
type OidcPolicy = {
  /** OIDC discovery root. https only, no query or fragment. */
  issuer: string
  claims: Record<string, string[]>
  label?: string
}

/** The gate for anything headed to storage; the verifier refuses failures. */
function validateOidcPolicy(policy: OidcPolicy) {
  const errors: { path: string; message: string }[] = []

  if (!policy.issuer) {
    errors.push({ path: "issuer", message: "Issuer is required." })
  } else {
    let url: URL | null = null
    try {
      url = new URL(policy.issuer)
    } catch {
      url = null
    }
    if (!url) {
      errors.push({ path: "issuer", message: "Issuer must be a valid URL." })
    } else {
      if (url.protocol !== "https:") {
        errors.push({ path: "issuer", message: "Issuer must use https." })
      }
      if (url.search || url.hash) {
        errors.push({
          path: "issuer",
          message: "Issuer must not contain a query or fragment.",
        })
      }
    }
  }

  const claims = policy.claims ?? {}
  if (!Object.keys(claims).includes("aud")) {
    errors.push({
      path: "claims.aud",
      message: "An audience (aud) claim is required.",
    })
  }
  for (const [name, accepted] of Object.entries(claims)) {
    if (!name.trim()) {
      errors.push({ path: "claims", message: "Claim names cannot be empty." })
      continue
    }
    // Prototype-chain names silently vanish from plain-object claim maps.
    if (["__proto__", "constructor", "prototype"].includes(name)) {
      errors.push({
        path: `claims.${name}`,
        message: `Claim name "${name}" is not allowed.`,
      })
      continue
    }
    if (!Array.isArray(accepted) || accepted.length === 0) {
      errors.push({
        path: `claims.${name}`,
        message: `Claim "${name}" must accept at least one value.`,
      })
      continue
    }
    if (accepted.some((value) => typeof value !== "string" || value === "")) {
      errors.push({
        path: `claims.${name}`,
        message: `Claim "${name}" has an empty or non-string accepted value.`,
      })
    }
  }

  return errors
}

/** The matching source of truth. `claims` must be a verified payload. */
function explainOidcPolicy(
  policy: OidcPolicy,
  claims: Record<string, unknown>
) {
  const checks: {
    claim: string
    accepted: string[]
    actual: unknown
    ok: boolean
    reason?: "missing" | "mismatch" | "invalid-rule"
  }[] = []

  checks.push({
    claim: "iss",
    accepted: [policy.issuer],
    actual: claims.iss,
    ok: claims.iss === policy.issuer,
    reason:
      claims.iss === policy.issuer
        ? undefined
        : claims.iss === undefined
          ? "missing"
          : "mismatch",
  })

  const rules = Object.entries(policy.claims ?? {})
  if (rules.length === 0) {
    checks.push({
      claim: "aud",
      accepted: [],
      actual: claims.aud,
      ok: false,
      reason: "invalid-rule",
    })
  }

  for (const [name, accepted] of rules) {
    if (
      !Array.isArray(accepted) ||
      accepted.some((value) => typeof value !== "string")
    ) {
      checks.push({
        claim: name,
        accepted: [],
        actual: claims[name],
        ok: false,
        reason: "invalid-rule",
      })
      continue
    }
    const actual = claims[name]
    if (actual === undefined || actual === null) {
      checks.push({
        claim: name,
        accepted,
        actual,
        ok: false,
        reason: "missing",
      })
      continue
    }
    const values = Array.isArray(actual) ? actual : [actual]
    const ok = values.some(
      (value) => typeof value === "string" && accepted.includes(value)
    )
    checks.push({
      claim: name,
      accepted,
      actual,
      ok,
      reason: ok ? undefined : "mismatch",
    })
  }

  return { ok: checks.every((check) => check.ok), checks }
}

function matchOidcPolicy(policy: OidcPolicy, claims: Record<string, unknown>) {
  return explainOidcPolicy(policy, claims).ok
}

function matchOidcPolicies(
  policies: OidcPolicy[],
  claims: Record<string, unknown>
) {
  return policies.find((policy) => matchOidcPolicy(policy, claims)) ?? null
}

export {
  validateOidcPolicy,
  explainOidcPolicy,
  matchOidcPolicy,
  matchOidcPolicies,
}
export type { OidcPolicy }

More from Shadcn UI Auth

Browse all components in this library →