feat: add PasswordAuth blueprint illustration
The primary-auth feature RememberMe composes onto. Transcribed from the ontology password-auth README + .als: the full AuthState lifecycle (Idle / Submitting / Authenticated / Refreshing / LockedOut / Error), its 8 functions, and a sign-in specimen shown across those states. Adds the auth transition kinds and wraps the state-machine legend to a second row so this richer machine's kinds fit without clipping.
This commit is contained in:
348
src/data/passwordAuthBlueprint.ts
Normal file
348
src/data/passwordAuthBlueprint.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
// 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" },
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user