Skip to content

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.

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.

  1. client.authLoginStart — open a login challenge bound to the subject and rate-limit posture.
  2. (Browser does the passkey assertion locally — biometric material never leaves the device.)
  3. client.authLoginFinish — close the loop with the assertion plus a presence receipt and a rate-limit-evaluation receipt.
  4. client.authSessionIssue — admit the session atom (or use the session ref returned by authLoginFinish).
  5. client.authSessionRefresh — extend on fresh presence.
  6. client.m13SessionRevoke — terminate.
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.

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.

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).

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.

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.

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.

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.

  • 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.”

  • 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.
  • 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_lifecycle schema exists but the binding from session to admitted atoms is not yet enforced at runtime in production.
  • Real rate-limit overlay. enforce_runtime_overlay: true records 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.