---
id: 4
title: "Authentication for a multi-tenant SaaS product"
version: "1.0.0"
status: stable
date: 2026-07-27
authors:
  - "Theo Zourzouvillys"
tags: [security, saas, auth, identity, sso, multi-tenancy]
summary: "A complete specification for SaaS authentication: one global user, explicit per-org membership, server-authoritative sessions that die with membership, SSO enforcement that closes every side door, scoped invitations, org-owned API keys, and audited support access."
use_when: "You are building sign-in for a product where people belong to customer organisations, and one person may belong to several."
not_for:
  - "Service-to-service authentication — workloads should get tokens from an STS, not sign in."
  - "Authorisation inside an organisation; this establishes who the caller is and which org they act in, not what they may do to a given record."
  - "Consumer products with no organisation concept, where the org model is pure overhead."
requires: [1, 3]
provides:
  - "saas-authentication"
  - "organization-membership"
  - "session-management"
implements_notes: [8, 10, 27, 37, 38, 40]
supersedes: null
superseded_by: null
aliases: []
crossrefs:
  ZFN-8: "The root of the impersonation position here: support access is recorded as the operator acting for the organisation, never as the customer performing the action."
  ZFN-10: "Applied here to the organisation named in a request, which is a switch to validate rather than context to trust."
  ZFN-27: "The standard for who gets to be inside a system at all, applied to access rather than hiring — the framing behind treating operator access as conditional and revocable rather than standing."
  ZFN-37: "Why sessions carry both idle and absolute lifetimes, and why support grants expire automatically instead of being cleaned up later."
  ZFN-38: "Directly behind the delegation model and the requirement that both actor and principal appear on every action."
  ZFN-40: "Why a machine principal has a named accountable owner and why its use is attributable in audit."
  ZBP-1: "Sender-constrained credentials. Cited by ZBP-4-R46: where a client can hold a key, an organisation's API keys should be bound to it so a leaked key alone is not enough to use."
  ZBP-2: "The workload counterpart to this document, and a deliberate contrast: it makes verification a local computation because volume is enormous and lifetimes are minutes, where this makes the session store authoritative because immediate revocation matters more."
  ZBP-3: "The isolation boundary this document produces a tenant context for. Required, not optional: an authentication layer that resolves the right organisation is worth nothing if the data layer does not enforce it."
references:
  - id: rfc9700
    title: "RFC 9700 — Best Current Practice for OAuth 2.0 Security"
    url: https://www.rfc-editor.org/rfc/rfc9700
    abstract: "The consolidated modern guidance for OAuth deployments: PKCE everywhere, exact redirect URI matching, no implicit grant, sender-constrained tokens, and the attack classes each mitigates. Where the older specifications left latitude, this closes it."
  - id: rfc7519
    title: "RFC 7519 — JSON Web Token (JWT)"
    url: https://www.rfc-editor.org/rfc/rfc7519
    abstract: "The JWT format: a set of claims, serialised as JSON, signed or encrypted as a JOSE object. The container most session and access tokens use, and the reason explicit typing and algorithm pinning matter so much elsewhere in this library."
  - id: rfc7644
    title: "RFC 7644 — System for Cross-domain Identity Management: Protocol (SCIM)"
    url: https://www.rfc-editor.org/rfc/rfc7644
    abstract: "The protocol enterprise identity providers use to push user and group lifecycle into a downstream application: create, update, deactivate. The mechanism by which an employee leaving a customer's directory becomes a deprovisioned account in your product, rather than a stale one."
  - id: owasp-authn
    title: "OWASP Authentication Cheat Sheet"
    url: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
    abstract: "Practical guidance on password storage, credential rotation, account recovery, and the failure modes of each. Most useful here for the recovery flows, which are consistently the weakest path into an account and the least examined."
---

## TL;DR

This specifies sign-in for a product where people belong to customer organisations. The shape of the
answer: **one global user account per person; membership in an organisation as an explicit, revocable
record; a server-authoritative session that cannot outlive the membership it depends on; and an
organisation's security settings that bind every path into it, not just the front door.**

The failure that matters here is not usually a broken password check. It is a side door: a session
that survives an employee's removal, an API key that belongs to a person who left, an invitation that
grants more than the inviter had, a domain auto-join on a domain nobody verified, or a support tool
that can enter any organisation without leaving a trace. This document is largely a specification of
those doors.

It builds on [ZBP-3](/zbp/3-tenant-isolation/), which enforces the boundary this establishes, and uses
[ZBP-1](/zbp/1-dpop-bound-requests/) for machine credentials.

## Applicability

**Use this when** your product has customer organisations with more than one person in them, when one
person may belong to more than one organisation, or when customers will eventually ask for SSO,
directory sync, and audit — which they will, at the exact moment the deal is large enough to matter.

**Do not use this when:**

