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

# Security & Governance

> Token model, secret management, threat model, revocation levels, audit requirements and defense in depth.

Reference for security reviews and conversations with the Security team. As
always: **the IdP authenticates, it does not authorize applications**.

## 1. Token model

| Token                 | Format           | Typical TTL           | Issued by         | Validated by                 |
| --------------------- | ---------------- | --------------------- | ----------------- | ---------------------------- |
| Authorization code    | Opaque           | 5–10 min              | OAuth engine      | Engine (token endpoint)      |
| OAuth access token    | JWT ES256        | 30 min                | Engine + hook     | Client app via JWKS          |
| Refresh token         | Opaque, rotating | 30 days               | OAuth engine      | OAuth engine                 |
| ID token (OIDC)       | JWT ES256        | 30 min                | Engine + hook     | Client app                   |
| Internal B Brands JWT | JWT HS256        | Short (900s exchange) | Exchange endpoint | B Brands internal authorizer |

**Key distinction**: ES256 tokens prove **identity** and are consumed by the
**client app** — they never reach the B Brands APIs. The HS256 token is consumed
by the B Brands APIs via the existing internal authorizer.

### Canonical OAuth access token claims (identity + account dimension)

```json theme={null}
{
  "iss": "https://auth.bbrands.io",
  "sub": "<user_id uuid>",
  "aud": "bbrands-idp",
  "exp": 1734268800,
  "iat": 1734267000,
  "jti": "<uuid>",
  "email": "user@example.com",
  "client_id": "<client uuid>",
  "https://bbrands.io/client_handle": "<client_handle>",
  "https://bbrands.io/platform_type": "<platform_type>",
  "https://bbrands.io/profile": {
    "account": "<account.document_id | null>",
    "odoo": "<account.external_id | null>",
    "user": "<user_id uuid>"
  }
}
```

**Inviolable rules**: `iss` is always the per-environment `auth.*` URL; `aud` is
always the fixed IdP audience `bbrands-idp`; the requesting client is carried in
`https://bbrands.io/client_handle`; `jti` is always present and drives the
blacklist; there is **no** permissions or role claim. The
`https://bbrands.io/profile` claim carries tenancy context only: `account` is
the account bound to the user strictly through the `primary_user` role (one
primary user per account), `odoo` is that account's external system id (`null`
until the account is mapped by the migration API) and `user` mirrors `sub`.
The UserInfo endpoint returns the same object.

### Client-side validation

```typescript theme={null}
import { jwtVerify, createRemoteJWKSet } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://auth.bbrands.io/.well-known/jwks.json"),
  { cooldownDuration: 30_000, cacheMaxAge: 3_600_000 },
);

export async function validateAccessToken(token: string, myHandle: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: "https://auth.bbrands.io",
    audience: "bbrands-idp",
    algorithms: ["ES256"],
  });
  if (payload["https://bbrands.io/client_handle"] !== myHandle) {
    throw new Error("Token was issued for another client");
  }
  return payload; // identity + account profile, never permissions
}
```

## 2. Where authorization lives

| Scenario                                | Who authorizes | Mechanism                      |
| --------------------------------------- | -------------- | ------------------------------ |
| User acts inside a client app           | The client app | Its own permission/group model |
| Client app consumes the B Brands APIs   | B Brands       | Existing internal RBAC         |
| M2M process (no user) consumes the APIs | B Brands       | API key + per-key permissions  |

<Warning>
  The exchange JWT grants the client the user's **full** permissions in B Brands
  (no per-client least-privilege). Acceptable for trusted first-party clients;
  introduce scoping in the exchange if you need to narrow it.
</Warning>

## 3. Secret management

| Secret                          | Generated              | Stored in B Brands   | Stored in client      | Rotation                |
| ------------------------------- | ---------------------- | -------------------- | --------------------- | ----------------------- |
| OAuth `client_secret`           | B Brands at onboarding | hashed in the engine | client secret manager | 90 days                 |
| Provisioning webhook secret     | B Brands at onboarding | for signing          | to verify             | 180 days                |
| Internal signing secret (HS256) | B Brands               | server env           | n/a                   | On suspected compromise |
| Engine service key              | Engine                 | server env           | n/a                   | 365 days                |
| ES256 signing key               | Engine                 | managed KMS          | n/a                   | Annual, with overlap    |
| API key (M2M)                   | B Brands               | hashed at rest       | client secret manager | Existing policy         |

