Guide: auth and sessions
Status: capability-state mix. The session lifecycle (issue, inspect, refresh, revoke) walks end-to-end as fixture rehearsals. The login bracket is shape-only for the cryptographic WebAuthn pipeline — no real assertion is verified. Recovery and rate-limit overlay enforcement are fixture-rehearsed only. See 022 gap report item 15.
This guide walks the login-to-session lifecycle for an end user. The goal is a session that ties durable receipts to a specific authenticated human — and that can be cleanly revoked when that human is gone.
Why sessions exist
Section titled “Why sessions exist”A session is the durable handle that says “this caller is acting on
behalf of that authenticated human.” Without it, every receipt the
membrane emits would float free of the human who triggered it. A
Gestalt session is not a bearer token in disguise — it is an
admitted atom binding an actor (a human person), a vessel (the
device they’re using), a human_presence_receipt, an explicit scope
set, and an explicit expiry.
Crucially, a session does not by itself authorize anything sensitive. Sensitive operations require fresh presence on top of the session. See human-auth-flows.md.
The flow
Section titled “The flow”client.authLoginStart— open a login challenge bound to the subject and rate-limit posture.- (Browser does the passkey assertion locally — biometric material never leaves the device.)
client.authLoginFinish— close the loop with the assertion plus a presence receipt and a rate-limit-evaluation receipt.client.authSessionIssue— admit the session atom (or use the session ref returned byauthLoginFinish).client.authSessionRefresh— extend on fresh presence.client.m13SessionRevoke— terminate.
Step 1 — start the login
Section titled “Step 1 — start the login”const tenant = "tenant_node:rheinwerk_calibration";
const login = await client.authLoginStart({ tenant, subject: "human_person:anna", passkey_binding: "passkey_binding:anna_platform_passkey_fixture", relying_party_id: "gestalt.local", origin: "https://gestalt.local", scopes: ["operate", "read_record"], source_ip_hash: "sha256:fixture_ip_hash", device_binding: "device_binding:fixture_browser", fixture: true,});
console.log(login.body.loginChallenge);console.log(login.body.rateLimitEvaluation);authLoginStart opens a passkey challenge and stamps a
rate-limit-evaluation receipt against the source IP hash and device
binding. Both refs are needed downstream. Only the hash of the
source IP is supplied — supplying raw_ip_address with
production_login: true would refuse.
Step 2 — passkey assertion (off-membrane)
Section titled “Step 2 — passkey assertion (off-membrane)”The browser performs the WebAuthn ceremony locally against the user’s authenticator. The membrane sees nothing of the biometric / TPM material; only the assertion artifacts (signed client-data, authenticator-data, signature) come back, hashed.
Step 3 — finish the login
Section titled “Step 3 — finish the login”const finish = await client.authLoginFinish({ tenant, actor: "human_person:anna", vessel: "vessel:fixture_sdk", scopes: ["operate", "read_record"], human_presence_receipt: "human_presence_receipt:fixture_private_presence", login_challenge: login.body.loginChallenge, auth_rate_limit_evaluation: login.body.rateLimitEvaluation, passkey_binding: "passkey_binding:anna_platform_passkey_fixture", device_binding: "device_binding:fixture_browser", expires_in_seconds: 3600, fixture: true,});
console.log(finish.body.session);authLoginFinish consumes the challenge, the rate-limit evaluation,
and the presence receipt — and emits the session atom.
Step 4 — issue session directly (alternative path)
Section titled “Step 4 — issue session directly (alternative path)”For an already-authenticated browser tab that needs a fresh session against a different scope:
const issued = await client.authSessionIssue({ tenant, actor: "human_person:anna", vessel: "vessel:fixture_sdk", scopes: ["operate"], human_presence_receipt: "human_presence_receipt:fixture_private_presence", device_binding: "device_binding:fixture_browser", expires_in_seconds: 1800, fixture: true,});Both authLoginFinish and authSessionIssue require a fresh
human_presence_receipt. A session cannot bootstrap itself out of
nothing.
Step 5 — refresh
Section titled “Step 5 — refresh”const refreshed = await client.authSessionRefresh({ tenant, session: issued.body.session, scopes: ["operate"], human_presence_receipt: "human_presence_receipt:fixture_private_presence", device_binding: "device_binding:fixture_browser", expires_in_seconds: 1800, fixture: true,});Refresh requires fresh presence: a stolen session token cannot refresh itself. The Koerper running the refresh must re-prompt for a passkey assertion (or a face-match fallback).
Step 6 — revoke
Section titled “Step 6 — revoke”const revoked = await client.m13SessionRevoke({ tenant, session: issued.body.session, enforce_runtime_overlay: true,});enforce_runtime_overlay: true tells the runtime to immediately
treat the session as invalid. Without the overlay, the revocation is
a recorded intent but the runtime keeps honoring the session until
expiry — set it to true for any real revocation.
Side branches
Section titled “Side branches”Inspect a session
Section titled “Inspect a session”const inspect = await client.authSessionInspect({ tenant, session: issued.body.session,});The only call in this family that may be made without a fresh presence receipt — read-only and does not authorize anything itself.
Soft-limit enforcement
Section titled “Soft-limit enforcement”const rate = await client.authRateLimitEvaluate({ tenant, policy_ref: "auth_rate_limit_policy:fixture_default", subject: "human_person:anna", route: "/v1/auth/login/finish", action: "login_finish", source_ip_hash: "sha256:fixture_ip_hash", device_binding: "device_binding:fixture_browser", window_seconds: 300, attempt_count: 4, soft_limit: 3, lockout_threshold: 10, lockout_seconds: 900, fixture: true,});
console.log(rate.body.softLimitTripped);console.log(rate.body.lockoutTripped);A softLimitTripped Koerper surfaces “slow down” UX. A
lockoutTripped Koerper refuses the next login finish until the
lockout window expires. The evaluation also produces the receipt
that is load-bearing for the subsequent authLoginFinish.
Recovery
Section titled “Recovery”When a human has lost access to their passkey:
const policy = await client.authRecoveryPolicy({ tenant, actor: "human_person:anna", human_presence_receipt: "human_presence_receipt:fixture_private_presence", recovery_policy_hash: "sha256:fixture_recovery_policy", recovery_contact_hashes: [ "sha256:fixture_recovery_contact_email", "sha256:fixture_recovery_contact_phone", ], recovery_methods: ["email_link", "sms_otp"], fixture: true,});
const exec = await client.authRecoveryExecute({ tenant, actor: "human_person:anna", recovery_policy: policy.body.recoveryPolicy, replacement_passkey_binding: "passkey_binding:anna_replacement_passkey_fixture", human_presence_receipt: "human_presence_receipt:fixture_private_presence", recovery_contact_hash: "sha256:fixture_recovery_contact_email", fixture: true,});Recovery requires fresh presence at both steps.
create_standing_from_recovery: true is refused — phishing a
recovery flow cannot promote an attacker to a company officer.
Privacy invariants
Section titled “Privacy invariants”- Hash-only IP and user-agent. Raw values are refused in production posture.
- No biometric material on the wire. Passkey assertion artifacts reach the membrane only as content hashes.
- No raw identifiers in receipts. Recovery contact hashes are carried; raw email / phone is refused.
- Presence does not create standing. Every operation that takes
create_standing_from_*will refuse it.
A Koerper should display these explicitly next to every login UI: “Your fingerprint and face never reached our servers. Logging in here does not change your role inside the company.”
What this proves
Section titled “What this proves”- The session lifecycle has structural endpoints with admitted receipts at every step.
- Rate-limit evaluation and presence verification are independent preconditions for login finish — neither alone is enough.
- Sessions cannot bootstrap themselves out of nothing; standing cannot be back-doored through recovery.
What does not work yet
Section titled “What does not work yet”- Real WebAuthn pipeline. The login bracket walks the shape, but passkey verification is fixture flag-driven, not a real assertion-against-credential cryptographic check.
- Real session-binding to durable atoms. Sessions today are
fixture-rehearsed. The
session_lifecycleschema exists but the binding from session to admitted atoms is not yet enforced at runtime in production. - Real rate-limit overlay.
enforce_runtime_overlay: truerecords the intent; the actual cluster-wide overlay enforcement is staged. - Real recovery contact verification. The flow does not actually deliver an email or SMS — only the structural receipt is admitted.
See 022 item 15.
Where to read next
Section titled “Where to read next”- API: auth and sessions
- API: human auth
- API: authority —
presenceApproval,sessionRevoke,keyRotate - Guide: human-auth flows
- Guide: standing and mandates — what does create standing