- **The caller is a workload, not a person.** Services should not "sign in". They attest and exchange
  for a short-lived token — [ZBP-2](/zbp/2-workload-security-token-service/).
- **You need authorisation, not authentication.** This resolves *who* is calling and *which
  organisation* they are acting in. What they may then do to a specific record is a separate layer,
  and merging the two produces a permission model nobody can reason about.
- **There are no organisations.** A consumer product where each account is one person does not need
  memberships, invitations, or SSO enforcement, and adding them costs real complexity.

## Scope and non-goals

In scope: the user and organisation model; membership and its revocation; session establishment,
resolution, and lifetime; credentials and multi-factor; federated sign-in and its enforcement; domain
verification; directory-driven provisioning and deprovisioning; invitations; machine principals owned
by an organisation; support and impersonation access; and audit.

Out of scope: intra-organisation authorisation and role design; the tenant isolation enforcement point
(ZBP-3); workload identity (ZBP-2); billing and entitlements; and the specifics of any one identity
provider's implementation quirks.

## Vocabulary

- **User** — a person. Exactly one user record per person, globally, regardless of how many
  organisations they belong to.
- **Organisation** — the tenant. The unit of isolation in ZBP-3 and the unit of ownership here.
- **Membership** — the explicit record joining a user to an organisation, carrying their roles and
  status within it. The authorisation-relevant object; a user alone is not.
- **Session** — server-authoritative state representing an authenticated person on a device.
- **Session token** — the opaque value the client presents, naming a session.
- **Active organisation** — which of the caller's memberships the current request is acting under.
- **Machine principal** — a non-human identity owned by an organisation: an API key or service account.
- **Support access** — a deliberate, audited path by which an operator of the product acts inside a
  customer organisation.
- **Identity provider (IdP)** — the customer's own directory, used for federated sign-in.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as in RFC 2119 and
RFC 8174.

## Architecture

```mermaid
flowchart TB
    P([person])
    IDP["customer IdP"]
    AUTH["`**AUTHENTICATION**
→ user (global, one per person)`"]
    SESS["`**session**
server-authoritative, revocable in one place
records user · auth method · MFA · IdP · device`"]
    MEM["`**MEMBERSHIP** (user × organisation)