Storage rules: never in the repository, never in logs (mask `Authorization`
headers), never in webhook payloads, encrypted at rest, access restricted to IdP
admins.

### `client_secret` rotation

<Steps>
  <Step title="Admin clicks Rotate">B Brands generates a new secret.</Step>
  <Step title="Grace period">Old and new are accepted simultaneously for a configurable grace window (default 24h).</Step>
  <Step title="Client updates config">At any point during the grace window.</Step>
  <Step title="Grace ends">The old secret stops being accepted; audit logs the rotation.</Step>
</Steps>

### ES256 signing key rotation

The engine supports `kid`-based rotation. Generate the new key, keep both active,
wait the max access-token TTL (30 min) + buffer (1h), retire the old key, then
remove after retention. The JWKS endpoint must expose both during the window.

## 4. Threat model (mini-STRIDE)

| #   | Threat                               | Mitigation                                               |
| --- | ------------------------------------ | -------------------------------------------------------- |
| T1  | `client_secret` theft                | Rotation, anomalous-use alerts, optional IP allowlist    |
| T2  | Authorization code interception      | TLS + PKCE + `state`                                     |
| T3  | Token replay                         | `jti` + blacklist + short TTL                            |
| T4  | Consent-screen phishing              | Clear UI, no auto-approve without proper `trust_level`   |
| T5  | Long-TTL exchange JWT                | Mandatory short TTL + blacklist                          |
| T6  | User compromise                      | MFA in B Brands, anomalous-login alerts, kill switch     |
| T7  | API key theft (M2M)                  | Least privilege, rotation, audit                         |
| T8  | Webhook replay                       | Idempotent `event_id`                                    |
| T9  | Forged webhook                       | HMAC signature + IP allowlist                            |
| T10 | Hook compromise                      | Code review, deploy from main, change audit              |
| T11 | Client A token presented to client B | Each client validates `https://bbrands.io/client_handle` |
| T12 | Timing attack on secret check        | Constant-time comparison                                 |
| T13 | Revocation race                      | Blacklist checked on every validation                    |

## 5. Revocation levels

| Level                        | Scope                          | Immediate effect                           | How                                 |
| ---------------------------- | ------------------------------ | ------------------------------------------ | ----------------------------------- |
| L1: Single token             | One access / exchange token    | Yes (blacklist)                            | `POST /oauth/revoke`                |
| L2: User session             | All user refresh tokens        | Next refresh fails                         | Engine admin API                    |
| L3: User × platform          | Tokens for that client/user    | Next refresh/exchange fails (≤ TTL)        | `DELETE /idp/access/:user/:client`  |
| L4: Whole client             | All tokens of that client      | Next refresh fails; live tokens last ≤ TTL | `POST /idp/clients/:handle/suspend` |
| L5: Whole user (kill switch) | All enablements, all platforms | Immediate if combined with blacklist       | Kill-switch routine                 |

## 6. Audit requirements

* **Immutable**: append-only, no UPDATE/DELETE.
* **Traceable**: `actor`, `ip`, `user_agent`, `correlation_id`.
* **Retention**: 12 months online, longer in cold storage.
* **Exportable**: `/idp/audit/users/:id/export` for GDPR/compliance.
* **Never logged**: full tokens (only `jti` prefix), plaintext secrets, passwords.

## 7. Defense in depth

| Layer          | Control                                                             |
| -------------- | ------------------------------------------------------------------- |
| Network        | TLS, HSTS, strict CORS, WAF                                         |
| OAuth client   | PKCE, `state`, exact redirect URI, validate `iss` + `client_handle` |
| OAuth endpoint | Rate limit, strict validation, audit                                |
| Hook           | Enablement check, failsafe-deny on error                            |
| Exchange       | Verify subject token + enablement + short TTL + blacklist           |
| Internal token | HS256, internal RBAC, blacklist                                     |
| Operational    | Admin MFA, audit log, alerts, runbooks                              |
