// The PasswordAuth blueprint, as data. Transcribed by hand from the ontology // `password-auth` README + password-auth.als. A *feature* that extends Form: the // user submits a credential, the backend verifies it, and a bounded session is // established — with uniform failures (no username enumeration), lockout, and // silent refresh. It is the host that RememberMe / MFA / Captcha compose onto. import type { Blueprint, StateMachine } from "./blueprint" // The full AuthState lifecycle. Idle → Submitting fans out to Authenticated / // Error / LockedOut; Authenticated ⇄ Refreshing; and reset() / unlock() / signOut() // all return to Idle. Node coordinates are hand-placed for a clean layout. const PASSWORD_AUTH_MACHINE: StateMachine = { initId: "idle", caption: "Session lifecycle", field: "state : AuthState", nodes: [ { id: "idle", x: 30, y: 140, w: 90, h: 44, label: "Idle" }, { id: "submitting", x: 190, y: 140, w: 120, h: 44, label: "Submitting" }, { id: "authenticated", x: 400, y: 140, w: 140, h: 44, label: "Authenticated" }, { id: "refreshing", x: 630, y: 44, w: 130, h: 44, label: "Refreshing" }, { id: "lockedout", x: 175, y: 252, w: 120, h: 44, label: "LockedOut" }, { id: "error", x: 395, y: 252, w: 100, h: 44, label: "Error" }, ], edges: [ { from: "idle", to: "submitting", label: "signIn(c)", kind: "submit", off: -34 }, { from: "submitting", to: "authenticated", label: "succeed(id)", kind: "succeed", off: -34 }, { from: "submitting", to: "lockedout", label: "lockOut()", kind: "lock", off: -20 }, { from: "submitting", to: "error", label: "fail()", kind: "fail", off: -24 }, { from: "authenticated", to: "refreshing", label: "refresh()", kind: "refresh", off: -26 }, { from: "refreshing", to: "authenticated", label: "refreshed(id)", kind: "succeed", off: -26 }, { from: "refreshing", to: "error", label: "refreshFailed()", kind: "fail", off: -30 }, { from: "authenticated", to: "idle", label: "signOut()", kind: "signout", off: 120 }, { from: "error", to: "idle", label: "reset()", kind: "reset", off: -40 }, { from: "lockedout", to: "idle", label: "unlock()", kind: "unlock", off: 30 }, ], } export const passwordAuth: Blueprint = { slug: "password-auth", name: "PasswordAuth", tagline: "Submit a credential, verify it against the backend, and hold a bounded session — with uniform failures, lockout, and silent refresh.", role: "feature", extendsName: "Form", sig: { header: "PasswordAuth ◁ Form", fields: [ { name: "state", type: "AuthState", note: "lifecycle position" }, { name: "identity", type: "lone Identity", note: "present iff Authenticated or Refreshing" }, { name: "pendingCredential", type: "lone Credential", note: "present only during Submitting", }, ], types: [ "AuthState = Idle | Submitting | Authenticated | Refreshing | LockedOut | Error", "Identity { subject : Subject, realm : Realm } -- subject unique per realm", "Credential { username : Username, secret : Secret } -- 8 ≤ len(secret) ≤ 128", "identity ≠ ⊥ ⟺ state ∈ {Authenticated, Refreshing}", "pendingCredential ≠ ⊥ ⟺ state = Submitting", ], }, functions: [ { id: "capture", name: "Capture", fig: "01", caps: ["PasswordAuth", "Form"], verb: "Collect username and secret from the form; mask the secret and mount both fields together so the OS credential manager can pair them.", state: [ ["username", "Username"], ["secret", "Secret"], ], behaviors: [], invariants: [ "len(secret) ∈ [8, 128] — enforced before signIn(c) fires; the upper bound is surfaced to the user, never silently accepted (bcrypt truncates at 72 bytes)", ], failures: [ "Form auto-submit — browser autofill triggers signIn without user intent; the unintended submission locks the user out faster on stale credentials", ], perf: [ "Form responsiveness — input fields remain accessible during Submitting; the submit button is disabled to prevent double-fire", ], notes: [ "Username and password are mounted together on the submitted form so the credential manager can pair them on save.", ], }, { id: "submit", name: "Submit", fig: "02", caps: ["PasswordAuth"], verb: "Transmit the credential over an authenticated channel (TLS) on a user gesture; never log or persist the secret.", state: [ ["state", "AuthState"], ["pendingCredential", "lone Credential"], ], behaviors: [ { sig: "signIn(c)", pre: "state = Idle", eff: "state ← Submitting, pendingCredential ← c", }, ], invariants: [ "pendingCredential ≠ ⊥ ⟺ state = Submitting — the credential is held only across the verification call", "the plaintext secret is never persisted, logged, or held past Submitting", ], failures: [ "Plaintext leak — secret logged, persisted, or kept in app memory past Submitting; exposed to crash dumps, analytics, or an attacker with disk read", "Form auto-submit — autofill fires signIn without a user gesture", ], perf: [ "Verification — signIn to Authenticated or Error completes within 2 s on a stable network", ], notes: [ "The signIn(c) submission must be triggered by a user gesture, never auto-fired on autofill completion.", ], }, { id: "verify", name: "Verify", fig: "03", caps: ["PasswordAuth"], verb: "Backend checks the credential against the credential store; returns an Identity or a uniform failure that never distinguishes unknown-user from wrong-password.", state: [ ["state", "AuthState"], ["identity", "lone Identity"], ], behaviors: [ { sig: "succeed(id)", pre: "state = Submitting, verification passed", eff: "state ← Authenticated, identity ← id, pendingCredential ← ⊥", }, { sig: "fail()", pre: "state = Submitting, verification failed", eff: "state ← Error, pendingCredential ← ⊥", }, ], invariants: [ "succeed(id) requires backend verification — the relying party never trusts client-side validation alone", "fail() does not distinguish 'wrong password' from 'unknown username' — messages stay uniform", ], failures: [ "Username enumeration — fail() reason distinguishes 'no such user' from 'wrong password'; an attacker maps valid usernames before a targeted attack", "Timing attack — verification time differs for valid vs invalid usernames; the attacker infers validity from response latency", "Credential reuse — the backend accepts a known-breached password, so a password compromised elsewhere grants access here", ], perf: [ "Constant-time response — failure responses take the same time regardless of username validity", "Failure visibility — Error surfaces with a uniform message, never revealing whether the username exists", ], notes: [], }, { id: "limit", name: "Limit", fig: "04", caps: ["PasswordAuth"], verb: "Track failed attempts per user / per IP; transition to LockedOut past the threshold.", state: [["state", "AuthState"]], behaviors: [ { sig: "lockOut()", pre: "state = Submitting, attempt threshold exceeded", eff: "state ← LockedOut, pendingCredential ← ⊥", }, ], invariants: [ "state = LockedOut is reached only from Submitting via lockOut() — never user-triggerable directly", ], failures: [ "Brute force — no attempt counting / no lockOut threshold; the attacker tries unbounded password guesses", "Lockout bypass — unlock() callable without elapsed cooldown; the rate limit is defeated and brute force resumes", ], perf: [ "Lockout feedback — LockedOut surfaces to the user within 1 s of trigger, with the cooldown duration shown", ], notes: [], }, { id: "establish", name: "Establish", fig: "05", caps: ["PasswordAuth", "Identity"], verb: "Persist the session token; expose subject and realm to the consumer.", state: [["identity", "lone Identity"]], behaviors: [], invariants: [ "identity ≠ ⊥ ⟺ state ∈ {Authenticated, Refreshing} — no ghost identity outside an active session", "the consumer sees subject and realm; the plaintext secret never crosses the client-persistence boundary", ], failures: [ "Replay (token theft) — the session token leaks (XSS, network capture); the attacker impersonates the user until the token expires", ], perf: [], notes: [ "Client persistence may store Identity.subject / Identity.realm and the composed session credential — never the plaintext secret in any form.", ], }, { id: "refresh", name: "Refresh", fig: "06", caps: ["PasswordAuth"], verb: "Renew the session token silently before expiry; preserve subject and realm.", state: [ ["state", "AuthState"], ["identity", "lone Identity"], ], behaviors: [ { sig: "refresh()", pre: "state = Authenticated, session token refreshable", eff: "state ← Refreshing", }, { sig: "refreshed(id)", pre: "state = Refreshing, id.subject = identity.subject, id.realm = identity.realm", eff: "state ← Authenticated, identity ← id", }, { sig: "refreshFailed()", pre: "state = Refreshing", eff: "state ← Error, identity ← ⊥", }, ], invariants: [ "refreshed(id) preserves both subject and realm — refresh never silently swaps user or tenant", ], failures: [ "Subject swap on refresh — refreshed(id) accepts an id with a different subject or realm; the attacker's refresh response steals the session", "Stale session — the session is used past expiry without refresh; API calls 401 and the user is surprise-logged-out", ], perf: [ "Silent refresh — completes within 200 ms and before any in-flight API call observes a 401", ], notes: [], }, { id: "recover", name: "Recover", fig: "07", caps: ["PasswordAuth"], verb: "Surface Error distinctly from LockedOut; expose reset() and the cooldown-driven unlock().", state: [["state", "AuthState"]], behaviors: [ { sig: "reset()", pre: "state = Error", eff: "state ← Idle" }, { sig: "unlock()", pre: "state = LockedOut, cooldown elapsed", eff: "state ← Idle", }, ], invariants: [ "unlock() is reachable only from LockedOut and requires elapsed cooldown — not user-triggerable", ], failures: [ "Forgotten password loop — no recovery path from LockedOut or an unknown username; the user is permanently locked out with no self-service unblock", ], perf: [], notes: [ "Recovery from a forgotten password is a sibling blueprint (PasswordReset), not part of this contract.", ], }, { id: "terminate", name: "Terminate", fig: "08", caps: ["PasswordAuth"], verb: "Clear the local session and invalidate the session token at the backend.", state: [ ["state", "AuthState"], ["identity", "lone Identity"], ], behaviors: [ { sig: "signOut()", pre: "state = Authenticated", eff: "state ← Idle, identity ← ⊥", }, ], invariants: ["signOut() clears identity — no active session remains locally"], failures: [ "Stale session — a logged-out session token still valid at the backend can be replayed", ], perf: [], notes: [ "A logout is not a credential operation — the secret never crosses the logout boundary; only the session credential is sent so the server can revoke.", ], }, ], coreInvariants: [ "identity ≠ ⊥ ⟺ state ∈ {Authenticated, Refreshing} — no ghost identity outside an active session", "pendingCredential ≠ ⊥ ⟺ state = Submitting — credentials are held only across the verification call", "the plaintext secret is never persisted, logged, or held past Submitting", "refreshed(id) preserves both subject and realm — refresh never swaps user or tenant", "succeed(id) requires backend verification — the relying party never trusts client-side validation alone", "unlock() is reachable only from LockedOut and requires elapsed cooldown — not user-triggerable", "len(secret) ∈ [8, 128] is enforced before signIn(c) fires", ], composition: { caps: [ { id: "core", label: "PasswordAuth : Form", sub: "state, identity", core: true }, { id: "form", label: "Form", sub: "fields, filled, invalid" }, { id: "cred", label: "Credential", sub: "username, secret" }, { id: "identity", label: "Identity", sub: "subject, realm" }, ], funcs: ["Capture", "Submit", "Verify", "Limit", "Establish", "Refresh", "Recover", "Terminate"], links: [ ["form", "Capture"], ["cred", "Capture"], ["cred", "Submit"], ["core", "Submit"], ["core", "Verify"], ["identity", "Verify"], ["core", "Limit"], ["core", "Establish"], ["identity", "Establish"], ["core", "Refresh"], ["identity", "Refresh"], ["core", "Recover"], ["form", "Recover"], ["core", "Terminate"], ["identity", "Terminate"], ], }, stateMachine: PASSWORD_AUTH_MACHINE, related: [ { name: "Form", relation: "extends · base" }, { name: "RememberMe", relation: "composable extension" }, { name: "MFA", relation: "composable extension" }, { name: "Captcha", relation: "composable extension" }, { name: "StatelessSession", relation: "composable extension" }, { name: "PasswordReset", relation: "sibling · recovery" }, { name: "SSO", relation: "sibling · delegated" }, { name: "Passkey", relation: "sibling · passwordless" }, ], }