status · roles · source
*revoked here ⇒ every session's access to that org ends*`"]
    ENF["`**ZBP-3 enforcement**
(tenant isolation)`"]

    P -- "sign-in: password + MFA,<br/>or federated to the org IdP" --> AUTH
    IDP -- "OIDC / SAML · SCIM" --> AUTH
    AUTH -- creates --> SESS
    SESS -- "per request:<br/>session → user → active org → membership" --> MEM
    MEM -- "yields tenant context" --> ENF
```

Three structural choices carry the design:

1. **The membership is the authorisation object, not the user and not the session.** A session says a
   person is who they claim. Only a live membership says they may act in this organisation, and it is
   checked on every request rather than baked into a token at sign-in. This is what makes removal
   actually remove.
2. **The session is server-authoritative.** This is the opposite of the choice
   [ZBP-2](/zbp/2-workload-security-token-service/) makes for workloads, deliberately. There, request
   volume is enormous and lifetimes are minutes, so verification must be a local computation. Here,
   volume is human-scale and sessions last days, so immediate revocation matters far more than saving
   a lookup. Same architect, opposite conclusion, because the forces genuinely differ — and a system
   that copies the workload answer into the human path ends up unable to log anyone out.
3. **Organisation security settings bind every path.** SSO enforcement that closes the password form
   but leaves existing sessions, personal API keys, and recovery flows open has not enforced anything.
   Most of the requirements below are that idea applied to each door in turn.

## Normative requirements

### Users and organisations

- **ZBP-4-R1** A person MUST have exactly one user record, regardless of how many organisations they
  belong to. Per-organisation duplicate user records MUST NOT be created.
- **ZBP-4-R2** A user record MUST be able to exist with no organisation membership, and MUST remain
  addressable after losing all memberships, so that history and audit references stay resolvable.
- **ZBP-4-R3** Identifiers for users, organisations, memberships, and sessions MUST be opaque and
  non-enumerable, per ZBP-3-R1.
- **ZBP-4-R4** An email address MUST NOT by itself confer access to any organisation. It is a
  contactable identifier and a verification target, never an authorisation input.
- **ZBP-4-R5** Email addresses MUST be verified before use for sign-in, recovery, or invitation
  matching, and MUST be re-verified on change.
- **ZBP-4-R6** An organisation MUST have a security configuration — enforced sign-in methods, MFA
  requirement, session lifetime, allowed domains — that is owned by that organisation and MUST NOT be
  weakened by any individual member's preference.
- **ZBP-4-R7** Deleting an organisation MUST NOT delete the user records of its members, and MUST
  revoke all memberships, sessions scoped to it, and machine principals owned by it.
- **ZBP-4-R8** The system MUST support one person holding memberships in multiple organisations
  simultaneously, including organisations with conflicting security configurations.

### Membership and request context

- **ZBP-4-R9** Membership MUST be an explicit record with a status. Absence of a membership record
  MUST deny; there MUST NOT be an implicit membership derived from email domain, invitation, or
  history.
- **ZBP-4-R10** Every request MUST resolve, in order: session → user → active organisation →
  membership → roles. Failure at any step MUST deny.
- **ZBP-4-R11** Membership MUST be re-checked on every request against authoritative state. Roles and
  membership status MUST NOT be read solely from a token or cookie issued earlier.
- **ZBP-4-R12** Revoking a membership MUST take effect within a bounded, documented interval that MUST
  NOT exceed 60 seconds, across every surface including active sessions, in-flight background work,
  and cached authorisation decisions.
- **ZBP-4-R13** The active organisation MUST be resolved to a membership the session's user actually
  holds. A client-supplied organisation identifier MUST be treated as a request to switch, validated
  against membership, and MUST NOT be trusted as context (ZBP-3-R3).
- **ZBP-4-R14** Switching the active organisation MUST re-evaluate that organisation's security
  configuration, and MUST deny if the current session does not satisfy it — for example a
  password-authenticated session switching into an SSO-enforced organisation.
- **ZBP-4-R15** The last member able to administer an organisation MUST NOT be removable or
  demotable without an explicit ownership transfer, so that an organisation cannot be orphaned.

### Sessions

- **ZBP-4-R16** A session MUST be server-authoritative: a record the server can revoke, whose
  revocation takes effect immediately for subsequent requests.
- **ZBP-4-R17** The session token MUST be opaque to the client, MUST carry at least 128 bits of
  entropy, and MUST be stored hashed at rest so that a database disclosure does not yield usable
  tokens.
- **ZBP-4-R18** A session MUST record how it was authenticated: the method, whether MFA was
  satisfied, the identity provider if federated, and the approximate device and client.
- **ZBP-4-R19** Sessions MUST have both an idle timeout and an absolute maximum lifetime, both
  configurable per organisation, with the strictest applicable value winning for a user with several
  memberships.
- **ZBP-4-R20** The session token MUST be rotated on privilege change — sign-in, MFA completion,
  step-up, and password change — and the previous value MUST be invalidated.
- **ZBP-4-R21** Browser-delivered session tokens MUST be set `HttpOnly`, `Secure`, and `SameSite=Lax`
  or stricter, and state-changing requests MUST be protected against cross-site request forgery
  independently of the cookie.
- **ZBP-4-R22** A user MUST be able to enumerate their active sessions and revoke any or all of them,
  and an organisation administrator MUST be able to revoke that organisation's members' sessions.
- **ZBP-4-R23** Changing a credential — password, MFA factor, or recovery method — MUST revoke all
  other sessions for that user by default.
- **ZBP-4-R24** A session MUST NOT grant access to an organisation whose membership has been revoked,
  even if the session itself remains valid for other organisations.

### Credentials and multi-factor

- **ZBP-4-R25** Passwords, where supported, MUST be stored with a memory-hard hash with per-credential
  salt, MUST be checked against a breached-password corpus at set time, and MUST NOT be subject to
  composition rules that reduce entropy.
- **ZBP-4-R26** Phishing-resistant factors — passkeys and hardware authenticators — MUST be
  supported, and an organisation MUST be able to require them.
- **ZBP-4-R27** Where an organisation requires MFA, that requirement MUST apply to every
  authentication path into it, including federated sign-in, recovery, and machine principal creation.
- **ZBP-4-R28** Sensitive operations — changing security configuration, managing machine principals,
  transferring ownership, exporting data — MUST require step-up re-authentication within a short,
  bounded recency window.
- **ZBP-4-R29** Account recovery MUST NOT bypass an organisation's enforced sign-in method or MFA
  requirement. Recovery restores access to the *user*; it MUST NOT restore access to an
  SSO-enforced organisation without the identity provider.
- **ZBP-4-R30** Authentication responses MUST NOT reveal whether an account exists, and rate limiting
  MUST apply per account and per source independently.

### Federated sign-in and provisioning

- **ZBP-4-R31** A domain MUST be verified by a control proof — a DNS record or a document served from
  the domain — before it may be associated with an organisation. Unverified domains MUST NOT
  influence any routing or joining decision.
- **ZBP-4-R32** Domain-based automatic joining, where offered, MUST require a verified domain, MUST be
  explicitly enabled by an organisation administrator, and MUST be recorded on each membership it
  creates.
- **ZBP-4-R33** A verified domain MUST NOT be claimable by a second organisation while the claim is
  active, and a public email provider's domain MUST NOT be claimable at all.
- **ZBP-4-R34** Federated assertions MUST be validated against the organisation's configured provider:
  signature, issuer, audience, recency, and single use of the assertion identifier. The
  organisation MUST be determined by the connection the assertion arrived on, never by a claim inside
  it.
- **ZBP-4-R35** When an organisation enforces SSO, every other path into that organisation MUST be
  closed for its members: password sign-in, existing non-SSO sessions, and personal machine
  principals. Enabling enforcement MUST invalidate sessions that do not satisfy it.
- **ZBP-4-R36** Directory-driven deprovisioning MUST revoke the membership, its sessions, and its
  machine principals within the interval in ZBP-4-R12.
- **ZBP-4-R37** Directory-driven provisioning MUST NOT grant administrative roles unless the mapping
  from directory group to role is explicitly configured by an organisation administrator.
- **ZBP-4-R38** Loss of connectivity to a customer's identity provider MUST fail closed for new
  sessions, and MUST NOT silently fall back to password authentication. A documented break-glass path
  MAY exist; it MUST be time-bounded and audited.

### Invitations

- **ZBP-4-R39** An invitation MUST be single-use, MUST expire, MUST be bound to a specific email
  address, and MUST be redeemable only by a user who has verified that address.
- **ZBP-4-R40** An invitation MUST NOT grant roles exceeding those the inviter holds and is permitted
  to delegate, and the granted roles MUST be re-validated at redemption, not only at issue.
- **ZBP-4-R41** Invitation tokens MUST be integrity-protected such that no field — organisation,
  email, or roles — can be modified without detection, and MUST be revocable before redemption.
- **ZBP-4-R42** Redeeming an invitation MUST create an explicit membership record and MUST be audited
  with the inviter, redeemer, roles, and time.

### Machine principals within an organisation

- **ZBP-4-R43** A machine principal MUST be owned by the organisation, not by the person who created
  it, and MUST survive that person's departure without granting continued access to that person.
- **ZBP-4-R44** A machine principal MUST have a named human owner for accountability, an expiry, and
  scopes narrower than or equal to the creating member's authority at creation time.
- **ZBP-4-R45** Machine principal credentials MUST be shown once, stored hashed, and MUST be
  individually revocable. Their use MUST be attributable in audit to the principal, per
  [ZFN-40](/zfn/40-no-anonymous-system-actor/).
- **ZBP-4-R46** Where the client can hold a key, machine principal credentials SHOULD be
  sender-constrained per [ZBP-1](/zbp/1-dpop-bound-requests/), so that a leaked key is not sufficient
  to use it.

### Support access and impersonation

- **ZBP-4-R47** Product operators MUST NOT hold standing access to customer organisations. Access MUST
  be requested per incident, time-bounded, and expire automatically.
- **ZBP-4-R48** Support access MUST be recorded as the operator acting for the organisation, with both
  identities present — never by assuming a customer user's identity such that the audit trail shows
  the customer performing the action.
- **ZBP-4-R49** Support access MUST be visible to the customer: surfaced in their own audit log, and
  where the organisation requires it, gated on explicit customer approval.
- **ZBP-4-R50** Support sessions MUST be read-only by default; write access MUST be a separate,
  explicitly elevated grant with its own justification and approval.

### Audit

- **ZBP-4-R51** Authentication events — sign-in, failure, MFA, session revocation, membership change,
  role change, invitation, machine principal lifecycle, support access — MUST be recorded with actor,
  organisation, method, source, and outcome.
- **ZBP-4-R52** Each organisation MUST be able to read its own audit records, and those records MUST
  NOT contain another organisation's data (ZBP-3-R26).

## Data model

The identity core — a person, the organisations they belong to, and the sessions they hold:

```mermaid
erDiagram
    USER ||--o{ MEMBERSHIP : holds
    ORGANISATION ||--o{ MEMBERSHIP : grants
    USER ||--o{ SESSION : "authenticates as"

    MEMBERSHIP {
        string status
        string roles
        string source "invite|sso|domain"
    }
    SESSION {
        string token_hash "hashed at rest"
        string auth_method "password|passkey|sso"
        bool mfa_satisfied
        string expires_at "idle + absolute"
    }
```

What an organisation owns — the settings and principals that belong to the tenant rather than to any
person in it:

```mermaid
erDiagram
    ORGANISATION ||--|| SECURITY_CONFIG : configures
    ORGANISATION ||--o{ VERIFIED_DOMAIN : claims
    ORGANISATION ||--o{ MACHINE_PRINCIPAL : owns

    SECURITY_CONFIG {
        bool enforced_sso
        bool require_mfa
        int session_idle
        int session_max
    }
    VERIFIED_DOMAIN {
        string proof
        string verified_at
    }
    MACHINE_PRINCIPAL {
        string owner_user "accountable human"
        string expires_at
        string scopes
    }
```

`membership.source` is not bookkeeping. When an organisation turns on SSO enforcement (ZBP-4-R35) or a
directory removes a user (ZBP-4-R36), it is how you determine which memberships and sessions are
affected. Without it, enforcement becomes a guess.

Note what the session record does **not** contain: roles, or the organisation's permissions. Those are
resolved per request from membership (ZBP-4-R11), which is what makes ZBP-4-R12 achievable at all.

## Request resolution algorithm

Every authenticated request, in this order:

| # | Step | Requirement | Failure |
|---|---|---|---|
| 1 | Session token present and well-formed | ZBP-4-R17 | 401 |
| 2 | Session found by token hash, not expired, not revoked | ZBP-4-R16 | 401 |
| 3 | Idle and absolute lifetimes satisfied | ZBP-4-R19 | 401 |
| 4 | User resolved and not disabled | ZBP-4-R1 | 401 |
| 5 | Active organisation resolved from session state, or from a validated switch | ZBP-4-R13 | 403 |
| 6 | Membership found for (user, organisation) and status is active | ZBP-4-R9, ZBP-4-R10 | 403 |
| 7 | Organisation security config satisfied by this session | ZBP-4-R14, ZBP-4-R35 | 403 or re-auth |
| 8 | Roles loaded from the membership record, not from the session | ZBP-4-R11 | 403 |
| 9 | Step-up recency satisfied, if the operation requires it | ZBP-4-R28 | 403 + step-up |
| 10 | Tenant context established and handed to the ZBP-3 enforcement point | ZBP-3-R2 | deny |

Step 6 before step 8, always. Reading roles from anywhere but the live membership record is what
produces the class of bug where a removed administrator keeps administering until their session
happens to expire.

Step 7 is the one most often missing entirely. A session created before an organisation enabled SSO
enforcement satisfies steps 1–6 perfectly well; only an explicit re-evaluation against the current
configuration closes it.

## Test vectors

### Invitation token

An invitation is verified only by its issuer, so a MAC is the right primitive — no public verification
is needed and a symmetric key is simpler to rotate. **This is a published test key and MUST NOT be
used for anything real.**

```
key (base64url, HMAC-SHA256): ZmllbGQtbm90ZXMtdGVzdC1pbnZpdGF0aW9uLWtleS12MQ
token form:  inv_<base64url(payload)>.<base64url(HMAC-SHA256(key, base64url(payload)))>
```

Payload:

```json
{
  "v": 1,
  "jti": "01JZQ9M4T7C2X8B5N0KWERZDAH",
  "org": "org_7Fq2mKdX9pLs",
  "email": "sam@customer.example",
  "roles": ["member"],
  "inviter": "usr_3Bn8vQwR1tYc",
  "iat": 1782000000,
  "exp": 1782604800
}
```

**V-INV1 — valid (expect: accept, then be unusable a second time).** Verified as authentic.

```
inv_eyJ2IjoxLCJqdGkiOiIwMUpaUTlNNFQ3QzJYOEI1TjBLV0VSWkRBSCIsIm9yZyI6Im9yZ183RnEybUtkWDlwTHMiLCJlbWFpbCI6InNhbUBjdXN0b21lci5leGFtcGxlIiwicm9sZXMiOlsibWVtYmVyIl0sImludml0ZXIiOiJ1c3JfM0JuOHZRd1IxdFljIiwiaWF0IjoxNzgyMDAwMDAwLCJleHAiOjE3ODI2MDQ4MDB9.AlSWIH5poAajFLD5xxXM2DOHZPL0jjdzdhfqDVgGk6g
```

**V-INV2 — role escalation attempt (expect: reject on integrity).** Identical payload except
`"roles": ["owner"]`, presented with V-INV1's unmodified MAC. The MAC over the modified body is
`bco2emM9xW85E8_ETEKveob6mXGZpNNJ77JkGkUp0ZM`; the token presents
`AlSWIH5poAajFLD5xxXM2DOHZPL0jjdzdhfqDVgGk6g`. Tests ZBP-4-R41.

```
inv_eyJ2IjoxLCJqdGkiOiIwMUpaUTlNNFQ3QzJYOEI1TjBLV0VSWkRBSCIsIm9yZyI6Im9yZ183RnEybUtkWDlwTHMiLCJlbWFpbCI6InNhbUBjdXN0b21lci5leGFtcGxlIiwicm9sZXMiOlsib3duZXIiXSwiaW52aXRlciI6InVzcl8zQm44dlF3UjF0WWMiLCJpYXQiOjE3ODIwMDAwMDAsImV4cCI6MTc4MjYwNDgwMH0.AlSWIH5poAajFLD5xxXM2DOHZPL0jjdzdhfqDVgGk6g
```

Note that integrity protection alone is not sufficient: even a correctly-MAC'd invitation must have
its roles re-validated against the inviter's current authority at redemption (ZBP-4-R40), because the
inviter may have been demoted in the seven days since.

### Decision matrix

The rest of this specification's behaviour is state-dependent rather than cryptographic, and is
verified as a decision table. A conforming implementation MUST produce these outcomes. Each row is a
test.

| # | Situation | Required outcome | Requirement |
|---|---|---|---|
| A1 | Valid session, active membership | allow | ZBP-4-R10 |
| A2 | Valid session, membership revoked 60s ago | **deny** | ZBP-4-R12, ZBP-4-R24 |
| A3 | Valid session, membership revoked in org X, active in org Y | deny for X, allow for Y | ZBP-4-R24 |
| A4 | Session's cached roles say admin; membership says member | treat as member | ZBP-4-R11 |
| A5 | Request names an org the user has no membership in | deny, indistinguishable from a non-existent org | ZBP-4-R13, ZBP-3-R20 |
| A6 | Password session, org later enabled SSO enforcement | **deny / force re-auth** | ZBP-4-R14, ZBP-4-R35 |
| A7 | Org requires MFA; session authenticated without it | deny, step up | ZBP-4-R27 |
| A8 | User recovers account by email; org enforces SSO | user restored, **org access still denied** | ZBP-4-R29 |
| A9 | Sign-in attempt for a non-existent account | same response and timing as a wrong password | ZBP-4-R30 |
| B1 | Invitation redeemed by a different email address | deny | ZBP-4-R39 |
| B2 | Invitation redeemed twice | second redemption denied | ZBP-4-R39 |
| B3 | Invitation granting `owner`, issued by a `member` | deny at issue and at redemption | ZBP-4-R40 |
| B4 | Invitation valid, but inviter demoted before redemption | re-validate; deny the excess roles | ZBP-4-R40 |
| C1 | Auto-join on an unverified domain | deny | ZBP-4-R31, ZBP-4-R32 |
| C2 | Second org claims an already-verified domain | deny | ZBP-4-R33 |
| C3 | Auto-join on a public email provider domain | deny | ZBP-4-R33 |
| C4 | SSO assertion naming an org other than the connection's | use the connection's org; deny the claim | ZBP-4-R34 |
| C5 | SSO assertion replayed | deny on assertion id single-use | ZBP-4-R34 |
| C6 | Customer IdP unreachable | new sessions fail closed; no password fallback | ZBP-4-R38 |
| C7 | SCIM deprovision received | membership, sessions, machine principals revoked ≤60s | ZBP-4-R36 |
| D1 | API key created by a user who has since left | key still valid; that person has no access | ZBP-4-R43 |
| D2 | API key scope exceeding its creator's authority | deny at creation | ZBP-4-R44 |
| D3 | Org enables SSO enforcement; personal API keys exist | personal keys closed; org-owned principals survive | ZBP-4-R35, ZBP-4-R43 |
| E1 | Operator accesses an org without a time-bound grant | deny | ZBP-4-R47 |
| E2 | Operator acts in an org | audit shows operator **and** org, not the customer as actor | ZBP-4-R48 |
| E3 | Support access occurs | visible in the customer's own audit log | ZBP-4-R49 |
| E4 | Operator writes during a read-only support session | deny | ZBP-4-R50 |

A2, A6, and D1 are the three that most often fail in a real implementation, and they fail silently —
nothing errors, the wrong person simply keeps working. A6 in particular cannot be caught by testing a
fresh sign-in, because the defect only exists for sessions that predate the configuration change.

## Threat model and failure modes

| Threat | Result |
|---|---|
| Employee removed from a customer, keeps their session | Membership re-checked per request; bounded revocation (ZBP-4-R11, ZBP-4-R12) |
| Employee removed, keeps a personal API key | Machine principals are org-owned and revoked with the membership (ZBP-4-R36, ZBP-4-R43) |
| Attacker registers an email at a customer's domain | Domain claims require a control proof (ZBP-4-R31) |
| Attacker claims a customer's domain in their own org | Domains are exclusive while claimed (ZBP-4-R33) |
| SSO assertion replayed or retargeted | Single-use assertion id; org from the connection (ZBP-4-R34) |
| Org enables SSO; attacker uses a pre-existing password session | Enforcement invalidates non-satisfying sessions (ZBP-4-R35) |
| Attacker uses account recovery to bypass SSO | Recovery restores the user, not the org (ZBP-4-R29) |
| Invitation tampered to grant `owner` | Integrity-protected and re-validated (V-INV2, ZBP-4-R40, ZBP-4-R41) |
| Session database disclosed | Tokens stored hashed; disclosure yields nothing usable (ZBP-4-R17) |
| Product operator browses customer data | No standing access; time-bound, audited, customer-visible (ZBP-4-R47…R49) |
| Cross-organisation data access through a valid session | Context handed to ZBP-3's enforcement point (ZBP-3-R9) |
| Customer's IdP compromised | **Their org is compromised.** This is inherent to federation and must be stated to customers, not engineered away |
| Product's own session store compromised | **Total.** Concentrating session authority is what buys instant revocation; it also concentrates this risk |

The last two rows are the honest limits. Federation delegates trust to the customer's directory — that
is what they are asking for. And a server-authoritative session store is a single high-value target,
which is the price of being able to end every session in the platform in one operation. Both are the
right trades for this problem; neither should be discovered during an incident review.

## Anti-patterns

- **Baking roles into the session.** Produces the removed-administrator bug, which is invisible until
  it matters (ZBP-4-R11).
- **A user record per organisation.** Breaks single sign-on, MFA enrolment, and account recovery, and
  makes "this person is at two customers" unrepresentable. It also multiplies the credential surface
  by the number of organisations (ZBP-4-R1).
- **Trusting the organisation id in the request.** It is a switch request to validate, not context
  (ZBP-4-R13, ZBP-3-R3).
- **SSO enforcement that only closes the login form.** Existing sessions, personal API keys, and
  recovery are the doors that matter (ZBP-4-R35).
- **Auto-join by email domain without verification.** Register a mailbox, join the organisation. The
  most direct tenancy takeover there is (ZBP-4-R31).
- **Personal API keys.** They leave with the person, or worse, do not (ZBP-4-R43).
- **Impersonation implemented as "log in as this user".** The audit trail then shows the customer
  performing the operator's actions, which is worse than no audit trail, because it is confidently
  wrong (ZBP-4-R48).
- **Standing operator access to all organisations.** One phished employee at the vendor is every
  customer's breach (ZBP-4-R47).
- **Recovery that bypasses org policy.** The recovery flow becomes the weakest sign-in method, and it
  is the one nobody threat-models (ZBP-4-R29).
- **Falling back to passwords when the customer's IdP is down.** Turns their outage into your breach
  (ZBP-4-R38).

## Rollout

Adding organisations to a product that has only users:

1. **Introduce organisation and membership records**, backfilling one organisation per existing user.
   Nothing changes behaviourally; every user is a sole member of their own organisation.
2. **Move authorisation reads onto membership** (ZBP-4-R11). Still one membership each, so still no
   behaviour change — but now the resolution path is correct.
3. **Add the active-organisation concept and switching** (ZBP-4-R13), with a real multi-membership
   test account.
4. **Add invitations** (ZBP-4-R39…R42). This is when organisations become genuinely multi-person and
   when role delegation first matters.
5. **Add the organisation security configuration** (ZBP-4-R6) with defaults matching current
   behaviour, so enabling it is a customer choice rather than a migration.
6. **Add federation, then enforcement** — in that order, and treat them as separate launches.
   Enforcement is the one that breaks people, because it must invalidate existing sessions
   (ZBP-4-R35).
7. **Add directory provisioning** (ZBP-4-R36, ZBP-4-R37). Deprovisioning is the part customers
   actually audit; build it first and demonstrate the revocation interval.
8. **Replace operator access with the audited, time-bound path** (ZBP-4-R47…R50). Do this before you
   need it in a customer security review, not during one.

Step 6 is the schedule risk. Every prior step is additive; enforcement is the first one that takes
access away from people who currently have it, and it needs a communicated window.

## Conformance checklist

**Model**

- [ ] One user record per person (ZBP-4-R1)
- [ ] Users survive with zero memberships (ZBP-4-R2)
- [ ] All identifiers opaque and non-enumerable (ZBP-4-R3)
- [ ] Email confers no access by itself (ZBP-4-R4)
- [ ] Email verified before use and re-verified on change (ZBP-4-R5)
- [ ] Org-owned security configuration, not weakenable by members (ZBP-4-R6)
- [ ] Org deletion revokes memberships, sessions, principals; keeps users (ZBP-4-R7)
- [ ] Multi-org membership with conflicting configs supported (ZBP-4-R8)

**Membership and resolution**

- [ ] Membership explicit; absence denies (ZBP-4-R9)
- [ ] Full resolution chain on every request (ZBP-4-R10)
- [ ] Roles and status read live, never from the token (ZBP-4-R11)
- [ ] Revocation effective ≤60s across every surface (ZBP-4-R12)
- [ ] Org switch validated against membership (ZBP-4-R13)
- [ ] Security config re-evaluated on switch (ZBP-4-R14)
- [ ] Last administrator cannot be removed without transfer (ZBP-4-R15)

**Sessions**

- [ ] Server-authoritative and immediately revocable (ZBP-4-R16)
- [ ] Opaque, ≥128 bits, stored hashed (ZBP-4-R17)
- [ ] Authentication method, MFA state, IdP, device recorded (ZBP-4-R18)
- [ ] Idle and absolute lifetimes, strictest-wins across orgs (ZBP-4-R19)
- [ ] Rotated on every privilege change (ZBP-4-R20)
- [ ] Cookie flags set; CSRF defence independent of the cookie (ZBP-4-R21)
- [ ] Users and admins can enumerate and revoke sessions (ZBP-4-R22)
- [ ] Credential change revokes other sessions (ZBP-4-R23)
- [ ] Session grants nothing in a revoked org (ZBP-4-R24)

**Credentials**

- [ ] Memory-hard hashing, breach-corpus check, no composition rules (ZBP-4-R25)
- [ ] Phishing-resistant factors supported and requirable (ZBP-4-R26)
- [ ] MFA requirement applies to every path in (ZBP-4-R27)
- [ ] Step-up on sensitive operations (ZBP-4-R28)
- [ ] Recovery cannot bypass org enforcement (ZBP-4-R29)
- [ ] No account enumeration; per-account and per-source rate limits (ZBP-4-R30)

**Federation**

- [ ] Domain control proof required before any effect (ZBP-4-R31)
- [ ] Auto-join requires verified domain and explicit enablement (ZBP-4-R32)
- [ ] Domains exclusive; public providers unclaimable (ZBP-4-R33)
- [ ] Assertions fully validated; org from the connection (ZBP-4-R34)
- [ ] Enforcement closes every other path and invalidates sessions (ZBP-4-R35)
- [ ] Deprovisioning revokes membership, sessions, principals (ZBP-4-R36)
- [ ] Directory-driven admin roles require explicit mapping (ZBP-4-R37)
- [ ] IdP outage fails closed; break-glass bounded and audited (ZBP-4-R38)

**Invitations**

- [ ] Single-use, expiring, email-bound, verified-address redemption (ZBP-4-R39)
- [ ] Roles bounded by inviter and re-validated at redemption (ZBP-4-R40)
- [ ] Integrity-protected and revocable (ZBP-4-R41)
- [ ] Redemption creates an explicit membership and is audited (ZBP-4-R42)

**Machine principals**

- [ ] Owned by the org, surviving their creator's departure (ZBP-4-R43)
- [ ] Named human owner, expiry, bounded scopes (ZBP-4-R44)
- [ ] Shown once, stored hashed, individually revocable, attributable (ZBP-4-R45)
- [ ] Sender-constrained where the client can hold a key (ZBP-4-R46)

**Support access**

- [ ] No standing access; per-incident and expiring (ZBP-4-R47)
- [ ] Both identities in the audit record (ZBP-4-R48)
- [ ] Customer-visible, approvable where required (ZBP-4-R49)
- [ ] Read-only by default; writes separately elevated (ZBP-4-R50)

**Audit**

- [ ] All authentication events recorded with full context (ZBP-4-R51)
- [ ] Orgs read their own records; no cross-org content (ZBP-4-R52)

**Test suite**

- [ ] V-INV1 and V-INV2 verify as stated
- [ ] Every row of the decision matrix is a test
- [ ] A2, A6, and D1 asserted explicitly — the three silent failures
- [ ] Revocation interval measured, not assumed (ZBP-4-R12)

## Rationale

Not speaking for people who have not consented is [ZFN-8](/zfn/8-dont-speak-for-anonymous-people/),
which is the root of the impersonation position here. That an agent — or an operator — acting with
someone else's credential is untraceable by design, and that delegation must be explicit, scoped, and
revocable, is [ZFN-38](/zfn/38-agents-are-principals/); that no automated actor is anonymous is
[ZFN-40](/zfn/40-no-anonymous-system-actor/). Verifying that a resource belongs to the caller rather
than inferring it from possession of an identifier is [ZFN-10](/zfn/10-verify-resource-owner/).
Sessions and support grants as leases that expire rather than standing rights is
[ZFN-37](/zfn/37-every-lock-is-a-lease/). And the standard for who gets to be inside a system at all —
including your own operators — is [ZFN-27](/zfn/27-no-assholes-strict-definition/) applied to access
rather than hiring: the privilege is conditional.

Two positions are specific to this document.

**The membership is the object.** Almost every serious bug in SaaS authentication comes from treating
the user or the session as the authorisation subject and then discovering that organisations revoke,
suspend, and reconfigure independently of either. Making membership the thing that is checked, live,
on every request, costs a lookup and removes an entire category.

**Sessions here are deliberately the opposite of workload tokens.** ZBP-2 says verification must be a
local computation and revocation is a rare, secondary path. This document says the session store is
authoritative and revocation is immediate. Both are correct for their traffic: one is millions of
five-minute credentials where a lookup per request would be architecturally fatal; the other is
human-scale, multi-day sessions where "log this person out now" is a routine, contractual requirement.
The mistake is not choosing either — it is choosing one and applying it to both.

## Changelog

- **2026-07-27** (1.0.0): First published. Specifies the user/organisation/membership model, the
  ten-step request resolution algorithm, server-authoritative sessions, credentials and step-up,
  domain verification and SSO enforcement, directory provisioning, invitations, org-owned machine
  principals, audited support access, and vectors V-INV1/V-INV2 plus the A/B/C/D/E decision matrix.
