> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Destructive-operation re-auth challenge

> How Orbit proves a human is at the keyboard before an irreversible operation — the mint → carry → consume challenge flow, its single-use and expiry semantics, the two step-up namespaces, the binding key that pins a challenge to one target, and which endpoints sit behind it.

# 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:

| Gated operation               | Mint endpoint                                      | Gated endpoint                                               |
| ----------------------------- | -------------------------------------------------- | ------------------------------------------------------------ |
| Delete your workspace         | `POST /api/v1/organizations/{id}/reauth-challenge` | `DELETE /api/v1/organizations/{id}`                          |
| Erase a contact (GDPR)        | `POST /api/v1/contacts/{id}/gdpr/delete/challenge` | `DELETE /api/v1/contacts/{id}/gdpr/delete`                   |
| Disable HIPAA mode            | `POST /api/v1/settings/hipaa/reauth-challenge`     | `PUT /api/v1/settings/hipaa` with `enabled: false`           |
| Force-transfer a conversation | `POST /api/v1/settings/inbox/reauth-challenge`     | `POST /api/v1/conversations/{id}/assign` with `force: true`  |
| Disable 2FA                   | `POST /api/v1/settings/security/2fa/challenge`     | `POST /api/v1/settings/security/2fa/disable`                 |
| Regenerate backup codes       | `POST /api/v1/settings/security/2fa/challenge`     | `POST /api/v1/settings/security/2fa/backup-codes/regenerate` |

The full handshake for each route, with request/response schemas, is in
[Settings endpoints](/api-reference/endpoints/settings) 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:

```mermaid theme={null}
sequenceDiagram
    participant Client as Dashboard / API client
    participant API as Orbit API

    Client->>API: POST .../reauth-challenge (authenticated session)
    API-->>Client: { challenge_token, expires_at }
    Client->>API: Destructive call with X-Reauth-Challenge header
    Note over API: Consume check — op, expiry, single-use
    API-->>Client: 200 / 204 (verb executed) or 401 REAUTH_REQUIRED
```

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.

| Gated endpoint                                        | Namespace       | Scope                 | Proof at mint                | Lifetime          |
| ----------------------------------------------------- | --------------- | --------------------- | ---------------------------- | ----------------- |
| `DELETE /organizations/{id}`                          | Session-recency | Organization `{id}`   | Active session               | 5 min, single-use |
| `DELETE /contacts/{id}/gdpr/delete`                   | Session-recency | Contact `{id}`        | Active session               | 5 min, single-use |
| `PUT /settings/hipaa` with `enabled: false`           | Session-recency | The current workspace | Active session (owner)       | 5 min, single-use |
| `POST /conversations/{id}/assign` with `force: true`  | Session-recency | Force-transfer intent | Active session (admin/owner) | 5 min, single-use |
| `POST /settings/security/2fa/disable`                 | Step-up         | The calling user      | Password or TOTP             | 5 min, single-use |
| `POST /settings/security/2fa/backup-codes/regenerate` | Step-up         | The calling user      | Password or TOTP             | 5 min, single-use |

## Failure semantics

The gate fails **closed**: any doubt means rejection, never silent
permission. The rejects you will see are:

* **Missing header** → `401 REAUTH_REQUIRED`. Retry the mint + carry +
  consume sequence; the header name is `X-Reauth-Challenge`.
* **Expired token** → `401 REAUTH_REQUIRED`. Mint again; the five-minute
  window starts at mint, not at the start of the gated call.
* **Already-consumed token** → `401 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 namespace** → `401 REAUTH_REQUIRED`. A session-recency token
  presented to a step-up gate, or the reverse.
* **Failed credential at step-up mint** → `401 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

* [Account security model](/concepts/account-security-model) — the
  posture this gate serves: 2FA, backup codes, the require-2FA bitmap,
  and the audit trail that records every challenge outcome.
* [Authentication and session model](/concepts/authentication-model) —
  the session properties (recency, possession) this page layers on top of.
* [Identity federation (SAML/SCIM)](/concepts/identity-federation-saml-scim) —
  where SP-initiated sessions differ and what idempotent identity applies.
* [Tenant isolation](/concepts/tenant-isolation) — the tenancy context
  that defines the binding key for workspace-level operations.
* [Settings endpoints](/api-reference/endpoints/settings) — the concrete
  request/response schemas for the mint and gated routes.
