Skip to main content

Destructive-operation re-auth challenge

Some operations cannot be undone: deleting a workspace, erasing a contact’s data under GDPR, disabling HIPAA mode, turning off 2FA, or regenerating backup codes. Every one of them stands behind a re-auth challenge — a short-lived, single-use token the gateway demands before it will flip irreversible state. The challenge answers one question a session cookie cannot: is there a person at this keyboard right now, or is this request arriving from a token that has been sitting in a browser since yesterday? The operations currently gated, with the mint endpoint you call first: The full handshake for each route, with request/response schemas, is in Settings endpoints and the workspace and contacts entries in the API reference. This page explains the model they all share, so you can reason about any gated endpoint — including new ones — without reading its handler.

Why a session alone is not enough

A session token proves possession: some process holds a valid credential. It does not prove identity at this instant: sessions are stolen via XSS, browser hijacking, and credential-stuffing passes, and a session minted last week is indistinguishable from one minted this minute. For a read-only endpoint that distinction rarely matters; for an endpoint that destroys data it is the whole question, because:
  • Irreversibility removes the recovery margin. A wrong page of contacts can be re-fetched; a deleted workspace cannot.
  • The attacker-proves-possession window is the entire session lifetime. Gating destruction behind a five-minute proof shrinks that window from weeks to minutes.
  • Silent postures are worse than loud failures. An erasure that fails with a 401 is visible and recoverable; an erasure that succeeds on a hijacked session is neither.
That is why Orbit refuses to run the destructive verb on session recency, and instead performs a fresh handshake.

The mint → carry → consume flow

Every gated operation follows the same three-step flow:
  1. Mint. The client calls the operation’s mint endpoint from an authenticated session. The server issues a fresh random token and returns it verbatim, with the expiry timestamp (five minutes from issue).
  2. Carry. The client carries the token away — in the dashboard this happens invisibly — and attaches it to the destructive call in the X-Reauth-Challenge request header.
  3. Consume. The destructive endpoint extracts the header and consumes the challenge before doing any work. Only a valid, unexpired, correct-scope token lets the verb run.
For the 2FA-facing verbs there is a fourth step: minting requires a credential proof (the current password or a current TOTP code) in the mint request body, and the operational reason for that is the namespace split below.

Single-use semantics and expiry

A challenge token is single-use and timeboxed:
  • Single-use. The consume step burns the token atomically — the first consume that finds a live token wins, and every later consume of the same token finds nothing. A network retry or a duplicate click cannot double-execute the verb.
  • Timeboxed. Tokens die five minutes after mint regardless of use. A token captured from a page refresh, a log line, or a proxy header has a five-minute shadow, not a session-lifetime one.
The consume is a single atomic operation against the store; a genuine outage of that store fails closed — the gate rejects with 401 REAUTH_REQUIRED rather than letting destruction proceed unchecked. If you see 401 REAUTH_REQUIRED despite a fresh token, the first diagnostic is that the mint and the consume were more than five minutes apart, or the token was already burned by a prior attempt.

Two namespaces: session-recency and step-up

There are two token pools, deliberately incompatible with each other: Session-recency challenge (POST .../reauth-challenge) proves the current session is active and the op was triggered interactively. It is the right gate for operations where possession of a live session plus a five-minute confirmation is sufficient assurance: workspace deletion, GDPR erasure of a contact, HIPAA disable, conversation force-transfer. Step-up challenge (POST /api/v1/settings/security/2fa/challenge) proves something stronger: the caller just now presented a fresh credential — the current password, or a current TOTP code — that the auth provider independently verified. Only after that proof does the mint succeed. The two pools never overlap:
  • A session-recency token cannot satisfy a step-up gate.
  • A step-up token cannot satisfy a session-recency gate.
  • The mint endpoints are different routes, with the step-up route requiring the credential fields in its body.
The 2FA-mutating verbs (disable, regenerate backup codes) sit behind the step-up gate because they control the recovery path itself: if a stolen session could disable 2FA without a credential, the session hijack becomes a permanent account takeover.

The binding key

A challenge is bound not only to the operation but, where the operation targets one resource, to the target. The binding key is the target’s identifier, and it changes what a token can authorize:
  • A challenge minted against workspace A cannot authorize the deletion of workspace B.
  • A challenge minted against contact 1 cannot erase contact 2.
Without this binding, a token minted for one target could be replayed against a sibling — a cross-target authorization bypass that the op tag alone cannot prevent. This is why the DELETE /organizations/:id mint endpoint takes the workspace id in its URL: the id you pass at mint must be the id you pass at delete, or the consume rejects. Multi-target operations are deliberately exempt: operations that by design span many resources in one call bind to the operation only, not to any single one of them. The rule to remember is single-target intent → binding key; intentional multi-target sweep → no binding key.

Coverage matrix

The table below is the whole surface at a glance. Scope is what the token is bound to; proof is what minting demands.

Failure semantics

The gate fails closed: any doubt means rejection, never silent permission. The rejects you will see are:
  • Missing header401 REAUTH_REQUIRED. Retry the mint + carry + consume sequence; the header name is X-Reauth-Challenge.
  • Expired token401 REAUTH_REQUIRED. Mint again; the five-minute window starts at mint, not at the start of the gated call.
  • Already-consumed token401 REAUTH_REQUIRED. Burned on the first consume; mint a fresh one rather than retrying the old.
  • Wrong target (binding mismatch)401 REAUTH_REQUIRED. The challenge was minted against a different workspace or contact id; mint against the actual target.
  • Wrong namespace401 REAUTH_REQUIRED. A session-recency token presented to a step-up gate, or the reverse.
  • Failed credential at step-up mint401 CREDENTIAL_REQUIRED from the challenge endpoint itself. Supply the correct current password or TOTP code.
Success and failure both write audit records with IP and user-agent, which is what makes the gate reviewable rather than just restrictive.

Operator surface: the dashboard

The dashboard runs this handshake for you on two surfaces:
  • Settings → General → Danger zone (delete workspace). Clicking delete first calls the mint endpoint, attaches the returned token to the confirmation step, and only then performs the DELETE — the flow is behind the “type DELETE to confirm” dialog.
  • Settings → Compliance → HIPAA. Disabling HIPAA prompts the same challenge first; enabling it does not, because it tightens posture rather than loosens it.
Where a dashboard dialog exists, the operator never sees the token — the UI completes the handshake in one click. Where you integrate directly, you run it: mint, carry, consume, within five minutes.

See also